Java GC简介
什么是 GC ?
Java程序不用像C++程序在程序中自行处理内存的回收释放。这是因为Java在JVM虚拟机上增加了垃圾回收(GC)机制,用以在合适的时间触发垃圾回收.
你都了解哪些垃圾收集算法 ?
引用计数法, 根搜索法, 标记-清除算法, 复制算法, 标记-压缩算法, 分代收集算法
什么是引用计数法 ?
当引用了某个对象A, A的引用计数器就加1; 当一个引用失效时, 就减1.
只要对象A的引用变为0, 就说明对象A就不可能再被使用(因为没有引用了啊)., 就可以被回收了.
但是如此的引用计数法, 无法解决循环引用的问题: A引用B, B引用A, 没有其他的有效引用指向A, 也没有其他的有效引用指向B, 没有有效引用就应当被回收了, 但是此时计数器都是1, 不等于0, 不会被判定为垃圾, 就造成了AB都无法被回收的问题.
什么是根搜索法 ?
大多数JVM采用的是根搜索算法. 这种算法以根对象集合为起始点, 开始遍历可达性. 可达的对象才是存活的对象.
根对象集合包括哪些?
Java栈内的对象引用, 本地方法栈内的对象引用, 运行时常量池中的对象引用, 方法区中类静态属性的对象引用, 一个类对应一个数据类型的Class对象.
不可达对象会直接被回收吗?
如果重写了finalize()方法, 那么还是可以复活一次的.
覆盖了finalize()后因为会被放入到F-Queue中, 由一个低优先级的Finalizer线程来执行, 缓慢地进行清除.
如果在清除到该对象前, 该对象逃脱了(与引用链上的对象产生了联系), 那么就复活了.
但是只能复活一次, 下一次就不会再执行finalize()了, 也就不会再复活了.
请用代码验证一下finalize"复活"
public class TestGC {
private static TestGC instance=null;
private static void isAlive(){
if(instance == null){
System.out.println("我被回收了");
}else{
System.out.println("我还活着");
}
} @Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 方法执行完了");
instance=this;
} public static void main(String [] args) throws InterruptedException {
System.out.println("新建一个实例");
instance=new TestGC(); instance=null;
System.gc();
System.out.println("\ngc");
Thread.sleep(1000);
System.out.println("1秒后:");
isAlive(); instance=null;
System.gc();
System.out.println("\ngc");
isAlive();
}
}
标记-清除算法
先标记, 再清除. 缺点就是会产生空间碎片
复制算法
预留另一块空间, 将存活的对象复制过去, 然后将原先的所有内容清空即可.
标记-压缩算法
标记后进行压缩.
优点是解决了标记-整理的空间碎片问题, 解决了复制算法的需要预留一块同样大小的空间的问题.
缺点是, 为了实现压缩, 需要对堆内容进行更多次数的遍历.
标记压缩算法的实现有: Two-Finger 算法, LISP2 算法
分代收集算法
将对象根据对象的生命周期进行分类. 分为年轻代, 老年代, 持久代. 对不同生命周期的对象使用不同的算法进行垃圾回收.
你都了解哪些垃圾收集器?
Serial/Serial Old收集器, ParNew收集器, Parallel/Parallel Old收集器, CMS收集器, G1收集器
介绍一下Serial/Serial Old收集器
Serial:
1. 作用于年轻代.
2. Client模式下的默认垃圾收集器.
3. 采用复制算法.
4. 串行回收.
5. -XX:+UseSerialGC (年轻代串行, 老年代串行)
6. 适用于小存储器, 少CPU
Serial Old:
1. 作用于老年代.
2. 标记-压缩算法.
3. 串行回收.
4. -XX:+UseSerialGC (年轻代串行, 老年代串行)
5. -XX:+UseParNewGC (年轻代并行, 老年代串行)
6. 适用于小存储器, 少CPU
介绍一下ParNew收集器
1. 多线程版的Serial.
2. -XX:+UseParNewGC (年轻代并行, 老年代默认串行)
3. -XX:+UseParallelGC (年轻代并行, 老年代默认串行)
介绍一下Parallel收集器
1. 与ParNew不同, 可以控制吞吐量, 通过-XX:+GCTimeRatio, 设置内存回收的时间所占JVM运行总时间 的比例.公式为: 1/(1+N), 默认N为99, 即默认1%的时间用于垃圾回收.
2. -XX:+MaxGCPauseMills , 用于设置执行内存回收时
3. -XX:+UseAdaptiveSizePolicy, 自适应GC策略. 包括年轻代的大小, Eden和Survivor的比例, 晋升老年代的对象年龄, 吞吐量和停顿时间 等.
4. -XX:ParallelGCThreads可以用于设置垃圾回收时的线程数量.
年轻代并行收集器
1. -XX:+UseParallelGC: 年轻代使用并行回收收集器, 老年代使用串行收集器
2. -XX:+UseParallelOldGC: 年轻代和老年代都是使用并行回收收集器
3. 采用复制算法.
老年代
1. -XX:+UseParallelOldGC: 年轻代和老年代都是使用并行回收收集器
2. 采用标记压缩算法.
介绍一下CMS垃圾回收器
1. CMS全称是Concurrent-Mark-Sweep.
2. 标记清除算法
3. 对年老代进行垃圾收集
4. -XX:+UseConcMarkSweepGC, 使用CMS收集器执行内存回收任务.
5. 过程: (1)Stop the world: 这个阶段会标记出可达对象,然后恢复被暂停的所有线程. (2)并发标记:然后并发地将不可达对象标记为垃圾. (3)C再次标记阶段: 程序会再次"stop the world", 以确保这些垃圾对象能够被成功且正确地标记. (由于在并发标记垃圾对象的过程中, 应用线程和垃圾收集线程在同时运行, 可能无法确保之前被标记为垃圾的无用对象都能够被成功且正确地被标记.)
6. 前面提到的几个老年代垃圾回收器都是采用标记-压缩算法, 而CMS采用的是标记-清除算法, 这意味着会有内存碎片产生, 擦用空闲列表(Free List)执行内存分配.
7. -XX:+UseCMS-CompactAtFullCollection, 用于指定在执行完Full GC后是否对内存空间进行压缩整理.
8. -XX:CMSFullGCs-BeforeCompaction, 用于设置在执行多少次Full GC后堆内存空间进行压缩整理.
9. 在并发标记阶段如果产生新的垃圾对象, CMS将无法对这些垃圾对象进行标记, 最终会导致这些新产生的垃圾对象没有被及时回收, 从而只能在下一次执行GC时释放这些之前未被回收的内存空间.
10. -XX:CMSInitiatingFraction, 用于设置当老年代中的内存使用率达到多少百分比的时候执行内存回收(低版本的JDK默认为68%, JDK6以上版本默认值为92%).这里的内存范围仅限于老年代, 而非整个堆空间, 因此可以有效降低Full GC的次数.
11. "Promotion Failed" "Concurrent Mode Failure"时, 会触发Full GC.
12. CMS默认启动的线程数是: (年轻代并行收集器的线程数 + 3) / 4 , 可以使用-XX:ParallelCMSThreads参数设定CMS的线程数量.
CMS收集器在老年代堆内存的回收中执行分为以下阶段:
阶段 | 说明 |
---|---|
(1) 初始标记 (Initial Mark) | (Stop the World Event,所有应用线程暂停) 在老年代(old generation)中的对象, 如果从年轻代(young generation)中能访问到, 则被 “标记,marked” 为可达的(reachable).对象在旧一代“标志”可以包括这些对象可能可以从年轻一代。暂停时间一般持续时间较短,相对小的收集暂停时间. |
(2) 并发标记 (Concurrent Marking) | 在Java应用程序线程运行的同时遍历老年代(tenured generation)的可达对象图。扫描从被标记的对象开始,直到遍历完从root可达的所有对象. 调整器(mutators)在并发阶段的2、3、5阶段执行,在这些阶段中新分配的所有对象(包括被提升的对象)都立刻标记为存活状态. |
(3) 再次标记(Remark) | (Stop the World Event, 所有应用线程暂停) 查找在并发标记阶段漏过的对象,这些对象是在并发收集器完成对象跟踪之后由应用线程更新的. |
(4) 并发清理(Concurrent Sweep) | 回收在标记阶段(marking phases)确定为不可及的对象. 死对象的回收将此对象占用的空间增加到一个空闲列表(free list),供以后的分配使用。死对象的合并可能在此时发生. 请注意,存活的对象并没有被移动. |
(5) 重置(Resetting) | 清理数据结构,为下一个并发收集做准备. |
CMS的GC步骤
1. CMS的堆内存结构(Heap Structure)
堆内存被分为3个空间.
年轻代(Young generation)分为 1个新生代空间(Eden)和2个存活区(survivor spaces). 老年代(Old generation)是一大块连续的空间, 垃圾回收(Object collection)就地解决(is done in place), 除了 Full GC, 否则不会进行压缩(compaction).
2. CMS年轻代(Young) GC 的工作方式
年轻代(young generation)用高亮的绿色表示, 老年代(old generation)用蓝色表示。如果程序运行了一段时间,那么 CMS 看起来就像下图这个样子. 对象散落在老年代中的各处地方.
在使用 CMS 时, 老年代的对象回收就地进行(deallocated in place). 他们不会被移动到其他地方. 除了 Full GC, 否则内存空间不会进行压缩.
3. 年轻代垃圾回收(Young Generation Collection)
Eden区和survivor区中的存活对象被拷贝到另一个空的survivor 区. 存活时间更长,达到阀值的对象会被提升到老年代(promoted to old generation).
4. 年轻代(Young) GC 之后
年轻代(Young)进行一次垃圾回收之后, Eden 区被清理干净(cleared),两个 survivor 区中的一个也被清理干净了. 如下图所示:
图中新提升的对象用深蓝色来标识. 绿色的部分是年轻代中存活的对象,但还没被提升到老年代中.
5. CMS的老年代回收(Old Generation Collection)
两次stop the world事件发生在: 初始标记(initial mark)以及重新标记(remark)阶段. 当老年代达到一定的占有率时,CMS垃圾回收器就开始工作.
(1) 初始标记(Initial mark)阶段的停顿时间很短,在此阶段存活的(live,reachable,可及的) 对象被记下来. (2) 并发标记(Concurrent marking)在程序继续运行的同时找出存活的对象. 最后, 在第(3)阶段(remark phase), 查找在第(2)阶段(concurrent marking)中错过的对象.
6. 老年代回收 - 并发清理(Concurrent Sweep)
在前面阶段未被标记的对象将会就地释放(deallocated in place). 此处没有压缩(compaction).
备注: 未标记(Unmarked)的对象 == 已死对象(Dead Objects)
7. 老年代回收 - 清理之后(After Sweeping)
在第(4)步(Sweeping phase)之后, 可以看到很多内存被释放了. 还应该注意到,这里并没有执行内存压缩整理(no compaction).
最后, CMS 收集器进入(move through)第(5)阶段, 重置(resetting phase), 然后等候下一次的GC阀值到来(GC threshold).
你了解G1 GC吗 ?
什么是G1 GC ?
G1垃圾收集器是在Java 7后才可以使用的特性,它的长远目标是代替CMS收集器。G1收集器是一个并行的、并发的和增量式压缩短暂停顿的垃圾收集器。G1收集器和其他的收集器运行方式不一样,不区分年轻代和年老代空间。它把堆空间划分为多个大小相等的区域(Region)。例如, 刚开始的时候Region A分配给了年轻代, 这个年轻代被回收结束后, 这个Region又被放回了空闲队列中, 可能下一次就会被分配给老年代. 当进行垃圾收集时,它会优先收集存活对象较少的区域,因此叫“Garbage First”。
JVM参数 -XX:+UseG1GC
G1 GC的收集过程涵盖4个阶段: 年轻代GC, 并发标记周期, 混合收集, Full GC
年轻代GC
1. 当Eden区间分配内存失败(满了)的时候, 一次年轻代回收就被触发了. 这次触发对于GC来说, 他的工作是释放一些内存, 属于一次轻量级的回收操作.
2. 当一个年轻代收集进行时, 整个年轻代会被回收, 所有的应用程序线程会被中断, G1 GC会启用多线程进行年轻代的回收.
3. 一次年轻代回收, 会让存活的对象提升一次年龄. 当这个年龄到达一定的阈值, 就会被提升到老年区.
4. 每一次年轻代回收暂停期间, G1 GC重新调整年轻代的大小.
5. JVM会将每个对象的年龄信息、各个年龄段对象的总大小记录在“age table”表中。基于“age table”、survivor区大小、survivor区目标使用率(-XX:TargetSurvivorRatio, 默认50%
)、晋升年龄阈值(-XX:MaxTenuringThreshold, 默认15
),JVM会动态的计算tenuring threshold的值。一旦对象年龄达到了tenuring threshold就会晋升到老年代。
6. 为什么要动态的计算tenuring threshold的值呢?假设有很多年龄还未达到TenuringThreshold的对象依旧停留在survivor区,这样不利于新对象从eden晋升到survivor。因此设置survivor区的目标使用率,当使用率达到时重新调整TenuringThreshold值,让对象尽早的去old区。
7. 如果希望跟踪每次新生代GC后,survivor区中对象的年龄分布,可在启动参数上增加-XX:+PrintTenuringDistribution
。
混合回收
1. 不仅仅对年轻代进行回收, 还需要进行一些老年代的回收.
2. -XX:InitiatingHeapOccupancyPercent (简称IHOP,默认值45), 当堆得占有率达到IHOP阈值时, 初始化一个并行标记循环, 并启动.
3. 并行标记循环包括以下几个阶段:
3. 在清洗阶段, G1 GC根据每个老年代区间的性能进行打分, 然后开始一次混合回收. 不仅回收年轻代中的所有垃圾, 还回收垃圾较多的老年代区间.
成功完成并发标记周期后,G1 GC 从执行年轻代垃圾回收切换为执行混合垃圾回收。在混合垃圾回收期间,G1 GC 可以将一些旧的区域添加到 eden 和存活区供将来回收。所添加旧区域的确切数量由一系列标志控制。G1 GC 回收了足够的旧区域后(经过多次混合垃圾回收),G1 将恢复执行年轻代垃圾回收,直到下一个标记周期完成。
G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。
Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
由上面的描述可知,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。
上文中,多次提到了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。
Young GC发生的时机大家都知道,那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。
G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。
除了以上的参数,G1 GC相关的其他主要的参数有:
参数 | 含义 |
---|---|
-XX:G1HeapRegionSize=n | 设置Region大小,并非最终值 |
-XX:MaxGCPauseMillis | 设置G1收集过程目标时间,默认值200ms,不是硬性条件 |
-XX:G1NewSizePercent | 新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
-XX:ParallelGCThreads | STW期间,并行GC线程数 |
-XX:ConcGCThreads=n | 并发标记阶段,并行执行的线程数 |
-XX:InitiatingHeapOccupancyPercent | 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous |
标记周期的各个阶段
初始标记阶段:在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描阶段:G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记阶段:G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
重新标记阶段:该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清理阶段:在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
三色标记算法
提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。
黑色:根对象,或者该对象与它的子对象都被扫描
灰色:对象本身被扫描,但还没扫描完该对象中的子对象
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
当GC开始扫描对象时,按照如下图步骤进行对象的扫描:
根对象被置为黑色,子对象被置为灰色。
继续由灰色遍历,将已扫描了子对象的对象置为黑色。
遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。
这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题
我们看下面一种情况,当垃圾收集器扫描到下面情况时:
这时候应用程序执行了以下操作:
A.c=C
B.c=null
这样,对象的状态图变成如下情形:
这时候垃圾收集器再标记扫描的时候就会下图成这样:
很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:
- 在插入的时候记录对象
- 在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
1,在开始标记的时候生成一个快照图标记存活对象
2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
3,可能存在游离的垃圾,将在下次被收集
这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:
混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。
1. G1的堆内存结构
堆内存被划分为固定大小的多个区域.
每个heap区(Region)的大小在JVM启动时就确定了. JVM 通常生成 2000 个左右的heap区, 根据堆内存的总大小,区的size范围允许为 1Mb 到 32Mb.
2. G1 堆空间分配
实际上,这些区域(regions)被映射为逻辑上的 Eden, Survivor, 和 old generation(老年代)空间.
图中的颜色标识了每一个区域属于哪个角色. 存活的对象从一块区域转移(复制或移动)到另一块区域。设计成 heap 区的目的是为了并行地进行垃圾回收(的同时停止/或不停止其他应用程序线程).
如图所示,heap区可以分配为 Eden, Survivor, 或 old generation(老年代)区. 此外,还有第四种类型的对象被称为 区域(Humongous regions),这种巨无霸区是设计了用来保存比标准块(standard region)大50%及以上的对象, 它们存储在一组连续的区中. 最后一个类型是堆内存中的未使用区(unused areas).
备注: 截止英文原文发表时,巨无霸对象的回收还没有得到优化. 因此,您应该尽量避免创建太大(大于32MB?)的对象.
3. G1中的年轻代(Young Generation)
堆被分为大约2000个区. 最小size为1 Mb, 最大size为 32Mb. 蓝色的区保存老年代对象,绿色区域保存年轻代对象.
注意G1中各代的heap区不像老一代垃圾收集器一样要求各部分是连续的.
4. G1中的一次年轻代GC
存活的对象被转移(copied or moved)到一个/或多个存活区(survivor regions). 如果存活时间达到阀值,这部分对象就会被提升到老年代(promoted to old generation regions).
此时会有一次 stop the world(STW)暂停. 会计算出 Eden大小和 survivor 大小,给下一次年轻代GC使用. 清单统计信息(Accounting)保存了用来辅助计算size. 诸如暂停时间目标之类的东西也会纳入考虑.
这种方法使得调整各代区域的尺寸很容易, 让其更大或更小一些以满足需要.
5. G1的一次年轻代GC完成后
存活对象被转移到存活区(survivor regions) 或 老年代(old generation regions).
刚刚被提升上来的对象用深绿色显示. Survivor 区用绿色表示.
总结起来,G1的年轻代收集归纳如下:
- 堆一整块内存空间,被分为多个heap区(regions).
- 年轻代内存由一组不连续的heap区组成. 这使得在需要时很容易进行容量调整.
- 年轻代的垃圾收集,或者叫 young GCs, 会有 stop the world 事件. 在操作时所有的应用程序线程都会被暂停(stopped).
- 年轻代 GC 通过多线程并行进行.
- 存活的对象被拷贝到新的 survivor 区或者老年代.
Old Generation Collection with G1
和 CMS 收集器相似, G1 收集器也被设计为用来对老年代的对象进行低延迟(low pause)的垃圾收集. 下表描述了G1收集器在老年代进行垃圾回收的各个阶段.
G1 收集阶段 - 并发标记周期阶段(Concurrent Marking Cycle Phases)
G1 收集器在老年代堆内存中执行下面的这些阶段. 注意有些阶段也是年轻代垃圾收集的一部分.
阶段 | 说明 |
---|---|
(1) 初始标记(Initial Mark) | (Stop the World Event,所有应用线程暂停) 此时会有一次 stop the world(STW)暂停事件. 在G1中, 这附加在(piggybacked on)一次正常的年轻代GC. 标记可能有引用指向老年代对象的survivor区(根regions). |
(2) 扫描根区域(Root Region Scanning) | 扫描 survivor 区中引用到老年代的引用. 这个阶段应用程序的线程会继续运行. 在年轻代GC可能发生之前此阶段必须完成. |
(3) 并发标记(Concurrent Marking) | 在整个堆中查找活着的对象. 此阶段应用程序的线程正在运行. 此阶段可以被年轻代GC打断(interrupted). |
(4) 再次标记(Remark) | (Stop the World Event,所有应用线程暂停) 完成堆内存中存活对象的标记. 使用一个叫做 snapshot-at-the-beginning(SATB, 起始快照)的算法, 该算法比CMS所使用的算法要快速的多. |
(5) 清理(Cleanup) | (Stop the World Event,所有应用线程暂停,并发执行) 在存活对象和完全空闲的区域上执行统计(accounting). (Stop the world) 擦写 Remembered Sets. (Stop the world) 重置空heap区并将他们返还给空闲列表(free list). (Concurrent, 并发) |
(*) 拷贝(Copying) | (Stop the World Event,所有应用线程暂停) 产生STW事件来转移或拷贝存活的对象到新的未使用的heap区(new unused regions). 只在年轻代发生时日志会记录为 `[GC pause (young)]`. 如果在年轻代和老年代一起执行则会被日志记录为 `[GC Pause (mixed)]`. |
G1老年代收集步骤
顺着定义的阶段,让我们看看G1收集器如何处理老年代(old generation).
6. 初始标记阶段(Initial Marking Phase)
存活对象的初始标记被固定在年轻代垃圾收集里面. 在日志中被记为 GC pause (young)(inital-mark)
。
7. 并发标记阶段(Concurrent Marking Phase)
如果找到空的区域(如用红叉“X”标示的区域), 则会在 Remark 阶段立即移除. 当然,"清单(accounting)"信息决定了活跃度(liveness)的计算.
8. 再次标记阶段(Remark Phase)
空的区域被移除并回收。现在计算所有区域的活跃度(Region liveness).
9. 拷贝/清理阶段(Copying/Cleanup)
G1选择“活跃度(liveness)”最低的区域, 这些区域可以最快的完成回收. 然后这些区域和年轻代GC在同时被垃圾收集 . 在日志被标识为 [GC pause (mixed)]
. 所以年轻代和老年代都在同一时间被垃圾收集.
10.拷贝/清理之后(After Copying/Cleanup)
所选择的区域被收集和压缩到下图所示的深蓝色区域和深绿色区域.
老年代GC(Old Generation GC)总结
总结下来,G1对老年代的GC有如下几个关键点:
- 并发标记清理阶段(Concurrent Marking Phase)
- 活跃度信息在程序运行的时候被并行计算出来
- 活跃度(liveness)信息标识出哪些区域在转移暂停期间最适合回收.
- 不像CMS一样有清理阶段(sweeping phase).
- 再次标记阶段(Remark Phase)
- 使用的 Snapshot-at-the-Beginning (SATB, 开始快照) 算法比起 CMS所用的算法要快得多.
- 完全空的区域直接被回收.
- 拷贝/清理阶段(Copying/Cleanup Phase)
- 年轻代与老年代同时进行回收.
- 老年代的选择基于其活跃度(liveness).
参考资料:
<<深入理解JVM&G1 GC>>
https://tech.meituan.com/g1.html
http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
https://www.ps.uni-saarland.de/courses/gc-ws01/slides/generational_gc.pdf
http://www.oracle.com/technetwork/cn/articles/java/g1gc-1984535-zhs.html
https://liuzhengyang.github.io/2017/06/07/garbage-first-collector/
http://blog.jobbole.com/109170/
https://github.com/cncounter/translation/blob/master/tiemao_2014/G1/G1.md
https://segmentfault.com/a/1190000007795862
http://moguhu.com/article/detail?articleId=56
https://www.javadoop.com/post/g1
https://blog.csdn.net/zero__007/article/details/52797684