JVM垃圾回收器详解
年轻代与老年代
我们知道在分代GC算法中,将我们的堆内存分为了年轻代与老年代,那为什么要将内存分为年轻代和老年代呢?
- 可以通过调整年轻代和老年代的比例来适应不同类型的应用程序,提高内存的利用率和性能.
- 新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
- 分代的设计中允许只回收新生代(minor gc),如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。
垃圾回收器的组合
垃圾回收器是垃圾回收算法的具体实现
由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用:
虚线表示已经被废弃
Serial-Serial Old
Serial
他是一种单线程串行回收年轻代的垃圾回收器,采用复制算法,由于是单线程,其优点就是单CPU处理器下吞吐量非常出色,但缺点也很明显,多CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间的等待,适用场景是Java编写的客户端程序或者硬件配置有限的场景
SerialOld
SerialOld是Serial垃圾回收器的老年代版本,与Serial同样是单线程串行执行的,但是使用的垃圾回收算法是标记-整理算法
其优缺点与Serial完全相同,在启动程序时,可添加虚拟机参数选择单线程垃圾回收器:
-XX:+UseSerialGC
:新生代、老年代都使用串行回收器
ParNew-CMS
ParNew
该垃圾回收器本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收,与Serial同样采用复制算法,其优点是多CPU处理器下停顿时间较短但其缺点是吞吐量和停顿时间不如G1所以在JDK9之后不建议使用,使用场景是JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用,可添加虚拟机参数:
-XX:+UseParNewGc
:新生代使用ParNew回收器,
CMS
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间,其优点是系统由于垃圾回收出现的停顿时间较短,用户体验好,但缺点也很多:1.内存碎片问题、 2.退化问题、 3.浮动垃圾问题,适用于大型的互联网系统中用户请求数据量大、频率高的场景比如订单接口、商品接口等
CMS执行步骤:
- 初始标记,用极短的时间标记出GC Roots能直接关联到的对象
- 并发标记,标记所有的对象,用户线程不需要暂停
- 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记
- 并发清理,清理死亡的对象,用户线程不需要暂停
CMS的缺点:
- CMS使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS会在Full GC时进行碎片的整理。这样会导致用户线程暂停,可以使用**-XX:CMSFullGCsBeforecompaction=N**参数(默认0)调整N次Ful GC之后再整理
- 无法处理在并发清理过程中产生的**“浮动垃圾”**,不能做到完全的垃圾回收
- 如果老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代
JDK14后就废弃了CMS
PS-PO
Parallel Scavenge
Parallel Scavenge是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量,具备自动调整堆内存大小的特点,其优点是吞吐量高,而且手动可控,为了提高吞吐量,虚拟机会动态调整堆的参数,缺点是不能保证单次的停顿时间,适用于后台任务,不需要与用户交互,并且容易产生大量的对象比如:大数据的处理,大文件导出
Parallel Old
Paralel Old是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集,采用标记整理算法,只能与Parallel Scavenge配套使用,可以使用虚拟机参数:
-XX:+UseParallelGC
或**-XX:+UseParallelOldGC
可以使用ParalleScavenge+ Parallel Old**这种组合
Parallel Scavenge允许手动设置最大暂停时间和吞吐量,Oracle官方建议在使用这个组合时,不要设置堆内存的最大值,垃圾回收器会根据最大暂停时间和吞吐量自动调整内存大小
可设置虚拟机参数设置期望值:
-XX:MaxGCPauseMillis=n
:设置每次垃圾回收时的最大停顿毫秒数,即最大暂停时间
-XX:GCTimeRatio=n
:设置吞吐量为n(用户线程执行时间 丰 n/n +1)
-XX:+UseAdaptiveSizePolicy
:设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小
G1垃圾回收器
JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器,Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间 ,但是会减少年轻代可用空间的大小,CMS关注暂停时间,但是吞吐量方面会下降,而G1设计目标就是将上述两种垃圾回收器的优点融合
- 支持巨大的堆空间回收,并有较高的吞吐量
- 支持多CPU并行垃圾回收
- 允许用户设置最大暂停时间
JDK9之后强烈推荐使用G1垃圾回收器
在G1出现之前,年轻代和老年代的内存空间一般是连续的,但G1的整个堆会被划分成多个大小相等的区域,称之为区Region,区域不要求是连续的,分为Eden、Survivor、Old区,Region的大小通过堆空间大小/2048计算得到,也可以通过参数:
**-XX:G1HeapRegionSize=32m
**指定(其中32m指定region大小为32M),Regionsize必须是2的指数,取值范围从1M到32M
G1的垃圾回收共有两种方式:1.年轻代的回收(Young GC)、2.混合回收(Mixed GC)
年轻代回收
-
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象,会导致STW,G1中可以通过参数:
-XX:MaxGCPauseMilis=n
:(默认200)设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间
执行流程:
-
新创建的对象会存放在Eden区,当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC
-
标记出Eden和Survivor区域中的存活对象
-
根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),并清空这些区域
其实G1并不会回收所有的Region
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了,比如
-XX:MaxGCPauseMilis=n
(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4个Region -
当再有新的对象添加到堆上,会放入新的Eden区
-
后续Young GC时与之前相同,只不过Survivor区中存活对象会一起被搬运到另一个Survivor区
-
当某个存活对象的年龄到达阈值(默认15),将被放入老年代Old区,未到15的又会被搬运到新另一个Survivor区
-
部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region
多次回收之后,会出现很多Old老年代区,此时总堆占有率达到值时(-XX:InitiatingHeapOccupancyPercent
默认45%)会触发混合回收MixedGC,回收所有年轻代和部分老年代的对象以及大对象区,采用复制算法来完成
混合回收
混合回收分为四个阶段:
- 初始标记(initialmark):标记Gc Roots引用的对象为存活
- 并发标记(concurrent mark):将第一步中标记的对象引用的对象,标记为存活
- 最终标记(remark或者FinalizeMarking):标记一些引用改变漏标的对象不管新创建、不再关联的对象
- 并发清理(cleanup):将存活对象复制到别的Region不会产生内存碎片
G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbagefirst)名称的由来
看到这里你会发现,混合回收阶段很类似与CMS的回收阶段,其实他们是有很大不同:
技术点 | CMS | G1 |
---|---|---|
回收区域 | 仅老年代 | 跨代回收(Young+Old Region) |
碎片处理 | 标记-清除(需定期Full GC整理) | 复制算法(自动整理碎片) |
停顿预测 | 无控制 | 通过-XX:MaxGCPauseMillis 设定 |
内存回收单位 | 整个老年代 | 按Region优先级回收(Garbage-First) |
注意:如果清理过程中发现没有足够的空Region存放转移的对象,会出现Full GC,单线程执行标记-整理算法
此时会导致用户线程的暂停,所以尽量保证应该用的堆内存有一定多余的空间