前言-概念
一、Stop-the-world
- 它是指 JVM 由于要执行 GC 而停止了应用程序的执行;
- 任何一种GC收集器中都会发生;
- 多数GC 优化都是通过减少 Stop-the-world 的时间来提高程序的性能(高吞吐、低停顿)。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
二、Safe-point(安全点)
程序执行时并非在所有地方都能停顿下来开始 GC,只有在某些特定的位置才可以,这些特定的位置被称为安全点(Safe-point)。在使用 GC roots 分析可达性时,引用关系不会发生改变的点就是安全点。
常用安全点有:
- 方法调用
- 循环跳转
- 异常跳转
三、GC Root的对象有哪些
- 虚拟机栈中引用的对象
虚拟机栈中的引用的对象可以作为GC Root。我们程序在虚拟机的栈中执行,每次函数调用调用都是一次入栈。在栈中包括局部变量表和操作数栈,局部变量表中的变量可能为引用类型(reference),他们引用的对象即可作为GC Root。不过随着函数调用结束出栈,这些引用便会消失
- 方法区中类静态属性引用的对象:简单的说就是我们在类中使用的static声明的引用类型字段
Class Person {
private static Object name;
}
- 方法区中常量引用的对象:简单的说就是我们在类中使用final声明的引用类型字段
Class Person {
private fanal Object name;
}
- 本地方法栈中引用的对象
四、Card Table
主要用于分代模型中帮助垃圾回收
G1由于做YGC时,需要扫描整个Old区,效率非常低,所以JVM设计了Card Table, 如果一个Old区Card Table中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card【以空间换时间】。 在结构上,Card Table用BitMap来实现(如0110010010,1表示Dirty)
五、RSet(Remember Set)
在G1收集器中,记录其他Region中的对象到本Region的引用,使得垃圾收集器不需要扫描整个堆,只需要扫描RSet,就知道当前分区中的对象是否有被引用。
如何判断对象是否死亡
一、引用计数法(JVM一般不采用这种)
每次创建对象的时候添加一个引用计数器,用于计数该对象被多少引用。
缺点:
- 每次对对象赋值时均需要维护引用计数器,且计数器本身也有损耗;
- 较难处理循环引用(如:两个对象之间相互引用)
二、可达性分析算法
通过一系列称为“GC Roots”的对象为起点,从这些节点向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明对象不可用
GC Root:有哪些对象?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象;
- 方法区中的类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中Native方法引用的对象
垃圾回收算法
一、标记-清除算法:
1、分标记和清除两阶段
标记阶段:首先通过根节点,标记所有从根节点开始的可达对象
清除阶段:清除所有未被标记的对象(标记阶段的不可达对象)
2、缺点
- 效率问题:标记和清除的效率都不高
- 空间问题:清除后产生大量不连续的内存碎片,可能会导致后续无法分配大对象而导致再一次触发垃圾收集动作
二、复制算法:一般是对对象存活率较低的一种回收操作
为了解决效率问题,它将内存分为大小相同的两块,每次使用其中的一块。当一块内存使用完后,将还存活的对象复制到另外一块去,然后将使用的空间一次清理掉。这样每次内存回收都是对内存区间的一半进行回收。
缺点:内存浪费,将内存分为大小相同的两块,只使用其中的一块
三、标记-整理算法:对于对象存活率较高的内存区域
标记-整理算法和标记-清除算法差不多,都是一开始对对象进行标记,但后续不是直接对对象清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
缺点:移动对象的成本
四、分代收集算法
根据各个年代的特点选择合适的垃圾收集算法:⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集
HotSpot为什么要使用分代收集算法?主要为了提升GC效率
GC过程
一、MinorGC:复制->清空->互换
- Eden、SurvivorFrom复制到SurvivorTo,年龄+1
当Eden区满的时候会触发第一次GC,将存活的对象拷贝到SurvivorFrom区;当Eden区再次触发GC时会扫描Eden区和From区,对这两个区进行垃圾回收,经过这次垃圾回收后还存活的对象,则直接复制到To区(如果有对象的年龄已到达老年的标准,则复制到老年代),同时将这些对象的年龄+1
-
清空Eden、SurvivorFrom区的对象
-
SurvivorFrom和SurvivorTo区互换【交换 from 和 to指针,以保证下一次 Minor GC时,to 指向的 Survivor区还是空的】
如果某对象在这两个区互换次数达到15次(这个次数由JVM参数决定,默认是15)后还存活,则进入老年代
GC收集器
常见的垃圾收集器以及其配合使用关系如下:

Serial收集器(年轻代、复制算法、-XX:+UseSerialGC):
- 单线程收集,串行的方式执行,必须暂停所有工作线程;
- 简单高效,没有线程交互的开销
ParNew收集器(年轻代、复制算法、-XX:+UseParNewGC)
- Serial收集器的多线程版本(并行GC),多核CPU下有更好的性能
Parallel Scavenge 收集器(年轻代、复制算法、-XX:+UseParallelGC)
- Serial收集器的多线程版本(并行GC,在整个扫描和复制过程采用多线程的方式进行)
- 目标是提高吞吐量(吞吐量 = 运行用户程序的时间 / (运行用户程序的时间 + 垃圾收集的时间))
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务
Serial Old收集器(老年代、标记 - 整理算法、-XX:+UseSerialOldGC)
- Serial收集器的老年代版本
- 单线程收集,串行的方式执行,必须暂停所有工作线程;
- 简单高效,没有线程交互的开销
Parallel Old收集器(老年代、标记-整理算法、-XX:+UseParallelOldGC)
- Parallel Scavenge 收集器的老年代版本
- 并行GC
CMS收集器(老年代、标记-清除算法、-XX:+UseConcMarkSweepGC)
划时代的意义:第一款垃圾回收线程和用户线程可以同时工作
主要有4个步骤:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(Stop-The-World)
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(Stop-the-world)
- 并发清除: 清理垃圾,不需要停顿
补充:
- 并发标记中使用的是“三色标记”算法,但是ZGC使用的是“颜色指针”
优点:并发收集、低停顿
缺点:
- 吞吐量低:虽然在两个并发阶段不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量下降
- 无法处理浮动垃圾:由于CMS并发清除阶段用户线程还在运行,伴随着程序还在产生新的垃圾,这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只能留到下次再清理,这一部分垃圾称为“浮动垃圾”
- 标记 - 清除算法带来的内存空间碎片问题
G1收集器:Oracle的jdk9默认垃圾收集器
-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200,其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可
主要有4个步骤:
- 初始标记:标记出所有与根节点直接关联引用对象,需要STW,耗时短;
- 并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。在此期间所有变化引用关系的对象,都会被记录在Remember Set Logs中
- 最终标记:标记在并发标记期间,新产生的垃圾。需要STW
- 筛选回收:根据用户指定的期望回收时间回收价值较大的对象(看"原理"第二条)。需要STW
原理:
- G1收集器将整个堆划分为多个大小相等的Region
- G1跟踪各个region里面的垃圾堆积的价值(回收后所获得的空间大小以及回收所需时间长短的经验值),在后台维护一张优先列表,每次根据允许的收集时间,优先回收价值最大的region,这种思路:在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存),并回收,做到尽可能的在有限的时间内获取尽可能高的收集效率
- 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的。
- 内存模型:
其中Humongous是大对象:超过单个region的 50% 就是一个大对象,也可跨越多个region
- GC触发时间
(1)YGC:Eden区空间不足、多线程并发自行
(2)FGC:Old空间不足
- 如果G1产生Full GC,如何优化?
(1)扩内存
(2)提高CPU性能
(3)降低Mixed GC触发的阈值,让Mixed GC提前发生
Mixed GC:当堆内存超过45%默认值,就会触发,回收时部分新生代和老年代,region满了就回收
-
Mixed GC过程:和CMS比较像,MixedGC最后是筛选回收,多了个筛选步骤。筛选就是找出垃圾最多的region。筛选后将存活对象复制到其他region,再将之前的region清空
-
优点
(1)并行与并发:G1能够重发利用多CPU、多核环境下的优势,使用多个CPU来缩短Stop-The-World停顿时间。
(2)分代收集:与其他收集器一样,分代概念在G1中依然存在。
(3)空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”来实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能够提供整体的可用内存。
(4)可预测停顿:G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1收集器时,Java堆的内存布局与其他收集器有很大的区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region(不需要连续)的集合。
- 和CMS收集器对比
(1)G1与CMS相比,仅在最后的"筛选回收"部分不同(CMS是并发清除),实际上G1回收器的整个堆内存的划分都与其他收集器不同;
(2)CMS需要配合ParNew,G1可单独回收整个空间
并发标记算法
一、三色标记法
使用白灰黑三种颜色标记对象,白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。
漏标问题:在并发标记过程中,如果B到D之间的引用断开,增加了A到D的引用
产生漏标问题的条件有两个:
- 黑色对象指向了白色对象:创建新对象时产生的漏标问题
- 灰色对象指向白色对象的引用消失:对象引用被修改的漏标问题
所以要解决漏标问题,只需要打破这两个条件之一即可:
- 增量更新:跟踪黑指向白的增加,关注引用的增加,将黑色重新标记为灰色,等下次再扫描一遍。CMS使用该方法。
- SATB(snapshot-at-the-beginning):记录灰指向白的消失,关注引用的删除,当灰——>白消失时,要把这个引用推到GC的堆栈,保证白还能被GC扫描到。G1使用该方法。
(1)在开始标记的时候生成一个快照图标记存活对象
(2)在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
(3)可能存在游离的垃圾,将在下次被收集
为什么G1采用SATB而不是用增量更新?
-
因为使用增量更新将黑色重新标记为灰色后,之前扫描过的还要再扫描一次,效率比较低;
-
G1有RSet和SATB配合。Rset里记录了其他对象指向自己的引用,这样就不用扫描其他区域了,只需要扫描RSet即可,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。