JVM的垃圾回收

JVM的垃圾回收机制和垃圾收集器

⚙️ 垃圾回收机制

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存中已经死亡的或者长时间没有使用的对象进行清除和回收。

垃圾回收的触发条件

  • 内存不足时 :当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求 :虽然垃圾回收是自动的,但是可以通过调用 System.gc()Runtime.getRuntime().gc() 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数 :启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。
  • 对象数量或内存使用达到阈值 :垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

📚 堆的空间分配

堆是垃圾收集器管理的主要区域。

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  • 新生代(Young Generation):存放新创建的对象。
    • Eden 区:所有新创建的对象首先分配到 Eden 区
    • Survivor 区:Eden 区中的存活对象会被复制到 Survivor 区(一般分为两个区域,S0 和 S1),经过多次 GC 存活的对象会逐渐晋升到老年代。
      • From区
      • To区
  • 老生代(Old Generation):存放存活时间较长的对象,通常是从新生代晋升过来的对象。
  • 永久代(Permanent Generation):存放类的元数据信息,包括类的静态变量、方法等

JDK 8 版本之后 永久代已被 元空间 取代,元空间使用的是直接内存

堆内存分配

提示

分代回收的原因

主要是为了提高垃圾回收效率,依据对象的生命周期特点来进行优化。

对象的生命周期特点

  • 大多数对象存活时间短 :大部分对象会很快变成垃圾,不再被使用,这些短生命周期的对象会分配在新生代。
  • 少部分对象存活时间长 :一些长期存活的对象不会很快被回收,分配在新生代的对象经过多次垃圾回收仍存活的,将晋升到老年代。

不同的回收算法

  • 新生代的回收 :新生代通常采用 复制算法,因为新生代中大部分对象生命周期短,大部分会在一次 GC 中被回收,复制算法只需要在内存中保留少量存活对象,并将它们复制到 Survivor 空间,回收剩余区域。这种算法效率很高,适合新生代对象频繁创建和回收的特点。
  • 老年代的回收:老年代中对象存活时间长,回收频率低,使用 标记-整理算法标记-清除算法,更加适合老年代对象的特性。

📌 新生代

新生代用来接收新创建的对象

💡 新生代的分区逻辑

主要是为了提供内存利用率。

由于新生代对象朝生夕死的特性,天然适合复制算法。如果将新生代一分为二,划两块区域,每次只使用其中一个,GC 后将存活的复制到另一个区域,然后清理老区域非存活对象,这样替换使用两块区域可以避免内存碎片的存在。内存利用率只用一半

因此要将新生代划分为Eden区和Survivor区,其中Survivor区被分S0和S1两个区域来执行标记-复制算法,轮流执行标记-复制算法

💡 Eden区

多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,JVM 会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。

通过 Minor GC 之后,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区,如果 From 区不够,则直接进入 To 区。

💡 Survivor区

Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

📌 老年代

老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。

由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记整理算法。

对于某些对象直接进入老年代

  • 大对象: 需要大量连续内存空间的对象,这部分对象都会直接进到老年代。这样做主要是为了避免在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。
  • 长期存活对象 :虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加 1 岁。当年龄增加到 15 岁时,这时候就会被转移到老年代。
  • 动态对象年龄 :如果 Survivor 空间中某个年龄段的对象总大小超过了 Survivor 空间的一半,那么该年龄段及以上年龄段的所有对象都会在下一次垃圾回收时被晋升到老年代,无需等到15岁。

🗑️ 垃圾判断算法

📌 引用计数法

  • 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
  • 缺点 :无法处理循环引用的问题,两个对象互相引用时,引用计数器永远不会为 0

📌 可达性分析算法

  • 原理 :从GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。
GC Root
  • 前置操作(Stop the world) :在进行垃圾回收之前,JVM 会暂停所有正在执行的应用线程。可达性分析过程必须确保在执行分析时,内存中的对象关系不会被应用线程修改。如果不暂停应用线程,可能会出现对象引用的改变,导致垃圾回收过程中判断对象是否可达的结果不一致,从而引发严重的内存错误或数据丢失。

提示

可以作为GC ROOT的对象

  • 线程栈中的引用 :每个线程栈中的局部变量、参数等。
  • 类的静态变量 :被类加载器加载后的类会存储在方法区,类的静态变量可以作为 GC Roots。
  • JNI 全局引用 :通过 JNI 创建的全局引用可以作为 GC Roots。
  • 运行时常量池中的常量(String 或 Class 类型)

🗑️ 垃圾回收算法

📌 标记-清除算法

标记-清除算法是最基础的垃圾回收算法,分为“标记(Mark)”和“清除(Sweep)”阶段。

  • 首先标记出所有不需要回收的对象

  • 在标记完成后统一回收掉所有没有被标记的对象。

    标记-清除

优缺点

📌 标记-复制算法

为了解决碎片空间的问题,出现了对标记-清除算法的改进。

原理:

优缺点

标记-复制算法

提示

新生代中主要使用标记复制算法

📌 标记-整理算法

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

标记-整理算法

📚 Stop The World

“Stop The World"是 Java 垃圾收集中的一个重要概念。在垃圾收集过程中,JVM 会暂停所有的用户线程.。主要原因是为了防止在垃圾收集过程中,用户线程修改了堆中的对象,导致垃圾收集器无法准确地收集垃圾。


⚙️ 分代垃圾回收机制

根据对象的分代机制,针对各代有不同的垃圾回收机制

📌 Minor GC

📌 Major GC

📌 Full GC

📌 Mixed GC

Mixed GC(仅适用于 G1 GC 的混合垃圾回收):


🗑️ 垃圾收集器

🗑️ 新生代垃圾收集器

💡 Serial 收集器

Serial(串行)收集器是最基本的垃圾收集器了。Serial收集器是单线程收集器,它只会使用一条垃圾收集线程去完成垃圾收集工作。同时在进行垃圾收集工作的时候必须触发 Stop-The-World(STW)操作,所有应用线程在 GC 时暂停。

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

💡 ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。与 CMS 收集器配合使用时,通常会选择 ParNew 收集器作为新生代收集器。

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

💡 Parallel Scavenge 收集器

也称为 “吞吐量收集器”,追求最大化 CPU 时间的利用率。并行处理新生代垃圾回收,适合大规模后台任务处理,注重吞吐量而非延迟。

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

🗑️ 老年代垃圾收集器

💡 Serial Old 收集器

Serial 收集器的老年代版本,使用标记-整理(Mark-Compact)算法进行垃圾回收。

💡 Parallel Old 收集器:

Parallel Scavenge 收集器的老年代版本,使用多线程并行标记-整理算法。

💡 CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它实现了让垃圾收集线程与用户线程(基本上)同时工作。

工作流程:CMS 使用 标记-清除 算法进行垃圾收集

提示

三色标记法

三色标记算法 是垃圾回收器中常用的一种 增量标记算法

三色标记的基本概念

标记过程

缺点

💡 G1收集器

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

G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代或老年代的角色。同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。

G1 基于标记–整理 算法, 不会产生空间碎片

工作流程

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)

💡 G1和CMS的区别和使用场景

💡 区别

💡 使用场景

CMS适用场景:

G1适用场景: