JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM高效稳定运行。
经典的JVM内存布局如图:

线程公用:Heap
、Metaspace
线程私有:Native Method Stacks、Program Counter Register、JVM Stacks
1. Heap(堆区)
Heap区是OOM(内存溢出)故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用。堆的内存既可以固定大小,也可以在运行是动态地调整,通过以下参数设定初始值和最大值,如:-Xms256M -Xmx1024M,其中 -X 表示他是JVM运行参数,ms是memory start的简称,mx是memory max的简称,分别代表最小堆容量和最大堆容量。通常情况下,服务器在运行过程中,堆空间不断的扩容和收缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM 的 ms 和 mx 设置成一样大小,避免在GC后调整堆大小时带来的压力。
-
堆分为两块:分别为新生代和老年代。对象产生之初在新生代,步入暮年之时在老年代,老年代也接纳在新生代无法容纳的超大对象。新生代=1个Eden区 + 2个survivor区。绝大部分对象在Eden区生成,当Eden区填满的时候,会触发YGC(Young Garbage Colleciton)。垃圾回收的时候,在Eden区实现清楚策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区。
-
Survivor区又分为 S0 和 S1 两块内存空间。每次 YGC 的时候,它们将存活的对象赋值到未使用的那块空间,然后将当前正在使用的那块空间完全清空,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor容量的上限,则直接移交老年代。

-
假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次 YGC 都会 +1。-XX:MaxTenuringThreshold参数配置能配置计数器的值达到某个阈值后,对象从新生代晋升到老年代。如果该参数配置为1,那么直接从Eden区移送到老年代。默认值为15,即在Survivor区交换14次后晋升至老年代。
-
如果在 Survivor 区无法放下,或者超大对象的阈值超过上限,则尝试在老年代分配,如果老年代也放不下,则会出发FGC(Full Garbage Collection),如果依然无法放下,则会抛出OOM。
2. Metaspace(元空间)
在JDK1.8中,因永久代(Perm)区大小固定,很难进行调优,并且FGC的时候会移动类元信息,已经被淘汰。代替它的便是元空间。区别于永久代,元空间在本地内存中分配。在JDK8里,Perm区中的所有内容中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内。
3. JVM Stack(虚拟机栈)
JVM中的虚拟机栈是描述 Java 方法执行的内存区域,它是==线程私有==的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中只有位于栈顶的帧
才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时 所有指令都只能针对当前栈帧进行操作。

-
操作栈:
操作栈是一个初始值为空的桶式结构结构栈。
public int simpleMethod() {
int x = 11;
int y = 12;
int z = x + y;
return z;
}
详细的字节码操作顺序:
public simpleMethod();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack = 2 , locals = 4, args_Size = 1 //最大栈深度为2,局部变量个数为4
BIPUSH 11 // 常量11压入操作栈
ISTORE_1 // 保存到局部变量表 slot_1 中(第一处)
BIPUSH 12 // 常量12压入操作栈
ISTORE_2 // 保存到局部变量表 slot_2 中
ILOAD_1 //把局部变量表的 slot_1 元素(int x)压入操作栈
ILOAD_2 //把局部变量表的 slot_2 元素(int y)压入操作栈
IADD // 把上方的两个数都取出来,在CPU里加一下,并压回操作栈的栈顶
ISTORE_3 // 把栈顶的结果存储到局部变量表 slot_3 中
ILOAD_3
IRETURN // 返回栈顶元素值
-
方法返回地址:
方法退出的过程相当于退出当前栈帧,退出可能有三种方式:
-
返回值压入上层调用枝帧。
-
异常信息抛给能够处理的枪帧。
-
PC 数器指向方法调用后的下一条指令。
4. Native Method Stack(本地方法栈)
本地方法栈( Native Method Stack)在JVM内存布局中也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。这个“内外”是针对JVM来说的,本地方法栈为Native方法服务。线程开始调用本地方法时,会进入一个不再受JVM约束的世界。本地方法可以通过 JNI ( Java Native Int rface )来访问虚拟机运行时的数据区 ,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。当大量本地方法出现时势必会削弱JVM对系统的控制力,因为它的出错信息都比较黑盒。对于内存不足的情况 本地方法栈还是会抛出 native heap OutOfMemory。
5. Program Counter Register(程序计数寄存器)
在程序计数寄存器( Program Counter Register, PC )中, Register 的命名源于
CPU 的寄存器,CPU 只有把数据装载到寄存器才能够运行。寄存器存储指令相关的
现场信息,由于 CPU 时间片轮限制,众多线程在并发执行过程中,任何一个确定的
时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生
自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,
线程执行或恢复都要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域
也不会发生内存溢出异常
总结:
最后,从线程共享的角度来看,堆和元空间是所有线程共享的,而虚拟机栈、本地方法栈、程序计数器是线程内部私有的,从这个角度看一个 Java 内存结构

注明:笔记出自书本《码出高效》
网友评论