Contents

JVM(二): 垃圾回收

1. 垃圾标记算法

1.1 引用计数法

在 Java 中,要操作对象则必须用引用进行。设置引用计数器,对象被引用时计数器加1,引用失效时计数器减1,如果计数器为0,则被标记为垃圾。引用计数法存在循环引用问题。

1.2 可达性分析

为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索,根据引用关系向下搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。 可以作为GC Root的对象包括下面几种:

  • 虚拟机栈帧中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

2. 垃圾回收算法

2.1 标记清除

  • 标记清除:标记要回收的对象,标记完成后统一回收被标记对象
  • 优点:速度快
  • 缺点:有内存碎片
    https://s2.loli.net/2023/06/07/TXKkH1GS75poNeZ.png

2.2 标记整理

  • 标记整理:标记要回收的对象后,将存活对象移动到一端,然后将边界外的内存回收
  • 优点:没有碎片
  • 缺点:速度慢
    https://s2.loli.net/2023/06/07/uazGpkYRLgdW2Po.png

2.3 标记复制

  • 标记复制:将没有标记的对象复制到To区,然后清除From区的所有对象
  • 优点:没有内存碎片,速度快
  • 缺点:占用两份内存
    https://s2.loli.net/2023/06/07/jGpMPLZ4t2aFfsH.png

3. 分代GC

3.1 垃圾回收过程

https://s2.loli.net/2023/06/07/HundWiPD28T91Xe.png

  • gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复行。
  • 将堆分为新生代和老年代,新生代用复制算法,老年代用标记清除或者标记整理算法。
  • 新生代有划分为Eden、From Survivor和To Survivor三个部分,对应的内存空间的大小比例为8:1:1。
  • 对象首先分配在Eden区,当对象刚分配到Eden区域时,对象的年龄为0。
  • 当Eden区第一次空间不足时,触发 minor gc,Eden区中存活的对象复制到To区中,存活的对象年龄加 1,清除所有Eden区不可达对象,并交换from 区和 to区。
  • 当Eden区第二次空间不足时,触发 minor gc,Eden区和 from 区中存活的对象复制到To区中,存活的对象年龄加 1,清除所有Eden区和 from区的不可达对象,交换 from 区和 to区 。
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit) 。
  • 经过minor gc之后又有一部分存活的对象会进入老年代,当老年代空间不足时,会触发老年代上的垃圾回收major gc。

3. 2 实例展示

https://s2.loli.net/2023/06/07/KCbTAczyer8RadU.png https://s2.loli.net/2023/06/07/AUMFC9bBXrevhZ8.png https://s2.loli.net/2023/06/07/HCrfViGvxLFBWyR.png
第一次放7MB,新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy复制到 to 中 https://s2.loli.net/2023/06/07/W4yUbJ3ZqGIxd6Y.png
第二次放512KB,新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to. 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。或内存紧张时会把大对象直接晋升到老年代. https://s2.loli.net/2023/06/07/Z4a15ASiL2WjOBK.png
如果是大对象,在新生代内存不够,老年代内存足够的时候,会直接晋升老年代,不触发GC https://s2.loli.net/2023/06/07/WZc3LrOYtUfFz8T.png
触发Full GC,新生代垃圾回收发现内存不够,触发老年代垃圾回收,内存也不够,则会抛出OutOfMemoryError:Java heap space

3.3 常见面试问题

问:为什么GC的时候要stop the world?/GC的时候不stop the world 会发生什么? 答:因为GC的时候有采用复制算法,即会对对象的地址进行复制,如果这个时候不暂停其他户的线程,可能会引发混乱。

4. 垃圾回收器

https://s2.loli.net/2024/05/14/mi8Mw5oSPLHZWnp.png

  • 串行垃圾回收器:Serial、Serial Old
  • 并行垃圾回收器:ParNew、Parallel Scavenge、Parallel Old
  • 新生代可以使用的垃圾回收器:Serial、ParNew、Parallel Scavenge
  • 老年代可以适用的垃圾回收器:CMS、Serial Old、Parallel Old
  • G1回收器适用于新生代和老年代
  • 相互之间有连线的表示可以配合使用

4.1 Serial GC (串行垃圾回收器)

  • 单线程垃圾回收器,适用于单处理器环境。
  • 在垃圾回收时会暂停所有应用线程(STW,即Stop-The-World)。
  • 在新生代使用"标记-复制"算法,在老年代使用"标记-整理"算法。
  • 适合小型应用程序和内存占用较少的环境,Client 模式的虚拟机的首选。

4.2 Parallel GC (并行垃圾回收器)

  • 多线程垃圾回收器,适用于多处理器环境。
  • 同时回收多个垃圾,减少垃圾回收时间。
  • 在新生代使用"标记-复制"算法,在老年代使用"标记-整理"算法。
  • 适用于需要较高吞吐量的应用。

4.3 CMS GC (并发标记-清除垃圾回收器)

  • Concurrent Mark-Sweep GC,通过并发的方式减少STW时间。
  • 在垃圾回收的标记阶段与应用线程并发运行,减少暂停时间。
  • 采用“标记-清除”算法,主要用于老年代的垃圾收集。
  • 适用于低停顿时间要求的应用,但会增加CPU使用率。

步骤:

  1. 初始标记(Stop The World):标记 GC Root 可以直接关联的对象,速度很快

  2. 并发标记(和用户线程一起):主要标记过程,标记全部对象

  3. 重新标记(Stop The World):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,停顿时间略长于初始标记,但远远短于并发标记的时间;

  4. 并发清除(和用户线程一起):基于标记结果,直接清除对象

由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。需要注意的是,CMS垃圾回收器虽然能够显著减少应用程序的暂停时间,但它也有一些缺点。例如,它会产生内存碎片,因为它只是简单地清除掉被标记为“垃圾”的对象,而不进行内存整理。此外,由于CMS垃圾回收器是并发执行的,所以它可能会消耗更多的CPU资源。

4.4 G1 GC (Garbage-First垃圾回收器)

  • 设计用于替代CMS GC,适用于多核处理器和大内存环境。
  • 分区收集垃圾,优先回收含垃圾最多的分区,减少STW时间。
  • 整体上是“标记-整理”算法,两个区域之间是“标记-复制”算法。
  • 适用于需要平衡吞吐量和停顿时间的应用。

关键概念:

  • Region(区域):G1将堆划分为多个大小相等的有优先级的区域,每个区域可以是Eden(伊甸区)、Survivor(幸存区)、Old(老年代)或Humongous(巨型区)之一。
  • Young GC:只处理Eden和Survivor区域的垃圾回收。
  • Mixed GC:处理所有区域,包括部分Old区域。

与 CMS 比较:

G1垃圾回收器是一种基于区域的垃圾回收器,它将堆空间划分为多个相同大小的区域(Region),并在每个区域中独立地进行垃圾回收。

  1. 减少内存碎片:G1通过将堆分成多个小区域(Region)并在回收时压缩这些区域,减少了内存碎片,而CMS会产生内存碎片,导致长时间的Full GC。
  2. 控制暂停时间:G1允许用户设置期望的暂停时间目标,并尽量在满足这个目标的情况下进行垃圾回收。CMS无法精确控制暂停时间,可能会有较长的停顿。

4.5 ZGC (Z垃圾回收器)

  • 专为超大内存应用设计,目标是将垃圾回收停顿时间限制在10毫秒以内。
  • 通过并发和分阶段垃圾回收实现低停顿时间。
  • 适用于低停顿时间和大内存需求的应用,几乎可以在任何时候以极低的延迟进行垃圾回收。

4.6. 常见面试问题

分代垃圾回收 VS 分区垃圾回收

分代收集算法

当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的 GC 算法。

分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。