一、语境中的并行与并发
并行
并行描述的时多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。
并发
并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。
二、串行收集器
- 单线程: Serial 收集器(年轻代) + Serial Old 收集器(老年代)
- 客户端模式的最好选择:适合个人电脑(适合CPU核数少的,多了也没用)、堆内存较小
三、吞吐量优先收集器
- 多线程并行:Parallel Scavenge 收集器(年轻代) + Parallel Old 收集器(老年代)。jdk8默认使用
- 吞吐量 = 运行用户代码时间 /(运行用户代码时间+运行垃圾收集时间【次数*单次时间,次数越多,时间越长】)
- 适合堆内存较大,多核 cpu
- 目标:尽可能让 单次GC的时间 最短。即单位时间内,STW的时间最短
四、响应时间优先收集器
- 多线程并发:ParNew 收集器(年轻代) + CMS(Concurrent Mark Sweep)收集器(老年代)
- 适合堆内存较大,多核 cpu
- 目标:尽可能让 单次STW的时间 最短
- 问题:
- “标记清除算法”:导致空间碎片化,无法存储大对象,进而导致”并发失败“。临时启用 SerialOld 收集器进行标记整理
- “并发标记、并发清理”:产生 “浮动垃圾”,需要预留空间。
- “重新标记”:先做新生代回收再重新扫描,减轻压力
五、G1收集器
JDK9 开始成为服务端模式下的默认垃圾收集器,CMS被声明为不推荐使用
-
定义:Garbage First ,jdk9默认使用。
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
-
适用场景
- 用于老年代
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是 标记+整理 算法,两个区域之间是 复制 算法。意味着G1运行期间不会产生内存空间碎片
-
相关 JVM 参数
- -XX:+UseG1GC :使用G1收集器
- -XX:G1HeapRegionSize=size:设置堆内存每个区域大小
- -XX:MaxGCPauseMillis=time:设置回收时最长停止时间
1)回收阶段
2)Young Collection
- 将新对象分配到各个伊甸园区域
- 当堆中伊甸园区域被逐渐被占满后,触发 Young Collection 并 STW 和 进行初始标记。将伊甸园区域幸存的对象使用复制的算法拷贝到幸存区域
- 当堆中伊甸园区域被逐渐被占满、幸存区域中对象超过阈值后,触发 Young Collection 并 STW 和 进行初始标记。将伊甸园区域幸存的对象使用复制的算法拷贝到幸存区域,将幸存区域中没有超过阈值的对象拷贝到另一个幸存区域,将幸存区域中超过阈值的对象晋升到老年代区域
3)Young Collection + 并发标记
-
Young Collection:在 Young GC 时会进行 GC Root 的初始标记
-
并发标记:老年代区域占用堆空间比例达到阈值时,进行 并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
4)Mixed Collection
会对 E、S、O 进行全面垃圾回收
-
最终标记(Remark)会 STW
-
拷贝存活(Evacuation)会 STW:会选择回收价值最高(获得的空间大小、回收所需时间)的区域进行回收,以满足暂停时间的设置
-XX:MaxGCPauseMillis=ms
5)写屏障
在虚拟机层面对 “引用类型字段赋值” 这个动作的AOP切面。也就是说在赋值的前后增加相应代码执行额外的动作
- 写后屏障:解决跨代引用问题,维护记忆集
- 写前屏障:解决并发标记阶段的引用变更问题,并发扫描时的原始快照
6)跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
-
记忆集(Remembered Set) 与 卡表(记忆集的实现)
-
在引用变更时,更新脏卡:通过 post-write barrier**(写后屏障)** + dirty card queue
- 异步更新:concurrent refinement threads 更新 Remembered Set
7)重新标记-Remark
pre-write barrier (写前屏障) + satb_mark_queue
- 删除引用(原始快照):白色的C对象 被 灰色的B对象 所引用,但在处理前被用户线程删除了灰色B对象的引用。
- 异步更新:此时C对象 删除引用操作前的写前屏障将C对象加入到一个队列中,并将其标记为灰色
- 插入引用:用户线程让 黑色的A对象 引用 白色的C对象。
- 重新标记阶段:STW,将队列中的对象取出并进行重新检测
8)DK8u20字符串去重
- -XX:+UseStringDeduplication
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 将所有新分配的字符串放入一个队列。当新生代回收时,G1并发检查是否有字符串重复,如果它们值一样,让它们引用同一个 char[]
- 注意,与 String.intern() 不一样
- String.intern() 关注的是字符串对象
- 而字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串表
9)DK8u40并发标记类卸载
-
-XX:+ClassUnloadingWithConcurrentMark: 默认启用
-
所有对象都经过并发标记后,就能知道哪些类不再被使用
-
卸载条件:当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
10)DK8u60回收巨型对象
- 一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝,回收时被优先考虑
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
11)DK9并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9 前:需要使用 -XX:InitiatingHeapOccupancyPercent
- JDK 9 时:可以动态调整
- -XX:InitiatingHeapOccupancyPercent 用来设置初始值
- 进行数据采样并动态调整,总会添加一个安全的空档空间
12)DK9更高效的回收
- 250+增强
- 180+bug修复
- https://docs.oracle.com/en/java/javase/12/gctuning
六、💡GC总结
-
SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - major gc
- 并发清除失败导致老年代内存不足 - full gc
-
G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存达到一定比例 - mixed gc
- 混合收集失败导致老年代内存不足 - full gc(多线程)