JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

image.png

jvm架构图

对应了类的加加载过程 :加载,链接(验证,准备,解析),初始化
image.png

JVM运行时内存

1.7

image.png

1.8

image.png

1.8之后jvm的变化,主要在方法区的变化

image.png
方法区在8之后的变化:

移除了永久代(PermGen),替换为元空间(Metaspace) 永久代中的class metadata(类元信息)转移到了native memory(本地内存,而不是虚拟机) 永久代中的interned Strings(字符串常量池) 和 class static variables(类静态变量)转移到了Java heap 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize)

PC寄存器

程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行
的字节码的行号指示器(存储指令地址的)。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条
需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

示例

image.png

多个线程如何暂停恢复线程并继续向下执行的

由于pc是线程私有的,每个线程都有自己的记录,暂停时会保存当前的指令地址回复的时候再拿到当前的指令地址即可

PC寄存器的特点

(1)区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟
机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的
地址。
(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过
程。
虚拟机栈记录了程序执行流程的轨迹。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入
栈到出栈的过程。
image.png

局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
每个#号开头的都指向了运行时常量池中的值
image.png

操作数栈

操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
总的来说,操作数栈要和局部变量表相互配合,操作数栈用来记录执行动作,局部变量表是存储空间。
看一个简单的加法操作
image.png
加载变量到操作数栈,随后将操作数栈的内容存入局部变量表
image.png
从局部变量表中获取存入的值,在操作数栈中进行计算后再将计算好的值放入局部变量表中
image.png

动态链接

每个栈帧都包含一个指向运行时常量池[1]中该栈帧所属方法的引用(就是那个方法的符号引用),持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存 有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态分派。
#开头的这段
image.png

方法的返回地址

方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

动态链接扩展-方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本 (即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作之一,Class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用。而不是方法在实际运行时内存布局中的入口地址(也就是之前说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使 得Java方法调用过程变得相对复杂,某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不 可改变的。调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法 的调用被称为解析(Resolution)。
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要有静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写出其他版本,因此它们都适合在类加载阶段进行解析。

调用不同类型的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持以下5条方法调用字 节码指令,分别是: ·invokestatic。用于调用静态方法。 ·invokespecial。用于调用实例构造器()方法、私有方法和父类中的方法。 ·invokevirtual。用于调用所有的虚方法。 ·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。 ·invokedynamic。先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。前面4 条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户设定的引 导方法来决定的。 只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方法就被称为“虚方法”(Virtual Method)。

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号 引用全部转变为明确的直接引用,不必延迟到运行期再去完成。

分派

Java具备面向对象的3个基本特征:继承、封装和多态。分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在 Java虚拟机之中是如何实现的。
静态分派:
“分派”(Dispatch)这个词本身就具有动态性,一 般不应用在静态语境之中,这部分原本在英文原版的《Java虚拟机规范》和《Java语言规范》里的说法都是“Method Overload Resolution”,即应该归入“解析”里去,但部分其他外文资料和国内翻译的许多中文资料都将这种行为称为“静态分派”。
为了解释静态分派和重载(Overload),准备了一段经常出现在面试题中的程序代码

  1. /**
  2. * 方法静态分派演示
  3. */
  4. public class StaticDispatch {
  5. static abstract class Human {
  6. }
  7. static class Man extends Human {
  8. }
  9. static class Woman extends Human {
  10. }
  11. public void sayHello(Human guy) {
  12. System.out.println("hello,guy!");
  13. }
  14. public void sayHello(Man guy) {
  15. System.out.println("hello,gentleman!");
  16. }
  17. public void sayHello(Woman guy) {
  18. System.out.println("hello,lady!");
  19. }
  20. public static void main(String[] args) {
  21. Human man = new Man();
  22. Human woman = new Woman();
  23. StaticDispatch sr = new StaticDispatch();
  24. sr.sayHello(man);
  25. sr.sayHello(woman);
  26. }

运行结果:
hello,guy!
hello,guy!
先来看一行代码Human man = new Man();代码中的“Human”称为变量的“静态类型”(Static Type),或者叫“外观类 型”(Apparent Type),后面的“Man”则被称为变量的“实际类型”(Actual Type)或者叫“运行时类型”(Runtime Type)。静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

  1. // 实际类型变化
  2. Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
  3. // 静态类型变化
  4. sr.sayHello((Man) human)
  5. sr.sayHello((Woman) human)

也就是说,重载的实际调用者在运行期才能知道是谁。到底是Man还是Woman,必须等到程序运行到这行的时候才能确定。而human的静态类型是Human,也可以在使用时(如 在上面代码中sayHello()方法中的强制转型)临时改变这个类型,但这个改变仍是在编译期是可知的,两次sayHello() 方法的调用,在编译期完全可以明确转型的是Man还是Woman。
再回到,开头的示例代码中,main()里面的两次sayHello()方法调用,在方法接收者已经确定是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中故意定义了两个静态类型相同,而实际类型不同的变量,但虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。
所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。这点也是为何一些资料选择把它归入“解析”而不是“分派”的原因。

动态分派
它与Java语言多态性的另外 一个重要体现[3]——重写(Override)有着很密切的关联。

  1. /**
  2. * 方法动态分派演示
  3. */
  4. public class DynamicDispatch {
  5. static abstract class Human {
  6. protected abstract void sayHello();
  7. }
  8. static class Man extends Human {
  9. @Override
  10. protected void sayHello() {
  11. System.out.println("man say hello");
  12. }
  13. }
  14. static class Woman extends Human {
  15. @Override
  16. protected void sayHello() {
  17. System.out.println("woman say hello");
  18. }
  19. }
  20. public static void main(String[] args) {
  21. Human man = new Man();
  22. Human woman = new Woman();
  23. man.sayHello();
  24. woman.sayHello();
  25. man = new Woman();
  26. man.sayHello();
  27. }
  28. }

运行结果:
man say hello
woman say hello
woman say hello
这里在java中是怎么确定调用哪个方法的?显然这里选择调用的方法版本是不可能再根据静态类型来决定的,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时产生了不同的行为,甚至变量man在两次调用中还执行了两个不同的方法。
通过javap看一下main的字节码

  1. public static void main(java.lang.String[]);
  2. Code:
  3. Stack=2, Locals=3, Args_size=1
  4. 0: new #16; //class org/fenixsoft/polymorphic/DynamicDispatch$Man
  5. 3: dup
  6. 4: invokespecial #18; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Man."<init>":()V
  7. 7: astore_1
  8. 8: new #19; //class org/fenixsoft/polymorphic/DynamicDispatch$Woman
  9. 11: dup
  10. 12: invokespecial #21; //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
  11. 15: astore_2
  12. 16: aload_1
  13. 17: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
  14. 20: aload_2
  15. 21: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
  16. 24: new #19; //class org/fenixsoft/polymorphic/DynamicDispatch$Woman
  17. 27: dup
  18. 28: invokespecial #21; //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
  19. 31: astore_1
  20. 32: aload_1
  21. 33: invokevirtual #22; //Method org/fenixsoft/polymorphic/Dynamic Dispatch$Human.sayHello:()V
  22. 36: return

0~15行的字节码是准备动作,作用是建立man和woman的内存空间、调用Man和Woman类型的实 例构造器,将这两个实例的引用存放在第1、2个局部变量表的变量槽中,这些动作实际对应了Java源码中的这两行:
Human man = new Man();
Human woman = new Woman();
接下来的16~21行是关键部分,16和20行的aload指令分别把刚刚创建的两个对象的引用压到栈顶,这两个对象是将要执行的sayHello()方法的所有者,称为接收者(Receiver);
17和21行是方法调用指令,这两条调用指令单从字节码角度来看,无论是指令(都是invokevirtual)还是参数(都是常量 池中第22项的常量,注释显示了这个常量是Human.sayHello()的符号引用)都完全一样,但是这两句指令最终执行的目标方法并不相同。那看来解决问题的关键还必须从invokevirtual指令本身入手。
根据《Java虚拟机规范》,invokevirtual指令的运行时解析过程大致分为以下几步:
1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果
通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
3)否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令并不是把常量池中方法的符号引用解析到直接引用上就结束了,还会根据方法接收者的实际类型来选择方法版本,这个过程就是Java语言中方法重写的本质。把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
虚拟机动态分派的实现
动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的 方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据。面对这种情况,一种基础而且常见的优化手段是为类型在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也 会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。
image.png
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。在图8-3中,Son重写了来自Father的全部方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。
查虚方法表是分派调用的一种优化手段,由于Java对象里面的方法默认(即不使用final修饰)就是虚方法,虚拟机除了使用虚方法表之外,为了进一步提高性能,还会使用类型继承关系分析(Class Hierarchy Analysis,CHA)、守护内联(Guarded Inlining)、内联缓存(InlineCache)等多种非稳定的激进优化来争取更大的性能空间。

本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
特点:1.加载native方法,java有的时候不能满足一些要求就需要借用操作系统底层的接口做事情,例如创建线程
2.虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机提供底层接口服务
3.线程私有的,生命周期同线程相同。
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常: (1)StackOverFlowError :线程请求的栈深度>所允许的深度。
(2)OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存

堆空间

是虚拟机管理的内存中最大的一块,几乎所有的java对象都存放在这里面。仍有一部分对象也不在堆中存储,由于即时编译技术的进步,逃逸分析的技术也在不断改进,因此在栈上分配、标量替换优化手段已经实现。

逃逸分析:在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。逃逸分析确定某个指针可以存储的所有地方,以及确定能否保证指针的生命周期只在当前进程或在其它线程中。Java的逃逸分析只发在JIT的即时编译中,Java的分离编译和动态加载使得前期的静态编译的逃逸分析比较困难或收益较少,所以目前Java的逃逸分析只发在JIT的即时编译中,因为收集到足够的运行数据JVM可以更好的判断对象是否发生了逃逸。 逃逸条件判定:一、对象被赋值给堆中对象的字段和类的静态变量。二、对象被传进了不确定的代码中去运行。如果满足了以上情况的任意一种,那这个对象JVM就会判定为逃逸。对于第一种情况,因为对象被放进堆中,则其它线程就可以对其进行访问,所以对象的使用情况,编译器就无法再进行追踪。第二种情况相当于JVM在解析普通的字节码的时候,如果没有发生JIT即时编译,编译器是不能事先完整知道这段代码会对对象做什么操作。保守一点,这个时候也只能把对象是当作是逃逸来处理。 逃逸分析的优化:

  • 将堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。
  • 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
  • 分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

image.png
特点:
1.堆是jvm所有线程共享的。
同时,堆中也包含每个私有的线程缓冲区Thread Local Allocation Buffer (TLAB)
2.在虚拟机启动的时候创建
3.为对象开辟空间
4.Java垃圾收集器管理的主要区域
5.堆在计算机物理存储上不连续、逻辑上是连续的,也是可以大小调节的,通过-Xms和-Xmx控制
6.方法结束后,对象不会立即移出堆,而是等待垃圾回收的时候才移除堆中
7.如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

堆的总大小可以是固定的,但是在堆中内部的分布free/total是动态分布的。

内存大小-Xmx/-Xms
使用示例: -Xmx20m -Xms5m

堆的分类

1.7

青年代:Eden,from survivor,to survivor
老年代:Old Memory
永久代:Perm
image.png

1.8

1.8之后去掉了永久代
image.png

年轻代和老年代

image.png
1.年轻代:主要存放新创建的对象,垃圾回收会比较频繁。年轻代分Eden,From,To三个区,
2.老年代:主要存生命周期比较长的对象,内存大小相比比较大,垃圾回收没有那么频繁。
image.png

新生代和老年代结构比

默认:新生代:老年代 = 1:2
默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5

新生代中:默认->Eden :From :To = 8 :1 :1
可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8
几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁.
image.png

对象的分配的过程

1.对象创建好后首先放入Eden区,如果是大对象(需要大量连续内存空间eg:长字符串)直接进入老年代。
“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。
2.当Eden内存不足时触发Minor GC,没有被清理的对象进入From区,对象有个位置用于记录GC次数,每当GC一次会累加一次
3.此时又出发了一次GC,在From被这次GC过之后仍然存在的对象将进入To区
4.经过不断的GC当一个对象被GC了超过15次之后,该对象将进入老年代
可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
image.png

元空间(方法区的部分实现)

1.7之前 把方法区当成永久代进行GC,1.8之后将方法区移至元空间并移除永久代。

永久代和元空间异同

存储位置不同:永久代物理上是堆的一部分,元空间属于本地内存
存储内容不同:永久代里面存了元数据信息、静态变量、运行时常量池等。现在将类元信息存在元空间中,并将内部字符串和、运行时常量池、类静态信息移动到Java堆中。也就是将原来永久代中的东西分配到不同的地方。
image.png

为什么要废弃永久代

1.从1.7开始虚拟机由HopspotVM和JRockitVM融合在一起,在JRockitVm中没有永久代的这个概念(java虚拟机规范说的)
2.永久代是由虚拟机内存管理,其中存了需多的类元数据,不易掌控它的大小,过大则浪费空间,过小则会造成内存溢出。
3.永久代GC的效率很低,不易回收

废弃的好处

1.元数据区分配在本地内存中,最大的分配空间就是计算机的内存大小,不易遇到内存溢出。(元空间的出现就是为了解决突出的类和类加载器元数据过多导致的OOM问题)
2.将运行时常量池从PermGen分离出来放入堆中,与类的元数据分开,提升类元数据的独立性。
3.将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。通过这个参数进行设置“MaxMetaspaceSize”。
4.永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

方法区(一种规范)

《虚拟机规范》:该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。

主要存储了类的常量,静态变量、即时编译后的代码等数据(方法区是堆的一个逻辑部分)
(1.8)元空间、(1.7)永久代是方法区的实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载类信息。方法区是jvm虚拟机的一种规范。

特点

方法区与堆一样是各个线程共享的内存区域
方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
方法区的大小跟堆空间一样 可以选择固定大小或者动态变化
方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类 导致方法区溢出虚拟机同样会跑出
(OOM)异常(Java7之前是 PermGen Space (永久代) Java 8之后 是MetaSpace(元空间) )
关闭JVM就会释放这个区域的内存

方法区内部结构

image.png

方法区的大小

jdk7:
通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
-XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。
jdk8:
元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定
默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。

方法区中的类也是会被垃圾回收的 条件: 首先该类的所有实例对象都已经从Java堆内存里被回收 其次加载这个类的ClassLoader已经被回收 最后,对该类的Class对象没有任何引用

运行时常量池

常量池&运行时常量池

字节码文件:中部包含了常量池
方法区中:内部包含了运行时常量池
常量池: 存放编译期间生成的各种字面量与符号引用,这部分内容在类加载后放入运行时常量池。所有变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用了另外方法时,就是通过常量池中指向方法的符号引用来表示的。
运行时常量池:常量池在运行时的表现形式,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来
的直接引用也存储在运行时常量池中。
常量池和运行时常量池的联系:编译后的字节码文件中包含类型信息、域信息、方法信息等。通过classLoader将字节码文件中的常量池信息加载到内存中,并存储到运行时常量池中。并且这两个东西存放的位置是不同的一个是在文件中,一个是在内存中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。

image.png

直接内存

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。引入一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。
image.png

原理: 普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuffer时,系统总是会使用一个“内核缓冲区”进行操作。 而DirectBuffer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuffer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuffer更快。