JVM:内存布局与垃圾回收

内存布局

 

JVM:内存布局与垃圾回收

Heap 堆区

Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象。

堆由垃圾收集器自动回收,堆区由各子线程共享使用。

堆的内存空间既可以固定大小,也可以在运行时动态地调整,但调整带来的扩容与回缩会形成不必要的系统压力,所以在线上成产环境中,JVM的最小堆容量Xms最大堆容量Xmx通常设置成一样的大小。

堆分成两大块:新生代老年代

对象产生之初在新生代,步入暮年时进入老年代。同时,老年代也接纳在新生代无法容纳的超大对象。

新生代通常由1个Eden区 + 2个Survivor区组成,绝大部分对象在Eden区生成,当Eden区装满会触发YGC:

1. 在Eden区实现清除策略,没有被引用的对象直接回收,依然存活的对象被移送到Survivor区。

2. 此时将Survivor区内存活对象复制到未使用的那块空间,并将当前空间完全清除。

3. 如果要移送的对象大于Survivor区域容量上限,则直接移送给老年代

4. 如果老年代也无法放下,则触发FGC。

5. FGC后仍然无法放下,则抛出OOM。

存活对象不会一直横跳在Survivor区,默认交换上线次数为14,第15次直接晋升至老年代

为JVM设置运行参数-XX:+HeapDumpOnOutOfMemortyError可以让JVM在OOM时输出堆内信息

JVM:内存布局与垃圾回收

 

Metaspace 元空间

Metaspace的前身Perm区在JDK8中已经被淘汰,在JDK7及之前的版本中,Hotspot才有Perm区,即永久代。

Perm的大小在启动时固定,很难进行调优。当发生FGC后,如果动态加载类过多,容易出现Perm区OOM。

JDK8使用元空间替换Perm,区别在于,元空间在本地内存中分配。

Perm区的所有内容中,字符串常量移至堆内存,其他内容移动至元空间内。

 

JVM Stack 虚拟机栈

栈是一个先进后出的数据结构,类似于弹夹,撞针从顶部开始访问。

JVM是基于栈结构的运行环境,因其移植性好,可控性强。

虚拟机栈是描述Java方法执行的内存区域,每个方法从开始调用到执行完成的过程就是栈帧从入栈到出栈的过程。

* 在活动线程中,只有位于栈顶的帧才是有效的,被称为当前栈帧。

* 正在执行的方法被称为当前方法,栈帧是方法运行的基本结构。

* 在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。

JVM:内存布局与垃圾回收

1)局部变量表

用于存放方法参数和局部变量

2)操作栈

一个初始状态为空的桶式结构栈。

在方法执行过程中,会有各种指令向栈中写入和提取信息;

“JVM的执行引擎是基于栈的执行引擎”,其中栈指的就是操作栈。

“i++和++i的区别”

前者是先将i从局部变量表取出压入栈顶,然后在变量表中实现了+1操作,此时对栈顶元素并没有影响。

后者是先在局部变量表中进行+1操作,再取数值压入栈顶并赋值。

所以i++并非原子操作,即使通过volatile关键字修饰,多线程同时写也会产生互相覆盖的问题。

3)动态链接

每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程中的动态连接。

4)方法返回地址

方法执行时有正常退出和异常退出两种退出情况,退出的过程相当于弹出当前栈帧,方式有三:

* 返回值压入上层调用栈帧

* 异常信息抛给能处理的栈帧

* PC计数器指向方法调用后的下一条指令

 

Native Method Stacks 本地方法栈

本地方法栈为Native方法服务,与虚拟机栈一样都是线程对象私有的。

线程调用本地方法时,会进入一个不再受JVM约束的世界,本地方法通过JNI访问虚拟机,具有和JVM相同的能力和权限。

大量本地方法出现会削弱JVM对系统的控制力,内存不足时会抛出Native heap OutOfMemory。

JNI:Java Native Interface,本地方法,如System.currentTimeMillis()。

 

Program Counter Register 程序计数寄存器

由于CPU时间片轮限制,在众多线程并发执行过程中的任何一个时刻,一个内核都只会执行某个线程中的一条指令,这必然会经常发生中断或恢复。为了保证不影响执行结果,每个线程在创建后都会产生自己的程序计数器和栈帧

程序计数器用来存放执行指令的偏移量和信号指示器等,线程的执行或恢复都要依赖它。

程序计数器在各个线程之间互不影响,此区域也不会发生OOM异常。

 

垃圾回收

垃圾回收的主要目的是清除不再使用的对象,自动释放内存。

JVM引入了GC Roots来判断对象是否存活,如果一个对象与GC Roots之间没有直接或间接的引用关系,那么该对象可被回收。

“标记-清除算法”

该算法从每个GC Roots出发,依次标记有引用关系的对象,最后将没有被标记的对象清除。

该算法会带来大量的空间碎片,会导致在分配一个较大连续空间时容易出发FGC。

“标记-整理算法”

该算法同样从GC Roots出发标记存活对象,然后将存活对象整理到内存空间的一端,形成连续的已使用空间,最后清理剩下的部分。

该算法类似计算机的磁盘整理。

“Mark-Copy算法”

为了能够并行地标记和整理将空间分为两块,每次只**其中一块,垃圾回收时把存活的对象复制到另一块未**的空间上,清除当前空间。

堆内存空间分为较大的Eden和两块较小的Survivor,每次只使用一块Survivor,这种情况下的“Mark-Copy”减少了内存空间的浪费。

该算法现作为主流的YGC算法进行新生代的垃圾回收。

 

垃圾回收器

垃圾回收器(Garbage Collector)是实现垃圾回收算法并应用在JVM环境中的内存模块。

Serial回收器

Serial回收器主要应用于YGC的垃圾回收,采用串行单线程的方式完成GC任务。

在收回的某个阶段会引发STW(Stop the world),暂停整个应用程序的执行,所以FGC得时间相对较长,频繁FGC会严重影响应用程序性能。

JVM:内存布局与垃圾回收

CMS回收器

Concurrent Mark Sweep Collector是收回停顿时间比较短、目前比较常用的垃圾回收器。

它通过初始标记、并发标记、重新标记、并发清除四个步骤完成GC工作,虽然第1、3步依然会引发STW,但第2、4步可以和应用程序并发执行。

CMS回收器采用“标记-清除算法”,会产生大量的空间碎片。

CMS可以通过配置-XX:UseCMSCompactAtFullCollection参数来强制JVM在FGC完成后对老年代进行压缩、执行一次空间碎片整理,但这也会引发STW。

为了减少STW次数,CMS还可以配置-XX:CMSFullGCsBeforeCompaction=n参数来延后老年代执行空间碎片整理,这样该行为将会发生在执行了n次FGC后。

G1回收器

Hotspot在JDK7中推出了新一代G1(Garbage-First Garbage Collector)垃圾回收器,通过-XX:+UseG1GC参数启用。

和CMS相比,G1具备压缩功能,能避免碎片问题,且暂停时间更加可控,性能总体上优秀一些。

在JDK11中G1作为默认垃圾回收器使用。

JVM:内存布局与垃圾回收

G1将Java堆空间分隔成了若干相同大小的区域,即region,包括Eden、Survivor、Old、Humongus四种类型。

其中Humongus是特殊的Old类型,专门放置大型对象。这样的划分方式意味着不需要一个连续的内存空间管理对象。

G1会优先回收垃圾最多的区域,采用“Mark-Copy算法”,具有非常好的空间整理能力,不会产生大量的空间碎片。

G1的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成GC。

 

G1的四种region都在堆内存中,执行GC时使用4个worker并发执行:

1. Initial Mark,标记GC Roots直接可达的存活对象,其实就是YGC,会引起STW。

2. Root Region Scan,根区域扫描,并发地从上一阶段标记的存活区域中扫描被引用的老年代对象。

3. Concurrent Mark,并发标记,从堆中标记存活的对象,类似CMS。

4. Remark,重新标记,完成最终的标记处理,类似CMS。

5. Cleanup,统计所有堆区域的存活对象,并将待回收区域按回收价值排序,为Mixed GC做准备。

ZGC收集器

JDK11引入、可伸缩低延迟的实验性垃圾收集器,据说暂停时间不超过10ms。

 

整理自《Easy Coding》。