JVM深入浅出(5)--- 垃圾收集器

自己在学习《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) (华章原创精品) - 周志明》时的一些思考和总结

1. 经典垃圾收集器

首先必须上这张图

image-20260213010240013

1.1 Serial收集器

最简单最基础的垃圾收集器,作用于新生代gc时需要暂停所有的用户线程,且是单线程垃圾回收,。

serial单线程收集的特点,注定了它在垃圾回收时没有线程交互的开销,简单高效。

垃圾收集时使用的是标记-复制算法

image-20260213021119849

1.2 ParNew收集器

是serial收集器的多线程版本,只能和CMS搭配工作,没有开启Parnew的jvm指令,当使用CMS垃圾收集器时,新生代收集器默认使用ParNew收集器。

image-20260213023005947

image-20260213021411130

1.3 Parallel Scavenge收集器

Parallel Scavenge收集器和ParNew收集器在很多地方都有相似之处,不同的是Parallel scavenge更关注的是吞吐量。

什么是吞吐量

\[\text { 吞吐量 }=\frac{\text { 运行用户代码时间 }}{\text { 运行用户代码时间 }+ \text { 运行垃圾收集时间 }} \]

良好的响应速度能提升用户体验,而高吞吐量则可以最高效利用处理器资源,尽快完成程序的运算任务。就好比延迟关注的是用户等待垃圾收集中STW的时间,而吞吐量关注的是跑完整个程序花费的时间

Parallel scavenge 控制吞吐量的参数

  • 控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis
  • 直接设置吞吐量大小的-XX:GCTimeRatio参数

除此之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注,开启这个参数后,虚拟机会根据实际运行情况,调节新生代大小,Eden/survivor 比例,晋升老年代对象大小这些参数来提供最合适的停顿时间和吞吐量

1.4 Serial Old收集器

Serial old 是serial收集器在老年代上的版本,使用的是标记-整理算法。这个收集器主要意义也是供客户端模式下的HotSpot虚拟机使用。

使用场景

  • 在JDK5 之前和parallel scavenge搭配,

  • 作为CMS发生失败时的备选,在并发收集发生Concurrent Mode Failure时使用。

image-20260213023005947

1.5 Parallel Old收集器

parallel scavenge收集器的在老年代版本。是JDK6之后推出的, 在此之前 Parallel scavenge 只能和serial old搭配,而这种搭配,老年代是单线程,新生代是多线程,当老年代内存较大时候,总吞吐量可能还不如ParNew + CMS。

在Parallel Old 推出后,"吞吐量优先“ 收集器有了合适的组合,在注重吞吐量的情况下可以使用parallel scavenge + Parallel Old

image-20260213025119209

1.6 CMS收集器

CMS是老年代的垃圾收集器,目标是低延迟,采用的是标记-清除算法实现的(之前提到过,由于标记清除不需要移动对象,所以延迟是低了,但是多了碎片内存访问的消耗,所以吞吐量就低了
流程:

  • 初始标记(STW)
    • 初始标记仅仅是标记一下GCroots直接访问到的对象,速度很快(得益于Oopmap的加成),会暂停所有用户线程
  • 并发标记
    • 并发标记时不会暂停其他用户线程,是通过GCroots遍历能访问到的引用对象。
  • 重新标记(STW)
    • 会暂停其他用户线程,重新标记是纠正一下并发标记期间的标记记录,采用的是增量更新算法增量更新是解决黑色节点指向白色节点的情况,是通过记录增加引用的黑色节点,在并发标记后将这些黑色节点变成灰色,重新访问它的引用对象)。
  • 并发清除
    • 不暂停用户线程

image-20260213030255372
CMS收集器的三个缺点:

  • 对处理器资源敏感,毕竟是并发设计的。因为线程占用会导致应用程序变慢,降低了吞吐量(我之前提到的标记清除算法的缺点也是其中之一)
  • CMS收集器无法处理浮动垃圾(在并发标记过程中,白色标记成黑色,导致这些垃圾需要在之后的gc中回收),有可能出现Con-current Mode Failure(并发失败),进而导致完全STW的full gc。当发生并发失败时候,就不得不把CMS换成Serial Old来使用。
    • 因为CMS必须预留空间给线程进行分配,所以通常在老年代内存还没使用满就得进行gc(JDK5的默认设置是68%)
  • 标记清除算法会产生大量的碎片化空间,进而导致full gc

1.7 Garbage First收集器

G1收集器是为了取代ParNew + CMS 的组合,与之前的收集器不同,G1收集器的作用范围是整个堆。G1是专门针对服务端应用程序的垃圾收集器

  • 什么是G1收集器的Mixed GC模式?

在G1收集器中,虽然还有新生代,老年代的概念,但是却没有实际新生代,老年代的内存,而是一个又一个的Region(意思是有些区属于eden区,survivior区,有些是老年代),而GC回收会优先回收那些回收价值高(哪个区回收内存最多)的区,这就是G1的Mixed GC模式。

  • G1的humongous区

除了新生代,老年代,G1还有一片特殊的区,专门用来存储大对象的,叫做humongous区,当对象超过G1中region的内存的一半时,会被放进humongous区中。

  • G1的回收思路

G1中的最小回收单元是region,G1中新生代,老年代不再是固定的了,而是一系列区的集合。G1收集器会去跟踪每个region的回收价值(也就是回收所获得空间以及回收花费时间的经验值),在后台中维护一个优先级列表,在回收时,优先选择价值高的区进行回收。

  • G1中如何解决跨代引用问题

和其他垃圾收集器一样,跨代引用问题也是通过记忆集来解决的。但是在G1中不太一样,G1中每个region都会有自己的记忆集,这些记忆集会记录下其他region到自己的指针,并记录下这些指针在哪些卡页中。G1的记忆集本质上是哈希表,key是region的地址,value是一个集合,里面的存储元素是卡表的索引号(也就是双向的结构,“我指向谁,谁指向我”),由于region数量比传统收集器分代要多,所以G1的内存占用也要比传统收集器更高

  • G1收集器中并发标记阶段处理措施

在CMS 中采用的是增量更新算法来纠正并发标记中用户线程的影响,而G1收集器采用的是原始快照算法(SATB)来做的

原始快照算法就是处理灰色节点引用被删除,导致有些黑色节点最终变成白色节点被回收。做法是通过记录删除的引用,在结束并发标记后,将删除的对象置灰,重新访问这些对象的引用

  • G1收集器在并发过程中遇到用户线程对象分配的情况

G1为每个region设置了两个名为TAMS(Top at Mark Start)的指针,把指针中这一块内存用于用户线程分配对象,G1收集器会标记这上面的对象是默认存活的,不加入垃圾回收的范围。

  • G1收集器怎样建立起可靠的停顿预测模型?

用户通过-XX:MaxGCPauseMillis参数指定的停顿时间 只意味着垃圾收集发生之前的期望值。G1收集器会记录每个region记忆集中脏卡数量,回收耗时,等花费成本,分析出平均值,标准方差,置信度等指标,来预测回收哪些region能做到回收价值最高,且停顿时间小于预期值。

  • G1算法详细步骤:G1除了并发标记外,其他都要暂停用户线程的

    • 初始标记

      • 标记GCroots直接关联到的对象,修改TAMS指针的值。耗时短,有STW
    • 并发标记

      • 不暂停用户线程,并发标记整个引用链,同时处理SATB对象
    • 最终标记

      • 主要是原始快照算法,处理并发标记剩下的中的SATB记录,会暂停所有用户线程
    • 筛选回收

      • 对region回收成本和价值排序,根据用户期待时间指定回收计划。必须暂停用户线程

image-20260213044146082

G1 VS CMS

  • G1优点
    • G1采用标记-整理和标记-复制,而CMS用的是标记-清除,这一点G1不会产生碎片化空间,适合长时间运行。
    • G1支持用户设定gc期望执行时间,G1会优先回收某些region,尽量追求在延迟和吞吐量中寻找一个平衡
  • G1缺点
    • 从内存角度来说,G1需要为每个region都维护其记忆集,这就导致G1占用内存更大,而CMS的记忆集就相对简单多了。
    • CMS是通过写后屏障来维护卡表,而G1不单单是通过写后屏障维护卡表,为了实现SATB算法,还要通过写前屏障来记录引用的变化

2.低延迟垃圾收集器

衡量垃圾收集器的三项指标,三者是不可能同时实现的三角

  • 延迟
  • 吞吐量
  • 内存占用

2.1 Shenandoah收集器

Shenandoah收集器和G1收集器高度类似,同样是基于Region的堆内存布局,同样有humongous区,回收策略也是优先回收价值最大的。

与G1的不同之处

  • G1回收的时候是要暂停用户线程的,但是shenandoah是并发回收的
  • shenandoah默认不使用分代收集,不会有新生代和老年代。
  • shenandoah不使用G1中的记忆集,而是改名用“连接矩阵”的全局数据结构来记录跨代引用。

shenandoah中并发回收阶段,是将存活的对象复制到未被使用的region中,如果用户线程暂停是很简单的,在并发的场景下,shenandoah用到了Brooks pointer转发指针来实现。这通常是在原先对象的内存上设置保护陷阱,当线程访问到原先对象的内存,就把访问转发到新对象上。

2.2 ZGC收集器

ZGC 和shenandoah目标类似,都是希望在不影响吞吐量的情况下尽量去减小延迟。

ZGC是基于region内存分区的,不设分代,使用了读屏障,染色指针,和内存多重映射的技术来实现可并发的标记-整理算法的,来降低延迟的一款垃圾收集器。

3. 如何选择合适的垃圾收集器

3.1 收集器的权衡

  • 应用程序的关注点:

    • 数据分析、科学计算类的任务,目标是能尽快算出结果,关注点就是吞吐量

    • SLA应用,关注的就是延迟

    • 客户端应用或者嵌入式应用,关注的就是内存占用

  • ·运行应用的基础设施

    • 系统架构,操作系统这些
  • JDK发行商,版本号

3.2 虚拟机及垃圾收集器日志

HotSpot所有功能的日志都收归到了“-Xlog”参数上

image-20260213054858075

  • 最关键的参数:
    • selector,由标签(Tag)和日志级别(Level)共同组成,指的是某个模块的名字,希望虚拟机打哪个模块的日志,如gc的则是-Xlog:gc
    • 日志级别从低到高,共有Trace,Debug,Info,Warning,Error,Off六种级别
    • 修饰器(Decorator)来要求每行日志输出都附加上额外的内容,支持附加 在日志行上的信息包括:
      • time:当前日期和时间。
      • uptime:虚拟机启动到现在经过的时间,以秒为单位。
      • timemillis:当前时间的毫秒数,相当于System.currentTimeMillis()的输出。
      • uptimemillis:虚拟机启动到现在经过的毫秒数。
      • timenanos:当前时间的纳秒数,相当于System.nanoTime()的输出。
      • uptimenanos:虚拟机启动到现在经过的纳秒数。
      • pid:进程ID。
      • tid:线程ID。
      • level:日志级别
      • tags:日志输出的标签集。

4. 内存分配与回收策略

  • 对象优先在Eden分配

大多数情况下,对象会先在新生代的eden区进行分配,当Eden空间不够时,则发起一次minor gc

  • 大对象直接进入老年代

HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配。目的是为了避免在Eden区和两个survivor区中来回复制,产生大量内存复制操作。

  • 长期存活的对象将进入老年代

对象的对象头中,设置了对象年龄计数器,每经历过一次minor gc的对象,这个年龄会+1,通过MaxTenuringThreshold设置进入老年代的对象年龄,当对象年龄大于这个值,将会进入老年代。

  • 动态对象年龄判定

当survivor区中,相同年龄的对象大小总和大于survivor空间的一半,年龄大于或者等于该年龄的对象就会进入老年代,无需等到gc年龄。

  • 空间分配担保
    • 在发生minor gc前 先检查老年代连续空间大小是否大于所有新生代对象,如果大于,可以确保minor gc安全,如果不大于,则检测handlepromotionfailure是否允许担保失败,如果允许,则取之前每一次回收晋升到老年代对象的内存平均值,看最大可用连续空间是否大于这个值,如果大于就尝试minor gc,如果小于或者不允许担保,则进行full gc
posted @ 2026-04-07 16:43  不会coding的喵酱  阅读(1)  评论(0)    收藏  举报