Java虚拟机:GC
关于JVM垃圾收集器的一切
前言
几乎所有的对象都在堆(Heap)分配,我们只有在程序运行期时才知道要创建哪些对象,这部分内存的分配和回收都是动态的。垃圾收集器所关注的正是这部分内存。学习GC之前务必清楚地知道JVM运行时内存,可以先阅读文章 Java虚拟机——内存区域
可回收对象
垃圾收集器回收的都是可回收对象,可回收对象一般有两种方法来判断:
- 引用计数器法:给每个对象设置一个引用计数器,对象被引用一次,计数器加一;引用失效时,计数器减一。任一时刻,引用计数器为0的对象就是可回收对象。但是引用计数器很难解决循环引用的问题,a,b两个对象如果相互引用,而没有被其他任何对象引用,理论上来说这两个对象均为可回收对象,但是因为他们的引用计数器均不为0,导致无法被回收。
- 可达性分析:基本思路是从一系列名为“GC Roots”的对象出发开始向下搜索,搜索走过的路径称为引用链,当一个对象到“GC Roots”没有路径相连(不可达)时,该对象就是可回收对象。
GC Roots对象包括以下:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象,比如Java类的引用类型静态变量
- 方法区中常量引用的对象,比如字符串常量池里的引用
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointerException、OutOfMemoryError)等,还有系统类加载器
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
垃圾收集算法
标记-清除算法
标记-清除算法(Mark-Sweep)分为“标记”和“清除”两个阶段,首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象。算法优点是简单,缺点有两个:效率低下、容易产生内存碎片。
算法可视化过程大致如下:
可以看到,整个过程需要扫描两次。
复制算法
复制算法(Copying)将可用内存分为大小相等的两块,每次只使用其中的一块。当其中的一块内存用完了,就将还存活的对象复制到另一块,然后再把可回收的对象都回收。这样每次都是对一半的区域进行垃圾清理,分配对象时不用考虑内存碎片的问题,只需要移动堆顶指针,按照顺序分配内存即可。
优点是实现简单,运行高效,不会产生内存碎片。缺点是内存利用率太低,每次只使用一半的内存。一般年轻代会用此算法,因为年轻代中约有98%的对象都是“朝生夕死”的,所以也不需要按照1:1来划分两块区域。HotSpot虚拟机年轻代是分为Eden区和两个Survivor区,大小比例是8 : 1(是Eden区和其中一个Survivor区的比例,两个Survivor一样大)。每次只使用Eden区和其中一个Survivor区,回收时,将Eden区和Survivor区的存活对象复制到另一个Survivor区,然后清理掉Eden区和使用过的Survivor区。
算法可视化过程大致如下:
标记-整理算法
标记整理算法(Mark-Compact)或者叫标记-压缩算法,算法分为“标记”和“整理”两个阶段,“标记”阶段和“标记-清理”算法一样。但是后续步骤不是直接清理标记对象,而是把存活对象向一端移动,然后直接清理端边界以外的内存。
算法可视化过程大致如下:
可以看到标记-整理算法也不会产生内存碎片
现在低版本商用虚拟机采用的都是分代收集算法,根据对象的存活周期把堆(Heap)分为年轻代、老年代、永久代(JDK1.8之后改成Matespace)。每次收集,年轻代会回收绝大部分对象,所以采用复制算法,只需要消耗少量对象的复制成本。老年代中对象存活周期较长,所以采用标记-清除算法或者标记-整理算法。
垃圾收集器
垃圾收算法是理论指导,垃圾收集器则是实际应用。HotSpot虚拟机垃圾收集器大致有以下几款(高版本还有ZGC和Shenandoah):
连线表示两款垃圾收集器可以搭配使用,所处区域表示垃圾收集器工作的区域。G1横跨新生代和老年代是因为G1逻辑上分区,物理上不分区。
Serial收集器
Serial是最基本,发展历史最悠久的收集器。这个收集器是一个单线程的收集器,在进行垃圾收集时,必须暂停所有工程的线程,直到它收集结束,也就是“Stop The World”。优点是简单、高效,单线程不会有线程切换的开销。
-XX:+UseSerialGC
参数指定使用该收集器。工作过程如下(Serial和Serial Old搭配使用):
ParNew收集器
ParNew是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余的行为均与Serial收集器一致。
参数-XX:+UseParNewGC
指定使用该收集器,工作过程如下(ParNew和Serial Old搭配使用):
Parallel Scavenge收集器
Parallel Scavenge是工作于新生代垃圾收集器,使用复制算法。目的是达到一个可控制的吞吐量,是吞吐量优先的垃圾收集器。
参数-XX:+UseParallelGC
指定使用该收集器,吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)
,Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大停顿时间的-XX:MaxGCPauseMillis
,以及直接设置吞吐量的参数-XX:GCTimeRatio
。
除此之外,还提供了参数-XX:+UseAdaptivePolicy
实现GC自适应调节策略,即不需要手动设置年轻代大小、Eden区与Survivor区比例、晋升老年代年龄等参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。其工作过程如下(Parallel Scavenge和Parallel Old搭配使用):
Serial Old收集器
Serial Old是Serial收集器的老年代版本,是一个单线程收集器,使用“标记-整理”算法,参数-XX:+UseSerialOldGC
指定使用该收集器。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,参数-XX:+UseParallelOldGC
指定使用该收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款以获取最短回收时间为目的的垃圾收集器,基于“标记-清除”算法,参数-XX:+UseConcMarkSweepGC
指定使用该收集器,运行过程分为四步:初始标记、并发标记、重新标记、并发清除。下文会详细介绍CMS。
G1收集器
G1(Garbage First)收集器与其他垃圾收集器相比,G1收集器具有并发与并行、分代收集、空间整合、可预测停顿等特点,参数-XX:+UseG1GC
指定使用该收集器。下文会详细介绍G1。
CMS收集器
CMS(Concurrent Mark Sweep)垃圾收集器是一款以获取最短停顿时间为目的的垃圾收集器,从名字上可以看出基于“标记-清除”算法实现。整个过程分为四个步骤:
- 初始标记(CMS initial Mark):该过程仅仅标记“Gc Roots”直接关联的对象,速度很快
- 并发标记(CMS concurrent Mark):该过程就是从Gc Roots开始向下搜索的过程
- 重新标记(CMS remark):该过程是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动那一部分对象的标记记录,消耗时间比初始标记长,但是远小于并发标记
- 并发清除(CMS concurrent sweep)
CMS收集器的主要优点是并发收集和低停顿;主要缺点有三个:
- 对CPU资源敏感。CMS收集器默认启动的回收线程数是
(CPU数量 + 3)/ 4
,当CPU数量大于4时,并发回收时垃圾线程不少于25%的CPU资源,但是当CPU数量小于4个时,CMS对用户程序的影响就会变得很大。 - CMS收集器无法处理浮动垃圾,可能会出现Concurrent Mode Failure失败,而导致另一次Full GC的产生。在并发清除阶段,由于工作线程还在运行,所以必定会产生新的垃圾。但是新的垃圾是出现在标记之后,所以只有下一次gc的时候才能清除,这部分垃圾就是浮动垃圾。
正是由于浮动垃圾的存在,所以CMS不能等到老年代几乎快满了才进行垃圾回收,而是预留一些空间。CMS收集器在老年代使用了大约68%(参数-XX:CMSInitiatingOccupancyFraction
指定)后就会启动。要是CMS运行期间预留的空间无法满足程序需要,就会出现Concurrent Mode Failure失败,这时虚拟机将启用后备方案,临时使用Serial Old收集器来重新进行老年代垃圾回收,这样停顿时间就很长了。 - 容易产生内存碎片:因为CMS基于“标记-清除”算法。内存碎片过多的话,容易给大对象的分配带来很大的麻烦,往往老年代还有大量的空间,但是无法找到连续的空间来分配大对象,所以不得不提前触发一次Full GC。
为了解决这个问题,虚拟机提供了一个参数-XX:+UseCMSCompactAtFullCollection
,用于CMS由于内存碎片要进行Full GC时,开启内存碎片的合并整理过程。内存碎片的问题解决了,但是停顿时间变长了。虚拟机还提供了另一个参数-XX:CMSFullGCsBeforeCompaction
,这个参数表示执行多少次不压缩的Full GC后,接着执行一次压缩的Full GC(默认值为0,表示每次Full GC时都压缩)。
G1收集器
特点
G1收集器,全称Garbage-First Garbage Collector。在JDK1.7中正式推出,在JDK1.9已经被设置成默认收集器。G1应用在多处理器和大容量内存环境中,实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:
- 像CMS收集器一样, 能与应用程序线程并发执行。
- 整理空闲空间更快。
- 需要更多的时间来预测GC停顿时间。
- 不希望牺牲大量的吞吐性能。
- 不需要更大的Java Heap。
优点
G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
- G1的STW更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间(
-XX:MaxGCPauseMillis
)。
内存结构
G1收集器并不是像上面几种收集器一样将内存空间划分为新生代、老年代和永久代(或者Metaspace),而是将堆划分为若干个区域Region(它在逻辑上是分代的,但是物理上不分代)。
(图片来源:https://tech.meituan.com/2016/09/23/g1.html)
这些Region被分成了Eden区(E)、Survivor区(S)、老年代(O)和Humongous区(H)。前三个概念和之前的分代含义一样。而Humongous表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。
H-obj有如下几个特征:
- H-obj直接分配到了老年代,防止了反复拷贝移动。
- H-obj在global concurrent marking阶段的cleanup和full GC阶段回收。
- 在分配H-obj之前先检查是否超过
-XX:InitiatingHeapOccupancyPercent
和-XX:G1ReservePercent
,如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。
默认情况下,Region大小在1M-32M之间,是2的指数。参数-XX:G1HeapRegionSize
可以修改
为了减少连续H-objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size。
GC
这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。
从每个Region的角度来看,G1使用的是复制算法,但是从整个堆来看,G1使用的是整理(压缩)算法。
G1提供了两种GC模式:Young GC和Mixed GC。这里并不是说G1不会产生Full GC,G1也会产生Full GC。
Young GC和Mixed GC都是STW模式。
- Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
- Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。Mixed GC是既回收年轻代,又回收部分老年代。
Mixed GC不是full GC,它只能回收部分老年代的Region,如果Mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。
global concurrent marking的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:
- 初始标记(initial mark,STW)。它标记了从GC Root开始直接可达的对象。
- 并发标记(Concurrent Marking)。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
- 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
- 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list。
第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。
Mixed GC发生的时机,由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。 CSet是指Collection Set,一组可以被回收的分区集合,在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可能来自Eden区、Survivor区或者老年代。CSet占用不会超过整个堆的1%大小。
G1HeapWastePercent
:在global concurrent marking结束之后,可以知道老年代regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。G1MixedGCLiveThresholdPercent
:老年代region中的存活对象的占比,只有在此参数之下,才会被选入CSet。G1MixedGCCountTarget
:一次global concurrent marking之后,最多执行Mixed GC的次数。G1OldCSetRegionThresholdPercent
:一次Mixed GC中能被选入CSet的最多老年代region数量。
完整的GC过程
新创建的对象在Eden区,当Eden区空间不足时,触发Minor GC(年轻代GC),会把Eden区和已使用的Survivor区中的存活对象复制到另一个Survivor区,然后清空Eden区和之前使用的Survivor区。
- 当对象熬过15次Minor GC(
-XX:MaxTenuringThreshold
参数指定该值,不同的垃圾收集器默认值不一样)后依然存活,会转移到老年代 - 如果Survivor区放不下该对象,会直接放入老年代
- 新生成的大对象(
-XX:+PretenureSizeThreshold
指定阈值,超过该值就是大对象,该参数只对Serial和ParNew两款收集器有效)直接进入老年代 - 如果在Survivor区相同年龄所有对象大小总和大于Survivor区的一半,则大于等于该年龄的对象进入老年代而无需等待
-XX:MaxTenuringThreshold
指定的参数。
Full GC是指老年代GC,但是老年代GC往往伴随着年轻代GC的发生,Full GC触发条件:
- 老年代空间不足
- 永久代空间不足(JDK7及以下)
- CMS收集器出现promotion failed或者concurrent mode failure。前者是在Minor GC过程中,Survivor空间不足,对象进入老年代,而老年代空间也不足时会触发Full GC;后者是CMS收集器收集时,老年代预留空间不足(并发清除阶段,产生浮动垃圾),会触发Full GC。
- Minor GC进入到老年代的平均大小大于老年代剩余空间
- 程序调用
System.gc()
,提醒收集器进行Full GC,不过仅仅是提醒,具体的执行还是由JVM决定。
常用参数
通用部分
- -XX:+PrintGC:打印GC日志
- -XX:+PrintGCDetails:打印GC详细日志
- -Xss:每个线程的虚拟机栈大小,一般256K
- -Xms:堆的初始值
- -Xmx:堆的最大值,一般设置和Xms一致,因为heap扩容时,会发生内存抖动,影响程序稳定性
- -XX:NewSize:年轻代初始空间大小。
- -XX:MaxNewSize:年轻代最大空间大小。
- -Xmn:年轻代大小
- -XX:SurvivorRatio:年轻代中Eden和Survivor的比值,默认8:1(Eden占8/10,两个Survivor各占1/10)
- -XX:NewRatio:老年代和年轻代内存大小比例,默认2:1
- -XX:MaxTenuringThreshold:年轻代对象进入老年代,熬过Minor GC次数的阈值
- -XX:+DisableExplictGC :禁用
System.gc()
- -XX:+PrintGCTimeStamps:JVM启动过后到GC所经历的时间
- -XX:+PrintGCDateStamps:GC发生的时间,
yyyy-MM-dd'T'HH:mm:ss.SSSZ +0800
- -XX:+PrintFlagsFinal:打印JVM参数,例如
java -XX:+PrintFlagsFinal -version |grep G1
Paralegal
- -XX:+ParallelGCThreads :并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
- -XX:+UseAdaptiveSizePolicy :GC自适应调节策略,自动选择各区大小比例
- GCTimeRatio:设置GC时间占用程序运行时间的百分比
- -XX:MaxGCPauseMillis:停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
CMS
- -XX:+UseConcMarkSweepGC:老年代使用CMS收集器
- -XX:ParallelCMSThreads:CMS线程数量
- -XX:CMSInitiatingOccupancyFraction:使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小
- -XX:+UseCMSCompactAtFullCollection:在Full GC时进行压缩,清理内存碎片
- -XX:CMSFullGCsBeforeCompaction :多少次Full GC之后,进行压缩
G1
- -XX:+UseG1GC:使用G1收集器
- -XX:MaxGCPauseMillis:建议值,G1会尝试调整Young区的块数来达到这个值
- -XX:GCPauseIntervalMillis:GC的间隔时间
- -XX:+G1HeapRegionSize:Region分区大小。 随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
- -XX:G1NewSizePercent:新生代最小比例,默认为5%,不需要修改,由G1自动调整
- -XX:G1MaxNewSizePercent:新生代最大比例,默认为60%,不需要修改,由G1自动调整
- -XX:GCTimeRatio:GC时间建议比例,G1会根据这个值调整堆空间
- -XX:ConcGCThreads:并发标记的线程数
- -XX:InitiatingHeapOccupancyPercent:触发标记周期的堆占用率阈值。默认占用率是整个堆的45%
参考
- 《深入理解Java虚拟机》
- https://tech.meituan.com/2016/09/23/g1.html
更多推荐
所有评论(0)