曹耘豪的博客

JVM之GC

  1. 对象存活判断
    1. 1. 引用计数 (Python)
    2. 2. 可达性分析 (Java)
    3. Java的四种引用
    4. finalize
    5. 方法区GC
  2. 垃圾收集算法
    1. 分代收集理论
    2. 标记-清除算法
    3. 标记-复制算法
    4. 标记-整理算法
  3. 用户线程停顿时机、安全点与安全区域
    1. 停顿的两种方案
  • 垃圾收集器
    1. Serial收集器
    2. ParNew收集器
    3. Parallel Scavenge收集器
    4. Serial Old收集器
    5. Parallel Old 收集器
    6. CMS收集器
    7. G1收集器(Garbage First)
    8. Shenandoah收集器
    9. ZGC
  • 垃圾收集器组合
  • updated on 2022-11-16

    对象存活判断

    1. 引用计数 (Python)

    每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

    2. 可达性分析 (Java)

    从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

    在Java语言中,GC Roots包括:

    Java的四种引用

    finalize

    方法区GC

    垃圾收集算法

    分代收集理论

    GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短

    “分代收集”(Generational Collection)算法,把Java堆分为新生代(Young Generation)和老年代(Old Generation),这样就可以根据各个年代的特点采用最适当的收集算法。

    标记-清除算法

    “标记-清除”(Mark-Sweep)算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

    缺点:

    标记-复制算法

    “标记-复制”(Mark-Coping)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

    这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

    标记-整理算法

    “标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

    用户线程停顿时机、安全点与安全区域

    安全点:触发GC时,程序需运行至特定的位置然后停顿,一般是指令序列的复用点,如方法调用、循环跳转、异常跳转等。

    停顿的两种方案

    1. 抢先式中断(Preemptive Suspension)
      • GC发出中断信号,用户线程判断是否在安全点,如果不在则继续运行至安全点
    2. 主动式中断(Voluntary Suspension)
      • 在安全点设置轮询标志,如果发现中断标志则在最近的安全点主动挂起

    安全区域:在一段代码片段中,引用关系不会发生变化

    垃圾收集器

    Serial收集器

    Serial收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。垃圾收集的过程中会Stop The World(服务暂停)

    ParNew收集器

    ParNew收集器其实就是Serial收集器的多线程版本

    Parallel Scavenge收集器

    Parallel Scavenge收集器类似ParNew收集器,Parallel Scavenge收集器更关注系统的吞吐量

    Serial Old收集器

    Serial Old是Serial收集器的老年代版本

    Parallel Old 收集器

    Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和算法。这个收集器是在JDK 1.6中才开始提供

    CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    收集步骤:

    1. 初始标记(CMS initial mark)

      • Stop The World
      • 仅标记一下GC Roots能直接关联到的对象
    2. 并发标记(CMS concurrent mark)

      • GC Roots Tracing
      • 和用户线程并行
    3. 重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

      • Stop The World
      • 这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
    4. 并发清除(CMS concurrent sweep)

    G1收集器(Garbage First)

    收集步骤:

    1. 初始标记(Initial Mark):标记GC Root关联的对象
      • Stop the World
      • 由普通Mintor GC伴随触发
    2. 并发标记(Concurrent Marking):进行可达性分析
      • 在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断
      • 若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收
      • 标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
    3. 最终标记(Final Marking):再标记“并发标记期间产生新的垃圾”
      • Stop the World
      • 处理SATB表(原始快照)
    4. 筛选回收(Live Data Counting and Evacuation),
      • Stop the World
      • 根据用户期望的停顿时间来制定回收计划
      • 把决定回收的那一部分Region的存活对象复制到空的Region中,在清理掉整个旧Region的全部空间
      • 并行

    Shenandoah收集器

    和G1收集器类似,不同点有:

    ZGC

    Region分布

    并发整理算法——指针颜色技术

    支持“NUMA-Aware”的内存分配,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。

    步骤:

    1. 并发标记:标记指针中的Marked1和Marked0标志位
    2. 并发预备重分配:根据特定的查询条件统计得出本次收集过程中要清理哪些Region
      • 扫描所有Region
      • 类卸载和弱引用处理
    3. 并发重分配
      • 每个Region维护转发表。外部引用会被内存屏障截获,然后更新该引用的值,所以只有第一次访问旧对象会慢
    4. 并发重映射:修正所有引用
      • 合并到下一次的“合并标记”阶段

    垃圾收集器组合

    组合新生代GC策略老年代GC策略说明
    组合1SerialSerial Old单线程,适合客户端
    组合2SerialCMS+Serial Old当CMS进行GC失败时,会自动使用Serial Old。
    组合3ParNewCMS
    组合4ParNewSerial Old
    组合5Parallel ScavengeSerial Old适用于后台持久运行的应用程序
    组合6Parallel ScavengeParallel Old
    组合7G1G1
       / 
      ,