分代垃圾回收
垃圾回收基础
如下图所示:
- 垃圾回收器主要回收堆内存,堆内存分为:新生代和老年代。
- 对于回收新生代GC:Minor GC或者叫Young GC。回收老年代的GC叫:Major GC 或者 Old GC.
- 需要注意Full GC:它不止回收堆内存,还会回收方法区(在JDK1.8 方法区在元空间;在JDK1.7 方法区在永久代)
分代回收的理论:
- 把绝大多数(98%)的朝生夕死的对象放在新生代
- 把熬过多次垃圾回收的对象就越难回收放在老年代
那么经过上述理论划分为新生代和老年代,那么垃圾回收算法是怎样的呢?
- 在新生代的算法:复制算法
- 在老年代的算法:标记清除算法、标记整理算法
垃圾回收算法
复制算法
实现简单、运行高效,没有内存碎片,但是空间利用率只有一半。
复制算法的原理是什么呢?
- 需要将内存空间一分为二,一半用起来,另一半预留
- 如下图所示:一半的空间用作预留的空间它在GC之前是不会分配对象的,而另一半会进行分配对象,我们加入下图中的圆是一个对象,这时候存储一半空间满了,就会进行GC垃圾回收。
那么就有了下面的第二步。
- 将存活的对象复制到预留的空间中去,这时候预留的空间就会进行分配对象,没有存活的对象直接回收掉,另一半的空间格式化掉当做预留的空间。
- 我们假设,上述的四个对象,只剩下两个对象存活,那么这两个对象会Copy到预留空间中,之前的空间会直接格式化,那么经过第二步变成了如下图的结果:
- 之后new新的对象会放在当前的空间,如果空间又满了,就会继续走第二步。
下面我们来看一下堆区的新生代是如何利用复制算法?
Eden区的来源:Appel式回收、提高空间利用率和空间分配担保 Eden区的来源,主要是解决上述复制算法理论的基础,空间利用率只有一半的问题 Eden 区占80% from区10% to区10% 几乎所有的对象优先在Eden区分配,当Eden区满了之后,就会判断对象存活(98%的对象是朝生夕死的),垃圾回收,存活对象进入From区,之后Eden就会全部清空,为下一次分配对象做准备。当Eden区又满了,就会继续触发垃圾回收,Eden存活的对象就会放在To区,同时From区的对象经过可达性分析存活的对象也会进入到To区。可能只看文字看的似懂非懂,下面我通过画图来展示。
- Eden 区几乎所有对象会在此分配,一般大对象是直接进入老年代
- 当Eden区空间满了以后,就会触发垃圾回收,根据根可达判断存活的对象,将存活的对象分配到From区,同时Eden区清空,为一下次分配对象做准备
- 当Eden区空间又满了,触发GC,根据根可达判断存活对象,将存活的对象复制到To区,同时From区的对象经过可达性分析存活的对象复制到To区
- 当Eden区又满了,就会触发同样的步骤,存活的对象会在From和To之间来回复制。
- 经过上述n个步骤,我们知道From区和To区只有10%的空间,当Eden区满了,而这时要将存活的对象移到To区,然而经过n个步骤存活的对象太多了超过了10%的预留空间,这时候就会触发空间分配担保操作, 就会进入老年代区域中。同时JVM还有一个优化的操作。(在新生代的对象没经过一次GC就会增加分代年龄,当分代年龄达到15(不是绝对的)对象还存活,就会进入老年代区)
通过上述图示,应该将新生代的复制算法讲解清楚了。新生代的复制算法利用Eden区将空间利用率降低到了只有10%是预留的,其他的空间都是利用的。
注意:如果是个大对象就会直接进入老年代。
标记清除算法(Mark-Sweep)
位置不连续,产生碎片;效率略低,两遍扫描;空间利用率100%
- 扫描空间根据可达性分析标记可回收的对象,打上标记。
- 扫描空间根据标记可回收的对象,进行清除。
标记清除算法会导致内存不连续,产生碎片。
假设一个对象占用的内存空间占据5个格子,但是回收后的空间,不连续的没有可存放的位置了,分配不下去了。
也就有了标记-整理算法,对内存不连续的内存空间进行整理
标记整理算法(Mark-Compact)
- 没有内存碎片,效率偏低,两遍扫描,指针需要调整。
标记整理算法的效率是偏低的,因为需要移动指针,假设一个对象的指针是在1号,而通过标记整理指针变成了10号位置,那么对应的引用就需要改变,方法要改变那么全部的线程要暂停把对应的引用进行更新。
- 第一次扫描,标记可回收的对象,这个和标记清除算法的第一步类似
- 第二次扫描,清除可回收的对象,整理内存空间。(这一步比标记清除算法多了一步空间整理,使空间连续)
JVM中常见的垃圾回收器
- 单线程垃圾回收器
- 多线程并行垃圾回收器
- 并发垃圾回收器
回收器 | 回收对象和算法 | 回收器类型 |
---|---|---|
Serial | 新生代,复制算法 | 单线程(串行) |
Parallel Scavenge | 新生代,复制算法 | 并行的多线程回收器 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 |
Serial Old | 老年代,标记整理算法 | 单线程(串行) |
Parallel Old | 老年代,标记整理算法 | 并行的多线程回收器 |
CMS | 老年代,标记清除算法 | 并发的多线程回收器 |
G1 | 跨新生代和老年代:标记整理+化整为零 | 并发的多线程回收器 |
- 单线程收集 Serial/Serial Old :
参数:UseSerialGC 最古老的单线程、独占。只适用于几十兆到一两百兆,每次垃圾回收都会停顿 STW(Stop The World) ,停止业务线程,等垃圾回收完毕之后更新业务线程的引用。
如下图,单线程收集
当GC的时候会暂停所有用户线程,启动GC线程
STW(Stop The World):垃圾回收器的发展的重点就是降低STW。随着垃圾回收器的发展也就有了多线程的垃圾回收器和并发的垃圾回收器
- 多线程收集 Parallel Scavenge/Parallel Old
Parallel Scavenge/Parallel Old 是JDK1.8默认的垃圾收集器
Parallel Scavenge:回收新生代
Parallel Old:回收老年代
垃圾回收器是多线程的提高回收的效率
下面是相关的参数:
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
UseAdaptiveSizePollcy : 完成最大的吞吐量
Parallel Scavenge/Parallel Old 垃圾收集器追求的是吞吐量,但是STW并没有减少多少。
STW会带来哪些危害?
假设STW垃圾回收暂停了10s 而 请求响应只用了1s 那么总共请求 1s + 10s= 11s
为了减少STW的时间,于是就有了并发垃圾回收器。
- 并发垃圾回收器 CMS(Concurrent Mark Sweep)/ParNew
CMS 主要针对老年代的垃圾回收器,主要是降低STW。由于Parallel Scavenge是新生代的垃圾回收器,但是Parallel Scavenge追求的吞吐量,并不能降低STW,于是就有了ParNew 来和CMS配对,ParNew 负责新生代、CMS 负责老年代。
CMS 采用的是标记清除算法,那么CMS是如何并发标记清除呢?看下面:
- 初始标记: 标记GC roots有直接关系的对象,标记速度会非常快。这时候就会暂停业务线程
- 并发标记: 进行GC Roots Tracing的过程,标记可回收的对象,而这时候的标记量会很大,时间会很长所以采取并发的方式,让业务线程和并发标记一起跑,不会暂停业务线程。GC和用户线程同时跑,肯定会有一些偏差,中间还有一些变化和没有标记的,所以就有了重新标记
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分的标记记录,这个过程会比初始标记长一些,但是远比并发标记端,所以会暂停所有用户线程(STW),标记剩余的,这段是时间较短的。
- 并发清除:在上述讲到的标记清除算法中,需要挨个删除标记的可回收的对象,这种操作肯定时间比较长,所以使用并发清除让用户和GC同时运行,
- 重置线程
在并发标记和并发清除可以很有效的降低STW。
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿
CMS的缺点:
- 对CPU资源敏感:面向并发设计的程序都对CPU资源比较敏感,在并发阶段虽然不会导致用户线程停顿,但是会占用一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数时(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,当CPU不足4个时,CMS对用户程序的影响就很可能变得很大。
- 无法处理浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再次收集处理他们,只好留待下一次GC时再清理掉。假如老年代100m,当到了92m触发垃圾回收,而这时候老年代有占用了90m,而这时候产生了20m的浮动垃圾如何处理呢?就会采用Serial Old来进行替代
- 标记清除算法导致的内存碎片,当空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。
CMS 的垃圾回收器带来的问题还是非常多的。维护起来非常麻烦的,所以在JDK1.7 JDK1.8都没有将CMS设置成默认的垃圾回收器。但是CMS是并发标记的垃圾回收器的基石,虽然有很多问题,但是CMS的并发标记思想才有了后面G1的优秀的垃圾回收器
为什么CMS不使用标记整理算法?
如果过采用标记整理算法,需要清理和整理的同时用户线程还会跑,而标记整理涉及到了指针的调整,那么CMS还得在多做一步,而使用标记清除只需要标记的清除就可以了。
在CMS之后,JVM推出了G1垃圾回收器。
- G1(Garbage First) 垃圾回收器
G1:追求停顿时间;Region区;筛选回收;可预测停顿;复制和标记整理算法
G1 将整个堆区化整为零,有了Region区:
- E和S是新生代;
- O 是老年代
- H 表示大对象也是老年代
G1 垃圾回收器的运作分为如下几个步骤:
- 初始标记: 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
- 并发标记: 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
- 筛选回收 :首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
开启G1垃圾收集器的参数: