jvm系列:垃圾回收

941 阅读25分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

垃圾回收(Garbage Collection,GC),就是释放垃圾占用的空间,防止内存泄露。更有效的利用内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

当我们调用一个方法的时候,就会创建这个方法的栈帧,当方法调用结束的时候,这个栈帧出栈,栈帧所占用的内存也随之释放。如果这个线程销毁了,那与这个线程相关的栈以及程序计数器的内存也随之被回收,那在堆内存中创建的对象还在占着很多的内存资源的。因此我们需要知道哪些对象是可以回收的,哪些对象是不能回收的。先看JVM内存分布的图

jvm顺序图.PNG 垃圾回收主要是针对堆和方法区进行的。因为程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,因存在于线程的生命周期内,当线程结束之后就会小时,因此不需要对这三个区域进行回收。

方法区存的是主要是类信息和运行时常量池,在很多大量使用反射,动态代理,CGLIB等 ByteCode框架,以及动态生成JSP,以及OSGI这类频繁定义ClassLoder的地方,方法区的运行时数据区在运行时疯狂的增加内容,没有垃圾回收器很容易发生OOM。还有这个运行时常量池也会出问题,以前的垃圾回收器不管这部分内存,如果运行时有很多的常量产生,那么常量池就会变得很大最后内存溢出引发事故,后来JVM重视这一块也开始垃圾回收。 另外堆区也是大量 OOM 的地方,运行期间可能会有数不清的对象产生,是垃圾回收期的重点照顾对象。

初识GC

在GC里面有两种算法来判断,一种是引用计数,对象引用的次数为0就是垃圾,另一种是可达性算法,如果一个对象不在以GC Root根节点为起点的引用链中,则视为垃圾。

如何判断对象是否可回收

上面讲了GC主要作用的区域是在堆中,那么又是怎么判断是否可以回收的呢?在GC里面有两种算法来判断,一种是引用计数,对象引用的次数为0就是垃圾,另一种是可达性算法,如果一个对象不在以GC Root根节点为起点的引用链中,则视为垃圾。

引用计数算法

首先看引用计数法,其思想是给对象加一个引用计数器, 对象被引用,就会在此对象的头上计数器加一,每当有一个引用失效时计数器的值减一,如果没有引用(引用次数为0)则此对象可回收。但是这种算法很难解决对象之间互相循环引用的问题。如下:

public class TestObject {

    public Object ref = null;

    public static void main(String[] args) {

        MyObject tObject1 = new TestObject();

        MyObject tObject2 = new TestObject();

        tObject1.ref = tObject2;

        tObject2.ref = tObject1;

        tObject1 = null;

        tObject2 = null;  
}

可达性算法

其思想就是从一系列的GC Root一直往下搜索,通过GC Root串成的一条线称为引用链,如果有对象不在任何一条以GC Root为起点的引用链中,则此对象就会被GC回收,这就是可达性算法。

可达性算法.PNG

以下对象可作为GC Root对象:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

System Class(系统类,例如Java.util.*)

Thread Block(一个对象存活在一个阻塞的线程中)

Thread(线程),正在运行的线程

Busy Monitor 调用了wait()或notify()或已同步的所有内容

GC必须是安全可靠的,存活的对象不能被错误的标记为垃圾而被释放掉,而不再存活的对象应该被及时标记并且被回收。GC的执行应该是高效的,因为一般来说,GC在进行垃圾收集的时候需要Stop-the-world,所以,GC活动的时间越短越好,还有一点需要注意的是,GC过后可能会造成内存碎片问题,为了清除内存碎片,应该选择具备内存压缩能力的GC算法,但是这个过程需要非常高效,因为这不是GC的主要功能,合理的做法应该是GC之后不强制进行内存压缩操作,只有在GC过后一段时间频繁发现因为内存碎片问题而造成内存申请失败的情况下再进行内存压缩,因为这个时候JVM除了内存压缩别无选择。下面列举出了几个设计GC的性能指标:

1.吞吐量:垃圾回收的过程应该尽量高效,运行正常的应用的时间占比应该尽量高 2.垃圾收集代价:垃圾收集所占的时间应该尽量少 3.Stop-the-world的时候应该尽量少 4.垃圾收集频率:不能频繁进行GC活动,时间应该花在应用的运行上

常见的垃圾回收算法

垃圾回收的几种方式:

标记-清除算法(Mark-Sweep):分为标记和清除两个步骤。第一步根据可达性算法标记被回收的对象,第二步回收被标记的对象。缺点有两方面,在效率上来说,标记和清除的过程效率都不高,在空间上来说,清除过后会产生很多不连续的内存碎片,会造成很多内存碎片问题。

标记清楚.PNG

复制算法(Copying):算法将可用内存分为相同的两部分,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。在对象存活率较高的时候效率会较低,因为要进行较多的复制操作。

复制算法.PNG

复制算法.PNG 标记-整理算法(Mark-Compact):和标记-清除一样,前一个过程是一样的,都是将可回收的对象标记起来,但是标记-整理算法在标记起来之后,不是进行简单的清除,而是将所有的存活对象往一端移动,然后将那些需要清除的对象清除。

标记整理.PNG

分代回收算法(Generational GC): 分代搜集算法是针对对象的不同特性,而使用适合的算法,这里面并没有实际上的新算法产生。与其说分代收集算法是第四个算法,不如说它是对前三个算法的实际应用。

首先我们先探讨一下对象的不同特性,内存中的对象其实可以根据生命周期的长短大致分为三种:

夭折对象(新生代):朝生夕死的对象,比如方法里的局部变量。

持久对象(老年代):存活的比较久但还是要死的对象,比如缓存对象,单例对象等等。

永久对象(永久代):对象生成后几乎不灭的对象,例如String池中的对象(享元模式)、加载过的类信息等等。

上述的对象对应在内存中的区域就是,夭折对象和持久对象在Java堆中,永久对象在方法区。

分代算法的原理就是根据对象的存货周期不同将堆分为年轻代和老年代。新生代又分为Eden 区,from Survivor 区(S0区),to Survivor 区(S1区),比例为8:1:1。

年轻.PNG

年轻代采用的回收算法是复制算法。新建的对象被创建后就会分配在Eden 区,当Eden区将满时,就会触发GC。

Eden.PNG

触发gc.PNG

在这一步GC会把大部分夭折对象回收,根据可达性算法标记出存活的对象,把存活对象复制到S0区,然后清空Eden 区。

年龄1.PNG

接着继续到下一次触发GC时,就会把Eden区和S0区的存活对象复制到S1区,然后清空Eden区和S0区。每次垃圾回收后S0和S1区的角色互换。每次GC后,如果对象存活下来则年龄加一。

+1.PNG

在年轻代中存活得越久的对象,年龄会越大,如果存活对象的年龄达到了我们设定的阈值,则会从S0(或S1)晋升到老年代。由于老年代的对象一般不会经常回收,所以采用的算法是标记整理法,老年代的回收次数相对较少,每次回收时间比较长。

三种算法在是否移动对象、空间和时间方面的一些对比,假设存活对象数量为 L、堆空间大小为 H,则:

1234.PNG

mark、sweep、compaction、copying 这几种动作的耗时其关系如下:

关系.PNG

常见的垃圾收集器

垃圾收集器其实就是上面讲的算法的具体实现,目前在 Hotspot VM 中主要有分代收集和分区收集两大类,不过未来会逐渐向分区收集发展。常见的垃圾收集器除了G1垃圾收集器外,都是只作用于一个区域,要么年轻代要么老年代,所以一般是配合使用。

收集器.PNG

收集器.PNG

可以根据GC算法的运行模式分为下面几类:

单线程GC:单线程GC在实现上使用单一线程来进行垃圾收集活动,比如Serial GC

并行GC:每次运行时,不管是YGC,还是FGC,会 stop-the-world,暂停所有的用户线程,并采用多个线程同时进行垃圾收集,比如Parallel GC。

并发GC:在新生代进行垃圾收集时和并行收集器类似,都是并行收集,而且都会stop-the-world,主要的区别在于老年代的收集上,在老年代进行垃圾收集时,大部分时间可以和用户线程并发执行的,只有小部分的时间stop-the-world,这就是它的优势,可以大大降低应用的暂停时间,比如CMS GC。

Serial收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvmclient模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率,适用于单cpu机器的场景。在用户的桌面应用场景中,即Client模式下的虚拟机来说是一个很好的选择。通过-XX:+UseSerialGC可以开启这种回收模式

serial.PNG

ParNew收集器

parnew.PNG

Serial收集器的多线程版本,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、 收集算法、 Stop The World、 对象分配规则、 回收策略等都与Serial收集器完全一样。 开启参数:-XX:+UseParNewGC 适用场景: 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与老年代的CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

Parallel Scavenge收集器

Parallel Scavenge收集器也被称为吞吐量优先收集器,作用于年轻代,多线程采用复制算法的垃圾收集器,跟ParNew 收集器有些类似。和ParNew 收集器不同的是,Parallel Scavenge收集器关注的是吞吐量,它提供了两个参数来控制吞吐量,分别是-XX:MaxGCPauseMillis(控制最大的垃圾收集停顿时间)、 -XX:GCTimeRatio(直接设置吞吐量大小)。

如果设置了-XX:+UseAdaptiveSizePolicy参数,虚拟机就会根据系统的运行情况收集监控信息,动态调整新生代的大小,Eden,Survivor比例等,以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标,这种调节方式称为GC的自适应调节策略。这也是Parallel Scavenge收集器和ParNew 收集器最大的区别。

Serial Old收集器

Serial Old 收集器是工作在老年代的单线程垃圾收集器,采用的算法是标记整理算法。在Client模式下可以和Serial收集器配合使用,如果在Server模式的应用,在JDK1.5之前可以和Parallel Scavenge收集器配合使用,另一种使用场景则是CMS垃圾收集器的后备预案,在发生Concurrent Mode Failure使用。

Parallel Old(并行GC)收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器加Parallel Old收集器。

Parallel.PNG Parallel.PNG

CMS(并发GC)收集器

以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除,可参考文档 JEP 363**

CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

初始标记(CMS initial mark) 并发标记(CMS concurrenr mark) 重新标记(CMS remark) 并发清除(CMS concurrent sweep) 其中初始标记、重新标记这两个步骤需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

cms.PNG

优点:并发收集、低停顿。

缺点:cpu敏感,浮动垃圾,空间碎片。

CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。

由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。

在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。

要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。

G1收集器

G1收集器是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。

其特点如下:

并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能单独管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象已获得更好的收集效果。 空间整合:与CMS的“标记-清除”算法不同,G1收集器从整体上看是基于“标记-整理”算法实现的,从局部(两个Region之间)上看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序的长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。 G1 内部结构和机制 G1垃圾回收器的内存分区不再采用传统的内存分区,将新生代,老年代的物理空间划分取消了。 取而代之的是,把堆内存分成若干个Region(区域),每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的STW。G1垃圾回收器和传统的垃圾回收器的最大区别就在于,弱化了分代概念,引入了分区的思想。

G1.PNG

region 大小在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 region。当然这个数字既可以手动调整,G1 也会根据堆大小自动进行调整。

在G1实现中,一部分region 是作为Eden,一部分作为Survivor,除了Old region,G1 会将超过 region 50% 大小的对象归类为 Humongous 对象,并放置在相应的 region 中。逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。

缺点:

region 大小和大对象很难保证一致,这会导致空间的浪费。特别大的对象是可能占用超过一个 region 的。并且region 太小不合适,会令你在分配大对象时更难找到连续空间。

Humongous 对象的分配和回收 Humongous region 作为老年代的一部分,通常认为它会在并发标记结束后才进行回收,但是在新版 G1 中,Humongous 对象回收采取了更加激进的策略。G1 记录了老年代 region 间对象引用,Humongous 对象数量有限,所以能够快速的知道是否有老年代对象引用它。如果没有,能够阻止它被回收的唯一可能,就是新生代是否有对象引用了它,但这个信息是可以在 Young GC 时就知道的,所以完全可以在 Young GC 中就进行 Humongous 对象的回收,不用像其他老年代对象那样,等待并发标记结束。

参数说明

JVM参数描述
UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,采用Serial + Serial Old的GC组合进行内存回收
UseParNewGC打开此开关后,使用ParNew + Serial Old的GC组合二
UseConcMarkSweepGC使用ParNew + CMS + Serial Old 的GC组合,Serial Old GC作为CMS GC出现Concurrent Mode Failure后的备用GC
UseParallelGCJVM 运行在Server模式下的默认值,使用Parallel Scavenge GC + Serial Old 的GC组合
UseParallelOldGC使用Parallel Scavenge GC + Parallel Old GC的GC组合
UseG1GC使用G1 GC来进行垃圾收集
UseSerialGC虚拟机运行在Client模式下的默认值,打开此开关后,采用Serial + Serial Old的GC组合进行
MaxG1PauseMillis设置期望的GC停顿时间,仅对G1 GC有用
SurvivorRatio新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden : Survivor = 8 : 1
PretenureSizeThreshold直接晋升到老年代的对象大小阈值,设置这个参数后,大于这个参数的对象将直接晋升为老年代
MaxTenuringThreshold晋升到老年代的对象年龄,每个对象在坚持一次Young GC之后年龄增加1,超过这个参数就会晋升到老年代
UseAdaptiveSizePolicy动态调整java堆中各个区域的大小以及进入老年代的年龄
ParallelGCThreads设置并行GC时进行内存回收的线程数量
GCTimeRatioGC时间占总时间的比率,默认为1%,仅在使用Parallel Scavenge GC的时候有效
MaxGCPauseMillis设置GC的最大停顿时间,仅对Parallel Scavenge GC有效
CMSInitiatingOccupancyFraction设置CMS GC在老年代空间被使用多少后出发GC默认值为68%,仅对于CMS有效
UseCMSCompactAtFullCollection设置CMS GC在进行一次垃圾收集之后是否需要进行内存碎片整理
CMSFullGCsBeforeCompaction设置CMS GC在进行了若干次垃圾收集之后进行一次内存碎片整理

判断 GC 有没有问题

评判 GC 的两个核心指标:

延迟(Latency): 也可以理解为最大停顿时间,即垃圾收集过程中一次 STW 的最长时间,越短越好,一定程度上可以接受频次的增大,GC 技术的主要发展方向。

吞吐量(Throughput): 应用系统的生命周期内,由于 GC 线程会占用 Mutator 当前可用的 CPU 时钟周期,吞吐量即为 Mutator 有效花费的时间占系统总运行时间的百分比,例如系统运行了 100 min,GC 耗时 1 min,则系统吞吐量为 99%,吞吐量优先的收集器可以接受较长的停顿。

目前各大互联网公司的系统基本都更追求低延时,避免一次 GC 停顿的时间过长对用户体验造成损失,衡量指标需要结合一下应用服务的 SLA,主要如下两点来判断:

延迟.PNG

即为一次停顿的时间不超过应用服务的 TP9999,GC 的吞吐量不小于 99.99%。举个例子,假设某个服务 A 的 TP9999 为 80 ms,平均 GC 停顿为 30 ms,那么该服务的最大停顿时间最好不要超过 80 ms,GC 频次控制在 5 min 以上一次。如果满足不了,那就需要调优或者通过更多资源来进行并联冗余。(大家可以先停下来,看看监控平台上面的 gc.meantime 分钟级别指标,如果超过了 6 ms 那单机 GC 吞吐量就达不到 4 个 9 了。)

但是要分析 GC 的问题,先要读懂 GC Cause,即 JVM 什么样的条件下选择进行 GC 操作,具体 Cause 的分类可以看一下 Hotspot 源码:src/share/vm/gc/shared/gcCause.hpp 和 src/share/vm/gc/shared/gcCause.cpp 中。

const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";

    case _full_gc_alot:
      return "FullGCAlot";

    case _scavenge_alot:
      return "ScavengeAlot";

    case _allocation_profiler:
      return "Allocation Profiler";

    case _jvmti_force_gc:
      return "JvmtiEnv ForceGarbageCollection";

    case _gc_locker:
      return "GCLocker Initiated GC";

    case _heap_inspection:
      return "Heap Inspection Initiated GC";

    case _heap_dump:
      return "Heap Dump Initiated GC";

    case _wb_young_gc:
      return "WhiteBox Initiated Young GC";

    case _wb_conc_mark:
      return "WhiteBox Initiated Concurrent Mark";

    case _wb_full_gc:
      return "WhiteBox Initiated Full GC";

    case _no_gc:
      return "No GC";

    case _allocation_failure:
      return "Allocation Failure";

    case _tenured_generation_full:
      return "Tenured Generation Full";

    case _metadata_GC_threshold:
      return "Metadata GC Threshold";

    case _metadata_GC_clear_soft_refs:
      return "Metadata GC Clear Soft References";

    case _cms_generation_full:
      return "CMS Generation Full";

    case _cms_initial_mark:
      return "CMS Initial Mark";

    case _cms_final_remark:
      return "CMS Final Remark";

    case _cms_concurrent_mark:
      return "CMS Concurrent Mark";

    case _old_generation_expanded_on_last_scavenge:
      return "Old Generation Expanded On Last Scavenge";

    case _old_generation_too_full_to_scavenge:
      return "Old Generation Too Full To Scavenge";

    case _adaptive_size_policy:
      return "Ergonomics";

    case _g1_inc_collection_pause:
      return "G1 Evacuation Pause";

    case _g1_humongous_allocation:
      return "G1 Humongous Allocation";

    case _dcmd_gc_run:
      return "Diagnostic Command";

    case _last_gc_cause:
      return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";

    default:
      return "unknown GCCause";
  }
  ShouldNotReachHere();
}

重点需要关注的几个GC Cause:

System.gc(): 手动触发GC操作。

CMS: CMS GC 在执行过程中的一些动作,重点关注 CMS Initial Mark 和 CMS Final Remark 两个 STW 阶段。

Promotion Failure: Old 区没有足够的空间分配给 Young 区晋升的对象(即使总可用内存足够大)。

Concurrent Mode Failure: CMS GC 运行期间,Old 区预留的空间不足以分配给新的对象,此时收集器会发生退化,严重影响 GC 性能,下面的一个案例即为这种场景。

GCLocker Initiated GC: 如果线程执行在 JNI 临界区时,刚好需要进行 GC,此时 GC Locker 将会阻止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。

如何使用这些 Cause 触发回收,可参考 CMS 的代码,在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中。

bool CMSCollector::shouldConcurrentCollect() {
  LogTarget(Trace, gc) log;

  if (_full_gc_requested) {
    log.print("CMSCollector: collect because of explicit  gc request (or GCLocker)");
    return true;
  }

  FreelistLocker x(this);
  // ------------------------------------------------------------------
  // Print out lots of information which affects the initiation of
  // a collection.
  if (log.is_enabled() && stats().valid()) {
    log.print("CMSCollector shouldConcurrentCollect: ");

    LogStream out(log);
    stats().print_on(&out);

    log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full());
    log.print("free=" SIZE_FORMAT, _cmsGen->free());
    log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available());
    log.print("promotion_rate=%g", stats().promotion_rate());
    log.print("cms_allocation_rate=%g", stats().cms_allocation_rate());
    log.print("occupancy=%3.7f", _cmsGen->occupancy());
    log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
    log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
    log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end());
    log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect());
  }
  // ------------------------------------------------------------------

  // If the estimated time to complete a cms collection (cms_duration())
  // is less than the estimated time remaining until the cms generation
  // is full, start a collection.
  if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
   
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f",
                  _cmsGen->occupancy(), _bootstrap_occupancy);
        return true;
      }
    }
  }
  if (_cmsGen->should_concurrent_collect()) {
    log.print("CMS old gen initiated");
    return true;
  }

  CMSHeap* heap = CMSHeap::heap();
  if (heap->incremental_collection_will_fail(true /* consult_young */)) {
    log.print("CMSCollector: collect because incremental collection will fail ");
    return true;
  }

  if (MetaspaceGC::should_concurrent_collect()) {
    log.print("CMSCollector: collect for metadata allocation ");
    return true;
  }

  // CMSTriggerInterval starts a CMS cycle if enough time has passed.
  if (CMSTriggerInterval >= 0) {
    if (CMSTriggerInterval == 0) {
      // Trigger always
      return true;
    }

    // Check the CMS time since begin (we do not check the stats validity
    // as we want to be able to trigger the first CMS cycle as well)
    if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
      if (stats().valid()) {
        log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
                  stats().cms_time_since_begin());
      } else {
        log.print("CMSCollector: collect because of trigger interval (first collection)");
      }
      return true;
    }
  }

  return false;
}

GC 问题分类

大致分为如下六大类:

Unexpected GC: 意外发生的GC,实际上不需要发生,可以通过一些手段去避免。

Space Shock: 空间震荡问题。

Explicit GC: 显示执行GC问题。

Partial GC: 部分收集操作的GC,只对某些分代/分区进行回收。

Young GC: 分代收集里面的Young区收集动作,也可以叫做Minor GC。

ParNew: Young GC 频繁。

Old GC: 分代收集里面的Old区收集动作,也可以叫做Major GC,有些也会叫做Full GC,但其实这种叫法是不规范的,在CMS 发生Foreground GC 时才是Full GC,CMSScavengeBeforeRemark 参数也只是在Remark 前触发一次Young GC。

CMS: Old GC频繁。

CMS: Old GC不频繁但单次耗时大。

Full GC: 全量收集的GC,对整个堆进行回收,STW 时间会比较长,一旦发生,影响较大,也可以叫做Major GC。

MetaSpace: 元空间回收引发问题。

Direct Memory: 直接内存(也可以称作为堆外内存)回收引发问题。

JNI: 本地Native方法引发问题。

总结

本篇文章主要是介绍了JVM的垃圾回收的理论知识,思路是先搞懂GC作用的区域是在堆中,然后介绍可达性算法的作用是为了标记存活的对象,知道哪些是可回收对象,接着就是使用垃圾回收算法进行回收,然后介绍了常见的几种垃圾回收算法(标记清除,复制算法,标记整理),最后再介绍常见的几种垃圾回收器。 还有GC问题的常见场景的判断与分析等。