JVM垃圾收集器(Garbage Collector)
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。在Java虚拟机中,垃圾回收器可不只有一种,什么情况下要使用哪一种,对性能又有什么样的影响,这都是我们必须要了解的。
串行回收器
串行回收器是指使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往能让其有更好的性能表现。串行回收器可以在新生代和老年代使用,根据不同的堆空间分为新生代串行回收器和老年代串行回收器。
新生代串行回收器(Serial收集器)
串行回收器是所有垃圾回收器中最古老的一种,也是JDK中最基本的垃圾回收器之一。曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。串行回收器主要有两个特点:
第一,它仅仅使用单线程进行垃圾回收。
第二,它是独占式的垃圾回收方式。
在串行回收器进行垃圾回收时,Java应用程序中的线程都需要暂停工作,等待垃圾回收完成。在串行回收器运行时,应用程序中的所有线程都停止工作,进行等待,这种现象称为“Stop-The-World”。它将造成非常糟糕的用户体验,在实时性要求较高的应用场景中,这往往是不能被接受的。
虽然如此,串行回收器却是一种成熟且经过长时间生产环境考验的极为高效的收集器。新生代串行回收器使用复制算法,实现相对简单、逻辑处理特别高效且没有线程切换的开销。在诸如单CPU处理器等硬件平台不是特别优越的情况下,它的性能表现可以超过并行回收器和并发回收器。
使用-XX:+UseSerialGC参数可以指定使用新生代串行回收器或老年代串行回收器。当虚拟机在Client模式下运行时,它是默认的垃圾回收器。
注意: 串行垃圾回收器虽然古老,但是久经考验,在大多数情况下,其性能表现是相当不错的。
老年代串行回收器(Serial Old)
老年代串行回收器使用的是标记-整理法。和新生代串行回收器一样,它也是一个串行的、独占式的垃圾回收器。由于老年代垃圾回收通常会需要比新生代垃圾回收更长的时间,在堆空间较大的应用程序中,一旦老年代串行回收器启动,应用程序很可能会因此停顿较长的时间。
虽然如此,作为老牌的垃圾回收器,老年代串行回收器可以和多种新生代回收器配合使用,同时它也可以作为CMS回收器的备用回收器。
若要启用老年代串行回收器,可以尝试使用以下参数。
·-XX:+UseSerialGC:新生代、老年代都使用串行回收器。
·-XX:+UseParNewGC(JDK 9、JDK 10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,不再支持此参数):新生代使用ParNew回收器,老年代使用串行回收器。
·-XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。
并行回收器
并行回收器在串行回收器的基础上做了改进,它使用多个线程同时进行垃圾回收。对于并行能力强的计算机,可以有效减少垃圾回收所需的实际时间。
新生代ParNew回收器 (ParNew)
ParNew回收器是一个工作在新生代的垃圾回收器。它只是简单地将串行回收器多线程化,它的回收策略、算法及参数和新生代串行回收器一样。ParNew回收器也是独占式的回收器,在回收过程中应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器,而在单CPU或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,由于多线程的压力,它的实际表现很可能比串行回收器差。
开启ParNew回收器可以使用以下参数。
·-XX:+UseParNewGC(JDK 9、JDK 10已经删除,因为ParNew需要和CMS搭配工作,而CMS已经被G1替代,不再支持此参数):新生代使用ParNew回收器,老年代使用串行回收器。
·-XX:+UseConcMarkSweepGC(JDK 9、JDK 10不建议使用,建议使用默认的G1垃圾回收器):新生代使用ParNew回收器,老年代使用CMS。
ParNew回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。一般,最好与CPU数量相当,避免过多的线程数影响垃圾回收性能。在默认情况下,当CPU数量小于8时,ParallelGCThreads的值等于CPU数量,当CPU数量大于8时,ParallelGCThreads的值等于3+((5×CPU_Count)/8)。
新生代ParallelGC回收器 (Parallel)
新生代ParallelGC回收器也是使用复制算法的回收器。从表面上看,它和ParNew回收器一样,都是多线程、独占式的回收器。但是,ParallelGC回收器有一个重要的特点:它非常关注系统的吞吐量。
新生代ParallelGC回收器可以使用以下参数启用。
·-XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代使用串行回收器。
·-XX:+UseParallelOldGC: 新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC回收器。
ParallelGC回收器提供了两个重要的参数用于控制系统的吞吐量。
·-XX:MaxGCPauseMillis: 设置最大垃圾回收停顿时间。它的值是一个大于0的整数。ParallelGC 在工作时,会调整 Java 堆大小或者其他参数,尽可能地把停顿时间控制在MaxGCPauseMillis 以内。如果读者希望减少停顿时间而把这个值设得很小,为了达到预期的停顿时间,虚拟机可能会使用一个较小的堆(一个小堆比一个大堆回收快),而这将导致垃圾回收变得很频繁,从而增加垃圾回收总时间,降低吞吐量。
·-XX:GCTimeRatio: 设置吞吐量大小。它的值是一个 0 到 100 之间的整数。假设GCTimeRatio的值为n ,那么系统将花费不超过1/(1+n )的时间进行垃圾回收。比如GCTimeRatio等于19(默认值),则系统用于垃圾回收的时间不超过1/(1+19)=5%。默认情况下,它的取值是99,即有不超过1/(1+99)=1%的时间用于垃圾回收。
除此之外,ParallelGC回收器与ParNew回收器另一个不同之处在于,它还支持一种自适应的GC调节策略。使用-XX:+UseAdaptiveSizePolicy可以打开自适应GC策略。在这种模式下,新生代的大小、eden区和survivor区的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
注意: ParallelGC回收器关注系统吞吐量。可以通过-XX:MaxGCPauseMillis和-XX:GCTimeRatio设置期望的停顿时间和吞吐量。但是鱼和熊掌不可兼得,这两个参数是相互矛盾的,通常如果减少一次收集的最大停顿时间,就会同时减小系统吞吐量,增加系统吞吐量又可能会同时增加一次垃圾回收的最大停顿时间。
老年代ParallelOldGC回收器(Parallel Old)
老年代ParallelOldGC回收器也是一种多线程并发的回收器。和新生代ParallelGC回收器一样,它也是一种关注吞吐量的回收器。从名字上看,它在ParallelGC中间插入了Old,表示这是一个应用于老年代的回收器,并且和ParallelGC新生代回收器搭配使用。
ParallelOldGC回收器使用标记压缩法,它在JDK1.6中才可以使用。
使用-XX:+UseParallelOldGC可以在新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。这是一对非常关注吞吐量的垃圾回收器。在对吞吐量敏感的系统中,可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。
CMS回收器
与ParallelGC和ParallelOldGC不同,CMS回收器主要关注系统停顿时间。CMS是Concurrent Mark Sweep的缩写,意为并发标记清除,从名称上就可以得知,它使用的是标记清除法,同时它又是一个使用多线程并行回收的垃圾回收器。
CMS主要工作步骤
CMS回收器的工作过程与其他垃圾回收器相比,略显复杂。CMS工作时的主要步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而预清理、并发标记、并发清除和并发重置是可以和用户线程一起执行的。从整体上说,CSM不是独占式的,它可以在应用程序运行过程中进行垃圾回收。
根据标记清除法,初始标记、并发标记和重新标记都是为了标记出需要回收的对象。并发清理则是在标记完成后,正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理和并发重置都是可以和应用程序线程一起执行的。
在整个CMS的回收过程中,默认情况下,在并发标记之后,会有一个预清理的操作(也可以关闭开关-XX:-CMSPrecleaningEnabled,不进行预清理)。预清理是并发的,除了为正式清理做准备和检查,还会尝试控制一次停顿的时间。由于重新标记是独占CPU的,如果新生代GC发生后,立即触发一次重新标记,那么一次停顿的时间可能很长。为了避免这种情况,预处理时会刻意等待一次新生代GC的发生,然后根据历史性能数据预测下一次新生代GC可能发生的时间,在当前时间和预测时间的中间时刻进行重新标记。这样可尽量避免新生代GC和重新标记重合,尽可能减少一次停顿的时间。
CMS主要的参数
启用CMS回收器的参数是-XX:+UseConcMarkSweepGC。CMS是多线程回收器,设置合理的工作线程数量对系统性能有重要的影响。
CMS默认启动的并发线程数是(ParallelGCThreads+3)/4。ParallelGCThreads表示GC并行时使用的线程数量,如果新生代使用ParNew,那么ParallelGCThreads也就是新生代GC的线程数量。这意味着有4个ParallelGCThreads时,只有1个并发线程,而有两个并发线程时,有5~8个ParallelGCThreads线程。
并发线程数量也可以通过-XX:ConcGCThreads或者-XX:ParallelCMSThreads参数手工设定。当CPU资源比较紧张时,受到CMS回收器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。
注意: 并发是指收集器和应用线程交替执行,并行是指应用程序停止,同时由多个线程一起执行GC。因此并行回收器不是并发的,因为并行回收器执行时,应用程序完全挂起,不存在交替执行。
由于CMS回收器不是独占式的回收器,在CMS回收过程中,应用程序仍然在不停地工作。在应用程序工作过程中,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。同时,因为应用程序没有中断,所以在CMS回收过程中,还应该确保应用程序有足够的内存可用。因此,CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,以确保应用程序在CMS工作过程中,依然有足够的空间支持应用程序运行。
这个回收阈值可以使用参数-XX:CMSInitiatingOccupancyFraction来指定,默认是68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。如果应用程序的内存使用率增长很快,在CMS的执行过程中,已经出现了内存不足的情况,CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收。此时,应用程序将完全中断,直到垃圾回收完成,这时,应用程序的停顿时间可能会较长。
注意: 通过-XX:CMSInitiatingOccupancyFraction可以指定当老年代空间使用率达到多少时进行一次CMS垃圾回收。
根据应用程序的特点,可以对参数-XX:CMSInitiatingOccupancyFraction进行调优。如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数,可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行回收器。
CMS是一个基于标记清除法的回收器。在本章之前的篇幅中已经提到,标记清除法将会产生大量内存碎片,离散的可用空间无法分配给较大的对象。
在这种情况下,即使堆内存仍然有较大的剩余空间,也可能会被迫进行一次垃圾回收,以换取一块可用的连续内存。这种现象对系统性能是相当不利的,为了解决这个问题,CMS回收器还提供了几个用于内存压缩整理的参数。
-XX:+UseCMSCompactAtFullCollection参数可以使CMS在垃圾收集完成后,进行一次内存碎片整理,内存碎片的整理不是并发进行的。-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后,进行一次内存压缩。
注意: CMS回收器是一个关注停顿的垃圾回收器。同时CMS回收器在部分工作流程中,可以与用户程序同时运行,从而减少应用程序的停顿时间。
有关Class的回收
在使用CMS回收器时,如果需要回收Perm区,那么默认情况下,还需要触发一次Full GC:
如果希望使用CMS回收器回收Perm区,则必须打开-XX:+CMSClassUnloadingEnabled开关。使用-XX:+CMSClassUnloadingEnabled后,如果条件允许,系统会使用CMS的机制回收Perm区的Class数据
G1回收器
G1回收器(Garbage-First)是在JDK 1.7中正式使用的全新的垃圾回收器,从长期目标来看,它是为了取代CMS回收器。G1回收器拥有独特的垃圾回收策略,和之前提到的回收器截然不同。从分代上看,G1依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。它使用了分区算法。作为CMS的长期替代方案,G1使用了全新的分区算法,其特点如下。
· 并行性: G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
· 并发性: G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,一般来说,不会在整个回收期间完全阻塞应用程序。
· 分代 GC: G1 依然是一个分代回收器,但是和之前的回收器不同,它同时兼顾年轻代和老年代,其他回收器或者工作在年轻代,或者工作在老年代。
· 空间整理: G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单地标记清理对象,在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少碎片空间。
· 可预见性: 由于分区的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,全局停顿也能得到较好的控制。
G1的新生代GC
新生代GC的主要工作是回收eden区和survivor区。一旦eden区被占满,新生代GC就会启动。新生代GC前、后的堆数据示例如图5.6所示,其中E表示eden区,S表示survivor区,O表示老年代。可以看到,新生代GC只处理eden区和survivor区,回收后所有的eden区都应该被清空,而survivor区会被回收一部分数据,但是应该至少仍然存在一个survivor区,类比其他的新生代回收器,这一点似乎并没有太大变化。另一个重要的变化是,老年代的区域增多,因为部分survivor区或者eden区的对象可能会晋升到老年代。
新生代GC发生后,如果打开了PrintGCDetails选项,就可以得到类似如下的GC日志(这里只给出了部分日志,完全的日志及其分析请看5.4.6节):
和其他回收器的日志相比,G1的日志内容非常丰富。当然我们最关心的依然是GC的停顿时间及回收情况。从日志中可以看到,eden区原本占用235MB空间,回收后被清空,survivor区从5MB增长到了11MB,这是因为部分对象被从eden区复制到survivor区,整个堆合计为400MB,堆内存从回收前的239MB下降到10.5MB。
G1的并发标记周期
G1的并发阶段和CMS有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发的部分单独提取出来执行。
并发标记周期可以分为以下几步。
· 初始标记: 标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序线程在这个阶段必须停止执行。
· 根区域扫描: 由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden区被清空,并且存活对象被移入survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代 GC 同时执行(因为根区域扫描依赖 survivor 区的对象,而新生代GC会修改这个区域),因此如果恰巧在此时需要进行新生代GC,就需要等待根区域扫描结束后才能进行。如果发生这种情况,这次新生代GC的时间就会延长。
· 并发标记: 和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发的过程,并且这个过程可以被一次新生代GC打断。
· 重新标记: 和CMS一样,重新标记也是会产生应用程序停顿的。由于在并发标记过程中,应用程序依然在运行,因此标记结果可能需要进行修正,所以在此对上一次的标记结果进行补充。在G1中,这个过程使用SATB(Snapshot-At-The-Beginning)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
· 独占清理: 这个阶段是会引起停顿的。它将计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段需要这些信息。
· 并发清理: 这里会识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。
混合回收
在并发标记周期中,虽然有部分对象被回收,但是总体上说,回收的比例是相当低的。但是在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象,在混合回收阶段就可以专门针对这些区域进行回收。当然,G1会优先回收垃圾比例较高的区域,因为回收这些区域的性价比也比较高。这也正是G1名字的由来。G1垃圾回收器的全称为Garbage First Garbage Collector,直译为垃圾优先的垃圾回收器,这里的垃圾优先(Garbage First)指的就是回收时优先选取垃圾比例最高的区域。
这个阶段叫作混合回收,是因为在这个阶段既会执行正常的年轻代GC,又会选取一些被标记的老年代区域进行回收,它同时处理了新生代和老年代,如图5.9所示。因为新生代GC的原因,eden区必然被清空,此外,有两块被标记为G的垃圾比例最高的区域被清理。被清理区域中的存活对象会被移到其他区域,这样做的好处是可以减少空间碎片。
必要时的Full GC
和CMS类似,并发回收由于让应用程序和GC线程交替工作,总是不能完全避免在特别繁忙的场合出现在回收过程中内存不充足的情况。当遇到这种情况时,G1也会转入一个Full GC。
G1的日志
G1相关的参数
对于G1,可以使用-XX:+UseG1GC标记打开G1的开关,对G1进行设置时,最重要的一个参数就是-XX:MaxGCPauseMillis,它用于指定目标最大停顿时间。如果任何一次停顿超过这个设置值,G1就会尝试调整新生代和老年代的比例、调整堆大小、调整晋升年龄等,试图达到预设目标。对于性能调优来说,有时候总是鱼和熊掌不可兼得,如果停顿时间缩短,对于新生代来说,这意味着很可能要增加新生代GC的次数。对于老年代来说,为了获得更短的停顿时间,在混合GC时,一次收集的区域数量也会变少,这样无疑增加了进行Full GC的可能性。
另外一个重要的参数是-XX:ParallelGCThreads,它用于设置并行回收时GC的工作线程数量。
此外,-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行。默认值是45,即当整个堆的占用率达到45%时,执行并发标记周期。InitiatingHeapOccupancyPercent一旦设置,始终都不会被G1修改,这意味着G1不会试图改变这个值来满足MaxGCPauseMillis的目标。如果InitiatingHeapOccupancyPercent值设置得偏大,会导致并发周期迟迟得不到启动,那么引起Full GC的可能性也大大增加,反之,一个过小的InitiatingHeapOccupancyPercent值会使得并发标记周期执行非常频繁,大量GC线程抢占CPU,导致应用程序的性能有所下降
ZGC
Z Garbage Collector,简称 ZGC,是 JDK 11 中新加入的尚在实验阶段的低延迟垃圾收集器。它和 Shenandoah 同属于超低延迟的垃圾收集器,但在吞吐量上比 Shenandoah 有更优秀的表现,甚至超过了 G1,接近了“吞吐量优先”的 Parallel 收集器组合,可以说近乎实现了“鱼与熊掌兼得”。
与CMS中的ParNew和G1类似,ZGC也采用标记-复制算法,不过ZGC对该算法做了重大改进:ZGC在标记、转移和重定位阶段几乎都是并发的,这是ZGC实现停顿时间小于10ms目标的最关键原因。
ZGC阶段
ZGC只有三个STW阶段:初始标记,再标记,初始转移。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
ZGC 的内存布局
ZGC 也采用基于 Region 的堆内存布局,但与它们不同的是, ZGC 的 Region 具有动态性,也就是可以动态创建和销毁,容量大小也是动态的,有大、中、小三类容量:
- 小型 Region (Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象
- 中型 Region (M edium Region):容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对象
- 大型 Region (Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB 在 JDK 11 及以上版本,可以通过以下参数开启 ZGC:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC 。
对象内存分配和回收
对象何时进入老年代
对于一般情况而言,当对象首次创建时,会被放置在新生代的eden区。为什么取名叫eden呢?eden是“伊甸园”的意思。根据圣经的记载,亚当和夏娃就住在伊甸园,那也是人类开始居住的地方。这里沿用伊甸园的名字也就是这个意思。在堆中分配的对象首先会被安置在eden区。如果没有GC的介入,那么这些对象不会离开eden区。
初创的对象在eden区
老年对象进入老年代
eden区中的对象何时能进入老年代呢?一般来说,当对象的年龄达到一定的大小,就自然可以离开年轻代,进入老年代。一般把对象进入老年代的事件,称为“晋升”。对象的年龄是由对象经历过的GC次数决定的。在新生代中的对象每经历一次GC,如果它没有被回收,它的年龄就加1。虚拟机提供了一个参数来控制新生代对象的最大年龄:MaxTenuringThreshold。在默认情况下,这个参数的值为15。也就是说,新生代的对象最多经历15次GC,就可以晋升到老年代。
大对象进入老年代
除了年龄,对象的体积也会影响对象的晋升。试想,如果对象体积很大,新生代无论eden区还是survivor区都无法容纳这个对象,自然这个对象无法存放在新生代,也非常有可能被直接晋升到老年代。如图5.11所示,如果需要一个连续的6MB空间,而新生代survivor区(只有5MB)无法容纳这样的大小,此时无论该对象年龄如何,它都会被直接晋升到老年代。
在TLAB上分配对象
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存。从名字上可以看到,TLAB是一个线程专用的内存分配区域。
为什么需要TLAB这个区域呢?这是为了加速对象分配。由于对象一般会分配在堆上,而堆是全局共享的。在同一时间,可能会有多个线程在堆上申请空间。因此,每一次对象分配都必须进行同步,而在竞争激烈的场合分配的效率又会进一步下降。考虑到对象分配几乎是Java最常用的操作,因此Java虚拟机就使用了TLAB这种线程专属的区域来避免多线程冲突,提高对象分配的效率。TLAB本身占用了eden区的空间。在TLAB启用的情况下,虚拟机会为每一个Java线程分配一块TLAB区域。
finalize()函数对垃圾回收的影响
常用的GC参数
1.与串行回收器相关的参数
·-XX:+UseSerialGC:在新生代和老年代使用串行回收器。
·-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例。
·-XX:PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接被分配在老年代。
·-XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代。
2.与并行GC相关的参数
·-XX:+UseParNewGC(考虑到兼容性问题,JDK 9、JDK 10已经删除):在新生代使用并行回收器。
·-XX:+UseParallelOldGC:老年代使用并行回收器。
·-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等,但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。
·-XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。它的值是一个大于0的整数。回收器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。
·-XX:GCTimeRatio:设置吞吐量大小。它的值是一个 0 到 100 之间的整数。假设GCTimeRatio的值为n ,那么系统将花费不超过1/(1+n )的时间用于垃圾回收。
·-XX:+UseAdaptiveSizePolicy:打开自适应GC策略。在这种模式下,新生代的大小、eden区和survivior区的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡。
3.与CMS回收器相关的参数(JDK9、JDK10已经开始废弃CMS回收器,建议使用G1回收器)
·-XX:+UseConcMarkSweepGC:新生代使用并行回收器,老年代使用CMS+串行回收器。
·-XX:ParallelCMSThreads:设定CMS的线程数量。
·-XX:CMSInitiatingOccupancyFraction:设置 CMS 回收器在老年代空间被使用多少后触发,默认为68%。
·-XX:+UseCMSCompactAtFullCollection:设置 CMS 回收器在完成垃圾回收后是否要进行一次内存碎片的整理。
·-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩。
·-XX:+CMSClassUnloadingEnabled:允许对类元数据区进行回收。
·-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收(前提是激活了-XX:+CMSClassUnloadingEnabled)。
·-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阈值的时候才进行CMS回收。
·-XX:+CMSIncrementalMode:使用增量模式,比较适合单CPU。增量模式在JDK 8中标记为废弃,并且将在JDK 9中彻底移除。
4.与G1回收器相关的参数
·-XX:+UseG1GC:使用G1回收器。
·-XX:MaxGCPauseMillis:设置最大垃圾回收停顿时间。
·-XX:GCPauseIntervalMillis:设置停顿间隔时间。
5.TLAB相关
·-XX:+UseTLAB:开启TLAB分配。
·-XX:+PrintTLAB(考虑到兼容性问题,JDK 9、JDK 10不再支持此参数):打印TLAB相关分配信息。
·-XX:TLABSize:设置TLAB区域大小。
·-XX:+ResizeTLAB:自动调整TLAB区域大小。
6.其他参数
·-XX:+DisableExplicitGC:禁用显式GC。
·-XX:+ExplicitGCInvokesConcurrent:使用并发方式处理显式GC。
GC日志分析
GC实战
新生代串行回收器GC日志
代码如下
import java.util.ArrayList;
import java.util.List;
public class GcTest {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
list.add(new byte[1024 * 1024]);
}
}
}
新生代串行回收器的输出日志如下(使用 -XX:+UseSerialGC -XX:+PrintGCDetails -Xms20m -Xmx20m)
[GC (Allocation Failure) [DefNew: 5248K->633K(6144K), 0.0029876 secs] 5248K->3706K(19840K), 0.0030643 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
DefNew 表示 Default New 默认新生代收集器就是serial收集器 它显示了一次垃圾回收前的新生代内存占用量和垃圾回收后的新生代内存占用量,以及垃圾回收所消耗的时间。
老年代串行回收器GC日志
老年代串行回收器的输出日志类如下:(使用 -XX:+UseSerialGC -XX:+PrintGCDetails -Xms20m -Xmx20m)
[Full GC (Allocation Failure) [Tenured: 12919K->12919K(13696K), 0.0017886 secs] 18252K->18039K(19840K), [Metaspace: 3016K->3016K(1056768K)], 0.0018184 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
它显示了垃圾回收前老年代和永久区的内存占用量,以及垃圾回收后老年代和永久区的内存占用量。
ParNew回收器GC日志
ParNew回收器的输出日志如下:(使用 -XX:+UseParNewGC -XX:+PrintGCDetails -Xms20m -Xmx20m):
[GC (Allocation Failure) [ParNew: 5248K->640K(6144K), 0.0022186 secs] 5248K->3769K(19840K), 0.0022746 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
可以看到,这个输出和新生代串行回收器的输出几乎是一样的,只有回收器标识符不同。
ParallelGC回收器GC日志
ParallelGC回收器的输出日志如下:(使用 -XX:+UseParallelGC -XX:+PrintGCDetails -Xms20m -Xmx20m):
[GC (Allocation Failure) [PSYoungGen: 5252K->504K(6144K)] 5252K->3860K(19968K), 0.0112150 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
它显示了ParallelGC回收器的工作成果,也就是回收前的内存大小和回收后的内存大小,以及花费的时间。
ParallelOldGC回收器
ParallelOldGC回收器的输出日志如下:(-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:+PrintGCDetails -Xms20m -Xmx20m)
[Full GC (Ergonomics) [PSYoungGen: 488K->0K(6144K)] [ParOldGen: 8524K->8825K(13824K)] 9012K->8825K(19968K), [Metaspace: 3016K->3016K(1056768K)], 0.0059792 secs] [Times: user=0.17 sys=0.01, real=0.01 secs]
它显示了新生代、老年代及永久区在回收前、后的情况,以及Full GC所消耗的时间。
CMS回收器
CMS回收器工作时的日志输出如下:(-XX:+UseConcMarkSweepGC -XX:+UseG1GC -XX:+PrintGCDetails -Xms20m -Xmx20m -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xms20m -Xmx20m)
[GC (Allocation Failure) [ParNew: 5248K->640K(6144K), 0.0023546 secs] 5248K->3806K(19840K), 0.0028198 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 5866K->0K(6144K), 0.0046164 secs] 9032K->8861K(19840K), 0.0046486 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8861K(13696K)] 9885K(19840K), 0.0003395 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[GC (Allocation Failure) [ParNew: 5219K->5219K(6144K), 0.0000184 secs][CMS[CMS-concurrent-mark: 0.001/0.006 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
(concurrent mode failure): 8861K->12924K(13696K), 0.0114673 secs] 14081K->13948K(19840K), [Metaspace: 3016K->3016K(1056768K)], 0.0115349 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [CMS: 12924K->12922K(13696K), 0.0038534 secs] 18146K->18043K(19840K), [Metaspace: 3016K->3016K(1056768K)], 0.0038922 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [CMS: 12922K->12907K(13696K), 0.0059485 secs] 18043K->18027K(19840K), [Metaspace: 3016K->3016K(1056768K)], 0.0059849 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 12907K(13696K)] 18027K(19840K), 0.0003805 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Final Remark) [YG occupancy: 5327 K (6144 K)][Rescan (parallel) , 0.0005398 secs][weak refs processing, 0.0000460 secs][class unloading, 0.0003701 secs][scrub symbol table, 0.0007446 secs][scrub string table, 0.0001868 secs][1 CMS-remark: 12907K(13696K)] 18234K(19840K), 0.0019847 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 6144K, used 5437K [0x00000000fec00000, 0x00000000ff2a0000, 0x00000000ff2a0000)
eden space 5504K, 98% used [0x00000000fec00000, 0x00000000ff14f670, 0x00000000ff160000)
from space 640K, 0% used [0x00000000ff160000, 0x00000000ff160000, 0x00000000ff200000)
to space 640K, 0% used [0x00000000ff200000, 0x00000000ff200000, 0x00000000ff2a0000)
concurrent mark-sweep generation total 13696K, used 12906K [0x00000000ff2a0000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3034K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 323K, capacity 386K, committed 512K, reserved 1048576K
以上信息是一次CMS回收的输出。可以看到,在CMS回收器工作过程中,包括初始化标记、并发标记、预清理、重新标记、并发清理和重发重置等几个重要阶段。在日志中,还可以看到CMS的耗时及堆内存信息。 发生abortable-preclean,表示CMS开始等待一次新生代GC。ParNew回收器开始工作,abortable-preclean终止。之后,CMS根据之前新生代GC的情况,将重新标记的时间放在一个最不可能和下一次新生代GC重叠的时刻,即两次新生代GC的中间点。
除此之外,CMS回收器在运行时还可能输出如下日志:
这说明CMS回收器并发回收失败。这很可能是应用程序在运行过程中老年代空间不够导致的。如果在CMS工作过程中,出现非常频繁的并发模式失败,就应该考虑进行调整,尽可能预留一个较大的老年代空间。或者可以设置一个较小的-XX:CMSInitiatingOccupancyFraction参数,降低CMS触发的阈值,使CMS在执行过程中仍然有较大的老年代空闲空间供应用程序使用。
G1回收器日志
G1的日志与先前的回收器相比已经丰富了很多。-XX:+UseG1GC -XX:+UseG1GC -XX:+PrintGCDetails -Xms20m -Xmx20m -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:ParallelGCThreads=4 -Xms20m -Xmx20m
0.142: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0011850 secs]
[Parallel Time: 0.8 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 142.5, Avg: 142.5, Max: 142.5, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 1.0]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.4, Avg: 0.4, Max: 0.4, Diff: 0.0, Sum: 1.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 2.3, Max: 4, Diff: 3, Sum: 9]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.7, Avg: 0.7, Max: 0.7, Diff: 0.0, Sum: 2.8]
[GC Worker End (ms): Min: 143.2, Avg: 143.2, Max: 143.2, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 3072.0K(12.0M)->0.0B(5120.0K) Survivors: 0.0B->1024.0K Heap: 6644.7K(20.0M)->4800.1K(20.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.144: [GC concurrent-root-region-scan-start]
0.144: [GC concurrent-root-region-scan-end, 0.0005262 secs]
0.144: [GC concurrent-mark-start]
0.144: [GC concurrent-mark-end, 0.0000479 secs]
0.144: [GC remark 0.145: [Finalize Marking, 0.0000752 secs] 0.145: [GC ref-proc, 0.0001162 secs] 0.145: [Unloading, 0.0005547 secs], 0.0008940 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.146: [GC cleanup 8896K->8896K(20M), 0.0003048 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.146: [GC pause (G1 Humongous Allocation) (young), 0.0008979 secs]
[Parallel Time: 0.6 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 146.3, Avg: 146.3, Max: 146.3, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.7]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.3, Avg: 0.3, Max: 0.3, Diff: 0.0, Sum: 1.4]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 7]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 0.5, Avg: 0.5, Max: 0.5, Diff: 0.0, Sum: 2.1]
[GC Worker End (ms): Min: 146.8, Avg: 146.9, Max: 146.9, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(5120.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 9920.2K(20.0M)->9862.6K(20.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.147: [Full GC (Allocation Failure) 9862K->9846K(20M), 0.0024553 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 9862.6K(20.0M)->9846.9K(20.0M)], [Metaspace: 3016K->3016K(1056768K)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
0.150: [Full GC (Allocation Failure) 9846K->9831K(20M), 0.0025859 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 9846.9K(20.0M)->9831.6K(20.0M)], [Metaspace: 3016K->3016K(1056768K)]
[Times: user=0.02 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.demo.GcTest.main(GcTest.java:11)
Heap
garbage-first heap total 20480K, used 9831K [0x00000000fec00000, 0x00000000fed000a0, 0x0000000100000000)
region size 1024K, 1 young (1024K), 0 survivors (0K)
Metaspace used 3034K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 323K, capacity 386K, committed 512K, reserved 1048576K
- 日志第一行:
0.142: [GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0011850 secs]
表示在应用程序开启0.148秒时发生了一次新生代GC,这是在初始标记时发生的,耗时0.0026345秒,意味着应用程序至少暂停了0.0026345秒。
- 后续并行时间:
[Parallel Time: 0.8 ms, GC Workers: 4]
表示所有GC线程总的花费时间,这里为1毫秒。并行回收时GC的工作线程数量是4。
- 给出每一个GC线程的执行情况:
[GC Worker Start (ms): Min: 142.5, Avg: 142.5, Max: 142.5, Diff: 0.0]
这里表示一共4个GC线程。同时,还给出了这几个启动数据的统计值,如平均(Avg)、最小(Min)、最大(Max)和差值(Diff)。Diff表示最大值和最小值的差。
- 给出了根扫描的耗时:
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.3, Diff: 0.1, Sum: 1.0]
在根扫描时,给出了这些耗时的统计数据。
- 给出了更新记忆集(Remembered Set)的耗时:
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
记忆集是G1中维护的一个数据结构,简称RS。每一个G1区域都有一个RS与之关联。由于G1回收时是按照区域回收的,比如在回收区域A的对象时,很可能并不回收区域B的对象,为了回收区域A的对象,要扫描区域B甚至整个堆来判定区域A中哪些对象不可达,这样做的代价显然很大。因此,G1在区域A的RS中,记录了在区域A中被其他区域引用的对象,这样在回收区域A时,只要将RS视为区域A根集的一部分即可,从而避免做整个堆的扫描。由于系统在运行过程中,对象之间的引用关系是可能时刻变化的,为了更高效地跟踪这些引用关系,会将这些变化记录在Update Buffers中。这里的Processed Buffers指的就是处理这个Update Buffers数据。这里给出的4个时间也是4个GC线程的耗时,以及它们的统计数据。从这个日志中可以看到,更新RS时分别耗时5.7、5.4、28、5.3毫秒,平均耗时11.1毫秒。
- 扫描RS的时间:
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
- 在正式回收时,G1会对被回收区域的对象进行疏散,即将存活对象放置在其他区域中,因此需要进行对象的复制。
[Object Copy (ms): Min: 0.4, Avg: 0.4, Max: 0.4, Diff: 0.0, Sum: 1.7]
这里给出的Object Copy就是进行对象赋值的耗时。
- 给出GC工作线程的终止信息:
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Termination Attempts: Min: 1, Avg: 2.3, Max: 4, Diff: 3, Sum: 9]
这里的终止时间是线程花在终止阶段的耗时。在GC线程终止前,它们会检查其他GC线程的工作队列,查看是否仍然还有对象引用没有处理完,如果其他线程仍然有没有处理完的数据,请求终止的GC线程就会帮助它尽快完成,随后再尝试终止。其中Termination Attempts展示了每一个工作线程尝试终止的次数。
- 显示GC工作线程的完成时间:
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [GC Worker Total (ms): Min: 0.7, Avg: 0.7, Max: 0.7, Diff: 0.0, Sum: 2.8] [GC Worker End (ms): Min: 143.2, Avg: 143.2, Max: 143.2, Diff: 0.0]
显示GC线程花费在其他任务中的耗时,单位是毫秒,可以看到这部分时间非常少: 这里显示了在系统运行后143.2毫秒,这几个线程都终止了。
- 显示清空CardTable的时间,RS就是依靠CardTable来记录哪些是存活对象的:
[Clear CT: 0.1 ms]
- 显示其他几个任务的耗时:
[Other: 0.3 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.2 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms]
比如选择CSet(Collection Sets)的时间、Ref Proc(处理弱引用、软引用的时间)、Ref Enq(弱引用、软引用入队时间)和Free CSet(释放被回收的CSet中区域的时间,包括它们的RS)。
注意: Collection Sets表示被选取的、将要被回收的区域的集合。
- 显示比较熟悉的GC回收的整体情况:
[Eden: 3072.0K(12.0M)->0.0B(5120.0K) Survivors: 0.0B->1024.0K Heap: 6644.7K(20.0M)->4800.1K(20.0M)] [Times: user=0.00 sys=0.00, real=0.00 secs]