JVM 知识--垃圾收集器
本文最后更新于:1 个月前
基于分代收集理论
⚡新生代收集器 (Minor GC)
⚡Serial
-
标记-复制算法
-
单线程: 不仅仅是只有一条收集线程,更是进行垃圾收集时必须暂停其他工作线程的工作
-
有 “STOP THE WORLD”,降低用户体验
-
优点是: 简单高效,仍然是 HotSpot 虚拟机客户端模式下的默认新生代收集器
因为它是所有收集器里额外内存消耗最小的,适用于内存资源受限的场景
由于没用线程交互的开销,可以获得最高的单线程收集效率
-
推荐搭配 Serial Old 使用
⚡ParNew
-
标记-复制算法
-
ParNew 实质上是 Serial 收集器的多线程版本
-
ParNew 是 HotSpot 虚拟机服务端模式下的默认新生代收集器
-
ParNew 在单线程的环境下绝对不会有比 Serial 收集器更好的效果
-
ParNew 最先退出历史舞台,只能和 CMS 一起使用
⚡Parallel Scavenge
-
标记-复制算法
-
多线程,能够并行收集
-
特点是保证一个可控的吞吐量[1],而 CMS 保证的是一个缩短用户线程的停顿时间
-
也被成为"吞吐量优先收集器"
⚡老年代收集器 (Major GC)
⚡Serial Old
-
标记-整理算法
-
单线程
-
默认是 HotSpot 虚拟机客户端模式下默认的老年代收集器
-
如果在服务端下,有两种用途:
-
JDK 5 以前,与 Parallel Scavenge 搭配使用
-
另一种就是作为 CMS 收集器发生失败时的后备预案,在并发收集时发生 Concurrent Mode Failure 时使用
-
⚡Parallel Old
-
Parallel Scavenge 的老年代版本
-
标记-整理算法
-
多线程
⚡CMS (Concurrent Mark Sweep)
-
Old GC, CMS 独有
-
标记-清除算法
-
以获取最短回收停顿时间为目标
-
运作过程更复杂
-
初始标记
仅仅标记 GC Roots 能够关联到的对象,速度快
-
并发标记
从 GC Roots 的直接关联对象开始遍历整个对象图,耗时长但是不用停顿用户线程
-
重新标记
修正并发标记期间,因用户线程继续运作而导致标记变动的一部分对象的标记记录,停顿时间比初始标记时间稍长
-
并发清除
清理删除掉已经标记死亡的对象
初始标记和重新标记两个过程仍然需要 “Stop The World”
-
-
缺点:
-
面向并发的程序对处理器资源比较敏感
虽然并发阶段不会导致用户线程停顿,但是由于占用了一部分资源,会导致应用程序变慢,降低总吞吐量
-
无法处理"浮动垃圾"
浮动垃圾是由于在并发清除阶段,用户线程继续产生的垃圾对象,而此次垃圾收集不能回收他们,只能等到下一次再清除
而且需要留足够的内存空间提供给用户线程使用,因此 CMS 收集器不能像其他收集器那样,等老年代几乎完全满了再进行收集
如果预留内存无法满足程序分配新对象的需要,有可能出现 “Concurrent Mode Failure” 失败,此时虚拟机就会启动预备方案,临时启用 Serial Old 收集器执行 GC,但这样停顿时间就很长了
-
标记-清除产生的大量空间碎片
无法找到足够大的连续空间分配大对象,而不得不产生另一次完全 “Stop the World” 的 Full GC
-
⚡其他
⚡G1 (Garbage First)
-
Mixed GC
-
被 Oracle 称为"全功能的垃圾收集器"
-
不再分代收集,而是面向整个堆的任何部分组成回收集 Set 进行回收,同时也是面向局部收集
注意,收集范围还是整个堆,但将堆划分为每一个 Region 看做是一个局部,每个 Region 代表不同的代,具体是哪个代是动态的,而不是像以前那样的要么是新生代,要么是老年代,要么是整个堆
即以前都是什么区域决定什么对象,现在是对象决定区域
-
是否回收衡量标准不是代数,而是哪块内存的多少,是回收的经验时间,即回收的收益大小
通过维护这样一个优先级列表,每次回收价值更大的对象
-
每一个 Region 可以扮演, Eden, Survivor, Major 或者 Humongous 区域
-
运作过程
-
初始标记
仅仅标记 GC Roots 能直接关联的对象,暂停用户线程,但耗时较短
-
并发标记
唯一不用暂停用户线程的阶段
从 GC Roots 开始对堆中的对象进行可达性分析,递归扫描整个对象的对象图
-
最终标记
短暂暂停用户线程,用于处理并发标记时有引用变动的对象
-
筛选回收
更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序
根据用户期望的停顿时间来制定回收计划,可以把一部分 Region 的存活对象复制到空的 Region 当中,再清理旧 Region
并且,由于是按照 Region 清理,G1 不会产生内存碎片!
-
-
里程碑:通过追求能够应付应用的内存分配速率
不追求一次把整个 Java 堆全部清理干净,而只要保证收集的速度能跟得上对象分配的速度就能工作的很完美
-
特点总结:
-
不分代
-
建立可预测的时间停顿模型,可以指定最大停顿时间
-
分 Region 的布局,不会产生空间碎片
-
按照收益回收的红利
-
-
缺点:
-
内存占用大
和 CMS 一样,使用卡表来处理跨区指针,但是比 CMS 更加复杂,占用堆的20%空间甚至更多
-
执行负载
G1 除了使用写屏障来更新卡表之外,还为了实现原始快照搜索(STAB)算法,用写屏障来跟踪并发时的指针变化情况,导致用户线程在执行阶段有额外的负担
-
⚡低延迟垃圾收集器
几乎整个工作过程都是并发的,只有在初始标记,最终标记阶段有短暂的停顿,且停顿时间基本固定
⚡Shenandoah
-
和 G1 有很多相似的地方
内存布局 初始标记,并发标记的思路 …
-
G1 就是由于合并了 Shenandoah 的代码才能获得多线程 Full GC 的支持
G1 修改的代码也会反映到 Shenandoah 上面
-
相比 G1 的改进
-
G1 的筛选回收阶段是多线程并行但不是并发的,而 Shenandoah 可以
-
默认不使用分代收集
-
G1 中的记忆集变成了连接矩阵的全局数据结构 —— 二维表,减少维护消耗
-
-
运作过程
-
初始标记
-
并发标记
-
最终标记
-
并发清理
清理那些整个 Region 连一个存活对象都没有的
-
并发回收 (核心)
先把回收集里面的存活对象复制到未被使用的 Region
但是这个复制对象的过程是在并发时完成的, 就可能会出现一边复制,一边读的问题
而它是通过读屏障和被称为 Brooks Pointers 的转发指针来解决的
-
初始引用更新
把所有指向旧对象的引用修正到复制到的新地址
只是建议一个线程集合点,会产生短暂的暂停
-
并发引用更新
真正开始引用更新,不再需要沿着对象图进行搜索,只需要按照内存物理地址的顺序,顺序搜索出引用类型
-
最终引用更新
修正 GC Roots 中的引用
-
并发清理
经过并发回收和引用更新之后,整个回收集中所有的 Region 已再无存活对象, 最后再调用一次并发清理过程回收 Region 的空间
-
关于 Brooks Pointers
其实原来的解决方案是设置自陷陷阱,主动触发异常,再进入预设好的异常处理器中,再由代码逻辑吧访问转发到复制后的对象上.但是如果没有操作系统的支持,会导致用户态频繁转到内核态,代价很大
而在原有对象布局结构的最前面统一增加一个新的引用字段,在不处于并发移动的i情况下,该引用指向自己
这样,当对象有了一份新的副本时,只需要就对象指针的值,指向新对象.只要就对象的内存依然存在,虚拟机内存中所有通过旧引用地址访问代码便仍然可用,都会被转发到新对象上继续工作
-
但是请注意,这个设计导致了必然会出现多线程竞争问题
例如当复制对象完成后,用户修改了旧对象,收集器线程才刚刚更新指针
所以,对指针转发的操作必须采用同步措施,在统一时间内只能让用户线程或者收集器线程其中之一访问
-
还有一点是执行频率问题
在面向对象编程的语言中,对象访问是非常重要的,非常频繁的,而 Shenandoah 同时设置读,写屏障,带来了数量庞大的性能开销
-
-
⚡ZGC
-
染色指针技术,将指针的高四位提取出来存储四个标志信息
-
运作过程
ZGC 的四个大阶段都是可以并发执行的,仅仅两个阶段中间会有短暂的停顿用户线程
-
并发标记
和 G1, Shenandoah 类似,只不过标记是在指针上而不是在对象上进行的
-
并发预备重分配
根据特定的查询条件统计要清理的 Region, 将这些 Region 重新组成分配集,准备复制到其他的 Region 中
-
并发重分配 (核心)
把重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个转发表,记录从旧对象到新对象的转发关系
如果用户线程此时访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据 Region 上的转发表记录,将访问转发到新复制的对象,这种行为成为指针的 “自愈”
相比 Shenandoah 的转发指针,每次都会有转发的固定开销,ZGC 运行时的负载比 Shenandoah 要低一些
-
并发重映射
修正整个堆中只想重分配集中旧对象的所有引用,类似 Shenandoah 的并发引用更新
但是 ZGC 这个任务并不是那么迫切的,因为即使是旧引用,也是可以自愈的,也不过是一次转发和修正
因此,ZGC 把这个阶段的工作合并到了下一次垃圾收集的并发标记阶段,节省了一次遍历所有对象图的开销
-
-
优点
由于 ZGC 完全没有记忆集,甚至没有分代,所以给用户线程的运行负担小很多
-
缺点
-
ZGC 对整个堆回收的速度较慢,但是由于对象的分配速率很高,并且很多朝生夕灭,但是在此时又被当作是存活对象,没有标记回收,导致产生了很多浮动垃圾
因为较长的回收时间,和高速产生的对象,并且这个过程如果持续维持的话,堆中能够用于腾挪的空间会越来越小
目前解决办法是尽可能的增加堆的大小,但是要本质上解决,还是必须引入分代收集理论,将对象都在一个专门的区域创建,然后专门对这个区域进行更快,更频繁的收集
-
⚡Epsilon
不能够进行垃圾收集
垃圾收集器的范围
注意:
Major GC 有不同语义,要弄清楚到底是指与 Full GC 同样的整个堆和方法区的收集,还是值与 Old GC 类似的仅仅老年代堆的收集
并发和并行
-
并行 (Parallel): 多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认用户线程此时处于等待状态
-
并发 (Concurrent): 描述的是垃圾收集器线程和用户线程之间的关系,说明同一时间垃圾收集器线程和用户线程都在运行,但由于垃圾收集器线程占用了一部分系统资源,应用程序虽然还行响应,但是吞吐量会受到一定影响
吞吐量=(运行用户代码时间)/(运行用户代码+_垃圾收集的时间) ↩︎
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!