垃圾回收器
新生代回收器
-
Serial
-
ParNew
-
parallel
老年代回收器
-
Serial Old
-
CMS
-
Parallel Old
新生代和老年代回收器
-
G1
Serial
特点
Serial收集器是最基本、发展历史最悠久的收集器。JDK1.3.1前是HotSpot新生代收集的唯一选择。
运行示意图
有如下特点:
-
针对新生代;
-
采用复制算法;
-
单线程收集;
-
进行垃圾收集时,必须暂停所有工作线程,直到完成;
优势:
简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
劣势:
会在用户不知道的情况下停止所有工作线程。
使用场景
-
Client 模式(桌面应用)
在用户的桌面应用场景中,可用内存一般不大,可以在较短时间内完成垃圾收集,只要不频繁发生,这是可以接受的
-
单核服务器
对于限定单个CPU的环境来说,Serial收集器没有线程切换开销,可以获得最高的单线程收集效率
参数设置
-
-XX:+UseSerialGC
:添加该参数来显式的使用串行垃圾收集器
ParNew
特点
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余均和Serial 收集器一致。
运行示意图
优势:
多线程版本的Serial,可以更加有效的利用系统资源
劣势:
同Serial,会在用户不知道的情况下停止所有工作线程
使用场景
Server模式下使用,亮点是除Serial外,目前只有它能与CMS收集器配合工作,是一个非常重要的垃圾回收器。
参数设置
-
-XX:+UseConcMarkSweepGC
:指定使用CMS后,会默认使用ParNew作为新生代收集器; -
-XX:+UseParNewGC
:强制指定使用ParNew; -
-XX:ParallelGCThreads
:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
parallel
特点
Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。与ParNew的不同之处在于Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。
运行示意图
有如下特点:
-
新生代收集器;
-
采用复制算法;
-
多线程收集;
-
关注点与其他收集器不同:
-
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;
-
而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量;
-
优势:
追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。
劣势:
应该说是特点,追求高吞吐量必然要牺牲一些其他方面的优势,不能做到既,又。ParNew收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间,原本10s收集一次, 每次停顿100ms, 设置完参数之后可能变成5s收集一次, 每次停顿70ms. 停顿时间变短, 但收集次数变多。
使用场景
根据相关特性,我们很容易想到它的使用场景,即:当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,程序主要在后台进行计算,而不需要与用户进行太多交互等就特别适合ParNew收集器。
-
例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序等
参数设置
-
-XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,大于0的毫秒数;
-
-XX:GCTimeRatio:设置垃圾收集时间占总时间的比率,0<n<100的整数;
Serial Old
特点
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。
有如下特点:
-
针对老年代;
-
采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
-
单线程收集;
优劣势基本和Serial无异,它是和Serial收集器配合使用的老年代收集器。
使用场景
-
Client模式;
-
单核服务器;
-
与Parallel Scavenge收集器搭配;
-
作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用
CMS
特点
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤:
-
初始标记,标记GC Roots 能够直接关联到达对象
-
并发标记,进行GC Roots Tracing 的过程
-
重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
-
并发清除,用标记清除算法清除对象。
运行示意图
有如下特点:
-
针对老年代;
-
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
-
以获取最短回收停顿时间为目标;
-
并发收集、低停顿;
-
需要更多的内存(看后面的缺点);
优势:
-
停顿时间短;
-
吞吐量大;
-
并发收集
劣势:
-
对CPU资源非常敏感
-
无法收集浮动垃圾
-
容易产生大量内存碎片
使用场景
-
与用户交互较多的场景;
-
希望系统停顿时间最短,注重服务的响应速度;
-
以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用。
参数设置
-
-XX:+UseConcMarkSweepGC
:指定使用CMS收集器
Parallel Old
特点
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,可以充分利用多核CPU的计算能力。
有如下特点:
-
针对老年代;
-
采用"标记-整理"算法;
-
多线程收集;
优劣势参考Parallel Scavenge收集器。
使用场景
-
JDK1.6及之后用来代替老年代的Serial Old收集器;
-
特别是在Server模式,多CPU的情况下;
这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge(新生代)加Parallel Old(老年代)收集器的"给力"应用组合;
参数设置
-
-XX:+UseParallelOldGC
:指定使用Parallel Old收集器
G1
特点
G1(Garbage-First)是JDK7-u4才推出商用的收集器
-
并行与并发:G1能充分利用多CPU,多核环境下的硬件优势。
-
分代收集:能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
-
空间整合:G1从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的,因此G1运行期间不会产生空间碎片。
-
可预测的停顿:G1能建立可预测的时间停顿模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
运行示意图
有如下特点:
-
并行与并发
-
分代收集,收集范围包括新生代和老年代
-
结合多种垃圾收集算法,空间整合,不产生碎片
-
可预测的停顿:低停顿的同时实现高吞吐量
-
面向服务端应用,将来替换CMS
优势:
-
能充分利用多CPU、多核环境下的硬件优势;
-
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;
-
不会产生内存碎片,有利于长时间运行;
-
除了追求低停顿处,还能建立可预测的停顿时间模型;
G1收集器是当今收集器技术发展的最前沿成果。
劣势:
G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
按照《深入理解Java虚拟机》作者的说法,CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。
所以,尽管是最前沿的成果,也不是完美无缺的。
使用场景
个人以为G1已经基本全面压制cms、parallel等回收器,缺点见上面的劣势。但如果不是追求极致的性能,基本可以无脑G1
参数设置
-
-XX:+UseG1GC
:指定使用G1收集器; -
-XX:InitiatingHeapOccupancyPercent
:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45; -
-XX:MaxGCPauseMillis
:为G1设置暂停时间目标,默认值为200毫秒; -
-XX:G1HeapRegionSize
:设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
文章来源:https://mp.weixin.qq.com/s/3KOvlT05xzj8drI7kVrL_A
在垃圾回收过程中如何判断一个对象是否是垃圾,有两种算法。一种是引用记数法,一种是可达性分析法。
引用记数法是早期垃圾回收器中使用的算法,每一个对象维护一个该对象被引用的记数,每引用一次,记数加1,每减少引用1次,引用减1,当引用为0时,表示该对象不再被引用,可以作为垃圾被清除。但是引用记数法有一个最致命的问题,就是无法解决循环引用的问题。
可达性分析法,是通过从GCRoots出发,找出内存中的引用链,那么链中的对象表示可达,即不能作为被垃圾回收的。引用链之外的对象即可作为垃圾回收。Java中使用的是可达性分析法。
所以在可达性分析法中,判断哪些引用是GCRoots是垃圾回收的起点,那么这篇文章,就说说哪些引用是GCRoots。
GCRoots
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的native方法)中引用的对象
三色标记法
我们把遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
- 白色:尚未访问过。
- 黑色:本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
- 灰色:本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
当Stop The World (以下简称 STW)时,对象间的引用 是不会发生变化的,可以轻松完成标记。
而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
多标(浮动垃圾)
假设已经遍历到E(变为灰色了),此时应用执行了 objD.fieldE = null
:
此刻之后,对象E/F/G是“应该”被回收的。然而因为E已经变为灰色了,其仍会被当作存活对象继续遍历下去。最终的结果是:这部分对象仍会被标记为存活,即本轮GC不会回收这部分内存。
这部分本应该回收 但是 没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响应用程序的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记开始后的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能会变为垃圾,这也算是浮动垃圾的一部分。
漏标(读写屏障)
var G = objE.fieldG;
objE.fieldG = null; // 灰色E 断开引用 白色G
objD.fieldG = G; // 黑色D 引用 白色G
此时切回GC线程继续跑,因为E已经没有对G的引用了,所以不会将G放到灰色集合;尽管因为D重新引用了G,但因为D已经是黑色了,不会再重新做遍历处理。
最终导致的结果是:G会一直停留在白色集合中,最后被当作垃圾进行清除。这直接影响到了应用程序的正确性,是不可接受的。
漏标只有同时满足以下两个条件时才会发生:
条件一:灰色对象 断开了 白色对象的引用(直接或间接的引用);即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
写屏障 + SATB
当对象E的成员变量的引用发生变化时(objE.fieldG = null;
),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录 原来的引用对象
}
【当原来成员变量的引用发生变化之前,记录下原来的引用对象】
这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB),当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。
比如 当时 D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。
值得一提的是,扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。
SATB破坏了条件一:【灰色对象 断开了 白色对象的引用】,从而保证了不会漏标。
写屏障 + 增量更新
当对象D的成员变量的引用发生变化时(objD.fieldG = G;
),我们可以利用写屏障,将D新的成员变量引用对象G记录下来:
void post_write_barrier(oop* field, oop new_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
remark_set.add(new_value); // 记录新引用的对象
}
}
【当有新引用插入进来时,记录下新的引用对象】
这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
增量更新破坏了条件二:【黑色对象 重新引用了 该白色对象】,从而保证了不会漏标。
读屏障(Load Barrier)
oop oop_field_load(oop* field) {
pre_load_barrier(field); // 读屏障-读取前操作
return *field;
}
读屏障是直接针对第一步:var G = objE.fieldG;
,当读取成员变量时,一律记录下来:
void pre_load_barrier(oop* field, oop old_value) {
if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {
oop old_value = *field;
remark_set.add(old_value); // 记录读取到的对象
}
}
这种做法是保守的,但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:
- CMS:写屏障 + 增量更新
- G1:写屏障 + SATB
- ZGC:读屏障
链接:https://www.jianshu.com/p/12544c0ad5c1