JVM (2):垃圾回收

1 什么时候会触发GC

在Java虚拟机中,垃圾回收(GC)会在以下几种情况下触发:

  1. 系统内存不足:当Java虚拟机检测到系统内存不足时,会触发垃圾回收来释放内存空间,以确保应用程序的正常运行。这通常是通过监视堆内存的使用情况来检测的。
  2. 调用System.gc()方法:虽然调用System.gc()方法并不会立即触发垃圾回收,但它会向Java虚拟机发出建议性的垃圾回收请求。Java虚拟机可以选择是否立即响应这个请求。
  3. 长时间停顿:当应用程序执行时间较长,而且没有进行垃圾回收时,Java虚拟机可能会为了避免堆内存耗尽而触发垃圾回收。这种情况下,垃圾回收通常会引起一段较长的停顿时间,称为Full GC。
  4. Young Generation满:在分代垃圾回收器中,当Young Generation区域满时,会触发一次Minor GC。这会导致Eden区和Survivor区的垃圾回收。
  5. Old Generation满:如果Old Generation区域满了,会触发一次Major GC(也称为Full GC)。这种情况下,整个堆内存都会进行垃圾回收。
  6. 永久代/元空间满:对于HotSpot虚拟机,如果永久代(Java 7之前)或者元空间(Java 8及之后)满了,会触发一次垃圾回收。这种情况下,垃圾回收主要针对类的元数据和常量池。

2 Full GC、Minor GC

2.1 Full GC

调用System.gc()方法
Old Generation满
空间分配担保:在发生Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC 。如果小于,则查看HandlePromotionFailure 设置是否允许担保失败;如果允许,那只会进行Minor GC; 如果不允许,则也要改为进行一次Full GC 。

3 内存分配与回收

  1. 对象优先在 Eden 区分配:大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

  2. 大对象直接进入老年代:大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。

  3. 长期存活的对象将进入老年代:对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

  4. 动态对象年龄判定

  5. 空间分配担保:空间分配担保是为了确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。

  6. 只要老年代的连续空间大于(新生代所有对象的总大小或者历次晋升的平均大小)就会进行minor GC,否则会进行full GC

4 死亡对象判断方法

4.1 引用计数法

  • 引用为0的对象不再被使用

  • 实现简单、效率高

  • 无法解决对象之间的循环引用问题

4.2 可达性分析算法

根搜索算法(GC Roots Tracing):通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

[!note] 基本思路

  1. 可达性分析算法是以根对象集合(GCRoots——就是一组必须活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达

  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链

  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象

  4. 在可达性分析算法中,只有能够被根对象集合直接或者问接连接的对象才是存活对象

Root对象主要包括:
①系统类加载器(bootstrap)加载的类。
②JVM方法区中静态属性引用的对象。
③JVM常量池中引用的对象。
④JVM虚拟机栈中引用的对象。
⑤JVM本地方法栈中引用的对象。
⑥活动着的线程。

可作为GC Roots的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 本地方法栈(Native 方法)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

快速找到GC Roots:
**OopMap存储两种对象引用:对象内的引用 栈、寄存器中的引用

Java虚拟机如何快速找到GC Roots?又是如何中断线程? - 知乎 (zhihu.com)

JVM之垃圾回收(一):引用计数法+可达性分析算法_垃圾回收 可达性分析和引用计数器哪个好-CSDN博客

4.3 引用类型

1.强引用(StrongReference)
当内存空间不足,Java 虚拟机抛出 OutOfMemoryError 错误,使程序异常终止

2.软引用(SoftReference)
可有可无,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

3.弱引用(WeakReference)
可有可无,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

4.虚引用(PhantomReference)
虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

4.4 回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

废弃常量:假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。

无用的类:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

5 垃圾收集算法

部分收集( Partial GC):指目标不是完整收集整个 Java堆的垃圾收集,其中又分为:

  • 新生代收集( Minor GC/Young GC):指目标只是新生代的垃圾收集。通常采用复制算法,速度较快且频繁。

  • 老年代收集( Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS收集器会有单独收集老年代的行为。混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行收集器会有这种行为。

整堆收集( Full GC):收集整个 Java堆和方法区的垃圾收集。而Full GC则发生在整个堆空间中,包括新生代和老年代(Old Generation),用于清理整个堆中的垃圾对象,速度较慢且可能导致较大的应用停顿。

因此,Minor GC和Full GC的主要区别在于它们发生的区域和影响范围。

5.1 标记-清除算法

标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。

存在问题:

  1. 效率问题:标记和清除两个过程效率都不高。

  2. 空间问题:标记清除后会产生大量不连续的内存碎片。

5.2 标记-复制算法

将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

存在问题:

  • 可用内存变小:可用内存缩小为原来的一半。

  • 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。

这种收集算法主要用于回收新生代,将内存划分为1:1的比例会浪费大量内存。

Appel式回收的具体做法是把新生代分为一块较大的 Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块 Survivor。发生垃圾搜集时,将 Eden和 Survivor中仍然存活的对象一次性复制到另外一块 Survivor空间上,然后直接清理掉 Eden和已用过的那块 Survivor空间。

回收后存活的对象可能大于Survivor空间的大小,当Survivor空间不足以容纳一次空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)

如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代。

5.3 标记-整理算法

标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

存在问题:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动象操作操作必须全程暂停用户应用程序才能进行必须全程暂停用户应用程序才能进行(Stop The World

5.4 分代收集算法

根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

6 垃圾收集器

image.png|500

垃圾收集器 关注 垃圾收集算法
Parallel Scavenge 吞吐量 标记-整理
CMS 延迟 标记-清除

6.1 Serial(串行)收集器

单线程收集器,使用一条垃圾收集线程去完成垃圾收集工作,在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。用户线程因垃圾收集而导致停顿。
新生代采用标记-复制算法,老年代采用标记-整理算法。

  • 简单高效

  • 运行在客户端模式下的默认新生代收集器

6.2 Serial Old收集器

Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

image.png

6.3 ParNew收集器

ParNew 收集器其实就是 Serial 收集器的多线程并行版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

新生代采用标记-复制算法,老年代采用标记-整理算法。

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

image.png

并行(并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

并发( Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请户线程都在运行。但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

6.4 Parallel Scavenge 收集器(吞吐量)

Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
$$
吞吐量=\frac{运行用户代码时间}{运行用户代码时间+运行垃圾收集时间}
$$
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交的分析任务。

新生代采用标记-复制算法,老年代采用标记-整理算法。

6.5 Parallel Old 收集器(吞吐量)

Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

image.png

6.6 CMS(Concurrent Mark Sweep)(停顿时间)

支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

采用标记-清除算法实现

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

  • 并发标记:并发标记阶段就是从 GC Roots的直接关联对象开始遍历整个对象图的过程 ,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录

  • 并发清除: 比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

image.png

6.6.1 优缺点

优点:并发收集、低停顿

  • 对 CPU 资源敏感; 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU 资源)而导致应用程序变慢,总吞吐量会降低。

  • 无法处理浮动垃圾: 由于CMS 并发清理阶段用户线程还在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后, CMS 无法在本次收集中处理掉它们,只好留待下一次GC 时再将其清理掉。这一部分垃圾就称为“浮动垃圾"。

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。 空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC 。

6.7 Garbage First(G1)

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。

  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。

  • 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。

  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。

image.png

初始标记:仅仅只是标记一下 GC Roots能直接关联到的对象,并且修改 TAMS 指针的 值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。并发标记:从 GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较最终标记:筛选回收:负责更新 Region的统计数据,对各个 Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

G1垃圾回收器具有以下特点:

  1. 区域化内存管理:G1将堆内存划分为多个固定大小的区域(Region),每个区域可以是Eden区、Survivor区或Old区。这种区域化的内存管理有助于更好地控制垃圾回收过程,减少停顿时间。

  2. 分代收集:虽然G1并不是一个传统的分代收集器,但它仍然将堆内存划分为年轻代和老年代,并且使用不同的垃圾回收策略来处理这两个代。

  3. 并发标记清除:G1使用了并发标记(Concurrent Marking)来减少垃圾回收暂停时间。在标记阶段,G1通过并发标记线程来标记活动对象,而在应用程序运行的同时,也会继续标记操作。这样可以减少标记阶段对应用程序的影响。

  4. 整理内存:G1使用了复制算法来清理内存,不再使用传统的压缩算法。在垃圾收集过程中,G1会选择一些区域进行垃圾收集,并将存活对象复制到其他区域中,从而实现内存的整理和碎片整理。

  5. 垃圾优先收集:G1根据垃圾回收需求来选择优先回收的区域,以此来提高垃圾回收效率。它会优先选择包含垃圾最多的区域进行回收,从而最大程度地减少垃圾对象。

7 参考

标记复制法、标记清除法和标记整理法的区别-CSDN博客