farmerjohngit / myblog

有深度的Java技术博客
1.83k stars 287 forks source link

JVM垃圾回收历险 #3

Open farmerjohngit opened 6 years ago

farmerjohngit commented 6 years ago

JVM垃圾回收历险

一句话介绍,垃圾回收就是由程序自动的回收已死对象。

已死对象就是程序中一定不会再被使用到的对象。

垃圾回收可以分为两个部分,一是如何判断对象已死,二是如何清理掉已死对象。

判断对象已死的方法

引用计数法

引用计数法比较好理解,就是为每个对象分配一个计数器,当一个对象被另一个对象引用时,其对应的计数器+1,当引用关系被解除时,计数器-1。当一个对象的计数器值为0时,则代表该对象可以被回收了。

引用计数法的优点是实现简单且回收效率高,而缺点就是无法解决循环引用的问题。

引用计数法在Python等一些语言中有使用到,但jvm并没有采用,关键原因也是其无法解决循环引用的问题(那python的循环引用对象不能被回收?)。

可达性分析

可达性分析是商用jvm中采用的判断对象已死算法。

该算法类似于图遍历,我们把所有对象描述为一张图,节点是对象,边是引用关系。

GC ROOT节点出发,遍历所有节点,对于遍历到的每个节点都做一个标识,遍历完成后。没有标识的节点说明是可回收的。

这里的GC ROOT在JVM中指的是以下几类对象:

  1. 被栈中的本地变量表引用的对象
  2. 被静态变量引用的对象
  3. 被常量引用的对象
  4. 被JNI方法中引用的对象

回收算法

上一节介绍了垃圾回收第一步:判断对象是否可以被回收,这一小节则会阐述一些常用的回收算法。

标记清除

标记清除算法也比较简单。通常使用一张(类似)来记录哪些空间已被使用。首先通过可达性分析找到所有的垃圾,然后将其占用的空间释放掉。 该算法的问题是可能会产生大量的内存碎片。 image

标记整理

为了解决内存碎片的问题,标记整理在标记清楚算法上做了优化,在找到所有垃圾对象后,不是直接释放掉其占用的空间,而是将所有存活对象往内存一端移动。回收完成后,所有对象都是相邻的。 image

复制算法

复制算法将内存区域划分为两个,同一时间只有一个区域有对象,每次垃圾回收时,通过可达性分析算法,找出所有存活对象,将这些存活对象移动到另一区域。为新对象分配内存时,可以通过智能指针的形式,高效简单。 复制算法的缺点是会浪费一部分空间以便存放下次回收后存活的对象且需要一块额外的空间进行担保(当一个区域存放不下存活的对象时)。

分代收集

在商用jvm中,大多使用的是分代收集算法。 根据对象的特性,可以将内存划分为3个代:年轻代,老年代,永久代(jvm8后称为元空间)。年轻代存放新分配的对象,使用的是复制算法,老年代使用标记清除or标记整理算法。其中年轻代分为一个Eden区和两个Survivor区,其比例默认为8:1:1(-XX:SurvivorRatio),新对象在Eden区分配,当Eden区存放不下时,触发一次Young GC,将Eden区和一个Survivor区域的所有存活对象拷贝到另一个Survivor区域。如果对象大小超过一定大小(-XX:PretenureSizeThreshold),则会直接在老年代分配。老年代采用标记清除或标记整理算法。

年轻代老年代比例默认为3:8(-XX:NewRatio,-Xmn),老年代一般来说要比年轻代要大,因为当年轻代空间不足以存放下新对象时,需要老年代来担保。

年轻代使用复制算法的原因是年轻代对象的创建和回收很频繁,同时大部分对象很快都会死亡,所以复制算法创建和回收对象的效率都比较高。

老年代不使用复制算法的原因是老年代对象通常存活时间比较长,如果采用复制算法,则复制存活对象的开销会比较大,且复制算法是需要其他区域担保的。 所以老年代不使用复制算法。

垃圾回收器

下文将介绍jvm中常用的垃圾回收器

Serial串行回收器(年轻代)

使用单线程,复制算法实现。在回收的整个过程中需要Stop The World。在单核cpu 的机器上,使用单线程进行垃圾回收效率更高。

使用方法:-XX:+UseSerialGC

ps:在jdk client模式,不指定VM参数,默认是串行垃圾回收器

Serial Old串行回收器(老年代)

与Serial相似,但使用标记整理算法实现。

ParNew并行回收器(年轻代)

Serial的多线程形式, -XX:+UseParNewGC(新生代使用并行收集器,老年代使用串行回收收集器)或者-XX:+UseConcMarkSweepGC(新生代使用并行收集器,老年代使用CMS)。

Parallel Scavenge 基于吞吐量的并行回收器(年轻代)

多线程的回收器,高吞吐量(=程序运行时间/(程序运行时间+回收器运行时间)),可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对响应时间要求不高的场景。

有一个自适应条件参数(-XX:+UseAdaptiveSizePolicy),当这个参数打开后,无需手动指定新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio)等参数,虚拟机会动态调节这些参数来选择最适合的停顿时间(-XX:MaxGCPauseMillis)或吞吐量( -XX:GCTimeRatio)。

Parallel Scavenge是Server级别多CPU机器上的默认GC方式,也可以通过-XX:+UseParallelGC来指定,并且可以采用-XX:ParallelGCThread来指定线程数。

Parallel Scavenge对应的老年代收集器只有Serial Old和Parallel Old。不能与CMS搭配使用的原因是,其使用的框架不同,并不是技术原因。

Parallel Old 基于吞吐量的并行回收器(老年代)

使用多线程和“标记-整理”算法。与Parallen Scavenge相似,只不过是运用于老年代。

CMS 关注暂停时间的回收器 (老年代)

基于标记清除算法实现,关注GC的暂停时间,在注重响应时间的应用上使用。

在说CMS具体步骤前,我们先看下CMS使用的垃圾标记算法:三色标记法

三色标记法

将堆中对象分为3个集合:白色、灰色和黑色

白色集合:需要被回收的对象

黑色集合:没有引用白色集合中的对象,且从GC ROOT可达。该集合的对象是不会被回收的

灰色集合:从根可达但是还没有扫描完其引用的所有对象,该集合的对象不会被回收,且当其引用的白色对象全部被扫描后,会将其加入到黑色集合中。

一般来说,会将被GC ROOT直接引用到的对象初始化到灰色集合,其余所有对象初始化到白色集合,然后开始执行算法:

1.将一个灰色对象加入到黑色集合

2.将其引用到的所有白色对象加入到灰色集合

3.重复上述两步,直到灰色集合为空

该算法保证从GC ROOT出发,所有没有被引用到的对象都在白色集合中,所以最后白色集合中的所有对象就是要回收的对象

CMS回收过程

分为4个过程,初始标记,并发标记,重新标记,并发清理。

初始标记: 从GC ROOT出发,找到所有被GC ROOT直接引用的节点。此过程需要Stop The World。

并发标记: 以上一步骤的节点为根节点,并发的遍历所有节点。同时会开启Write Barrier.如果在此过程中存在黑色对象增加对白色对象的引用,则会记录下来。

在这里,我们试想下如果三色标记法是先执行步骤2后执行步骤1(上面三色标记算法的步骤),会发生什么?

如下图,在GC过程中,用三色标记法遍历到A这个对象(图1),将A引用到的BCD标记为灰色。之后,在应用程序线程中创建了一个对象E,A引用了它( 图2这个阶段GC是并发标记的)。然后将A标记为黑色(图3)。在gc扫描结束后,E这个对象因为是白色的,所以将被回收掉。这显然是不能接受的,并发垃圾回收器的底线是允许一部分垃圾暂时不回收(见下面的浮动垃圾),但绝不允许从根可达的存活对象被当作垃圾处理掉! image

重新标记: 因为并发标记的过程中可能有引用关系的变化,所以该阶段需要Stop The World。以GC ROOTWritter Barrier中记录的对象为根节点,重新遍历。 这里为什么还需要再遍历GC ROOT?因为Writter Barrier是作用在堆上的,无法感知到GC ROOT上引用关系的变更。

并发清理: 并发的清理所有垃圾对象

CMS通过将步骤拆分,实现了降低STW时间的目的。但CMS也会有以下问题:

1.浮动垃圾,在并发标记的过程中(及之后阶段),可能存在原来被引用的对象变成无人引用了,而在这次gc是发现不会清理这些对象的。

2.cpu敏感,因为用户程序是和GC线程同时运行的,所以会导致GC的过程中程序运行变慢,gc运行时间增长,吞吐量降低。默认回收线程是(CPU数量+3)/4,也就是cpu不足4个时,会有一半的cpu资源给GC线程。

3.空间碎片,标记清除算法都有的问题。当碎片过多时,为大对象分配内存空间就会很麻烦,有时候就是老年代空间有大量空间剩余,但没有连续的大空间来分配当前对象,不得不提前触发full gc。CMS提供一个参数(-XX:+UseCMSCompactAtFullCollection),在Full Gc发生时开启内存合并整理。这个过程是STW的。同时还可以通过参数(-XX:CMSFullGCsBeforeCom-paction)来这只执行多少次不压缩的Full GC后,来一次压缩的。

4.需要更大的内存空间,因为是同时运行的GC和用户程序,所以不能像其他老年代收集器一样,等老年代满了再触发GC,而是要预留一定的空间。CMS可以配置当老年代使用率到达某个阈值时( -XX:CMSInitiatingOccupancyFraction=80 ),开始CMS GC。

在old GC运行的过程中,可能有大量对象从年轻代晋升,而出现老年代存放不下的问题(因为这个时候垃圾还没被回收掉),该问题叫Concurrent Model Failure,这时候会启用Serial Old收集器,重新回收整个老年代。Concurrent Model Failure一般伴随着ParNew promotion failed(晋升担保失败),解决这个问题的办法就是可以让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法,或者降低触发cms gc的阈值

G1 新一代垃圾回收器 (整个堆)

讲实话,我还没太看懂,没法写自己的理解, 少年等我。。

gMan1990 commented 5 years ago

年轻代老年代比例默认为3:8???

gezhiwei8899 commented 2 years ago

老哥 图都裂了,能维护下吗?

silentmoooon commented 2 years ago

老哥 图都裂了,能维护下吗?

直接复制标题去百度,基本上都能找到图正常的转载,哈哈, 不过这篇貌似要给标题加上双引号或google才能找到