JVM 垃圾回收简介

21 阅读16分钟

JVM GC

一、什么是垃圾回收?

**垃圾回收(GC)**是 JVM 自动管理内存的一项机制。它会自动检测和回收程序中不再被引用的对象,释放内存空间,避免内存泄漏和手动管理内存的复杂性。


二、JVM 内存结构与 GC 相关区域

JVM 的堆内存主要分为以下几个区域:

  • 新生代(Young Generation)

    • 包含 Eden 区和两个 Survivor 区(S0、S1)
    • 新创建的对象首先分配在 Eden 区
  • 老年代(Old Generation / Tenured Generation)

    • 存放生命周期较长的对象
  • 元空间(Metaspace,JDK8 及以后)

    • 存放类的元数据(类信息、方法等)

三、GC 的基本过程

  1. 对象创建:新对象分配在新生代的 Eden 区。
  2. Minor GC(小型回收) :主要回收新生代,回收 Eden 和 Survivor 区中不再被引用的对象。
  3. 对象晋升:经过多次 Minor GC 仍存活的对象会被移动到老年代。
  4. Major GC / Full GC(大型回收/完全回收) :回收老年代(甚至整个堆,包括新生代和老年代),通常耗时较长。

四、常见的垃圾回收器

1. Serial GC

  • 单线程回收,适合单核、内存小的场景。
  • 参数:-XX:+UseSerialGC

2. Parallel GC(吞吐量优先 GC)

  • 多线程回收,适合多核服务器,追求高吞吐量。
  • 参数:-XX:+UseParallelGC(JDK8 默认)

3. CMS GC(并发标记清除 GC)

  • 低停顿,适合对响应时间要求高的应用。
  • 参数:-XX:+UseConcMarkSweepGC

4. G1 GC(Garbage First GC)

  • 面向服务端,低停顿,分区回收,适合大堆内存。
  • 参数:-XX:+UseG1GC(JDK9 及以后默认)

5. ZGC、Shenandoah(JDK11+)

  • 超低停顿,适合大内存、对延迟极敏感的场景。
  • 参数:-XX:+UseZGC-XX:+UseShenandoahGC

五、GC 的主要算法

1. 标记-清除(Mark-Sweep)

  • 标记所有存活对象,清除未被标记的对象。

2. 复制算法(Copying)

  • 新生代常用,将存活对象复制到另一块区域,清空原区域。

3. 标记-整理(Mark-Compact)

  • 标记存活对象,然后将其移动到一端,清理无效空间,减少内存碎片。

4. 分代收集(Generational Collection)

  • 根据对象生命周期将堆分为新生代和老年代,分别采用不同算法优化回收效率。

六、GC 的触发时机

  • 新生代空间不足时,触发 Minor GC。
  • 老年代空间不足时,触发 Major GC 或 Full GC。
  • 调用 System.gc() 可能触发 Full GC(不推荐频繁使用)。
  • 元空间溢出时也可能触发 Full GC。

七、GC 日志与调优

  • 可以通过 JVM 参数开启 GC 日志,分析回收频率、耗时等:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
  • 常见调优手段:

    • 调整堆大小(-Xms-Xmx
    • 选择合适的 GC 算法
    • 优化对象创建和引用,减少垃圾产生
    • 监控和分析 GC 日志,定位性能瓶颈

八、GC 对应用的影响

  • 停顿(Stop-The-World) :GC 过程中,应用线程会被暂停,影响响应时间。
  • 频繁 GC:可能导致性能下降,甚至“GC风暴”。
  • Full GC:耗时最长,频繁 Full GC 通常是内存泄漏或参数设置不合理的信号。

九、常用 JVM GC 参数示例

# 指定最大/最小堆内存
-Xms2g -Xmx2g

# 选择 G1 GC
-XX:+UseG1GC

# 打印 GC 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

十、总结

  • JVM 垃圾回收是自动管理内存的机制,避免内存泄漏和手动释放内存的复杂性。
  • 常见 GC 器有 Serial、Parallel、CMS、G1、ZGC 等。
  • GC 过程包括对象分代、标记、清除、复制、整理等步骤。
  • 合理选择和调优 GC 策略,对 Java 应用的性能和稳定性至关重要。

GC 相关区域

一、JVM 内存结构与 GC 相关区域

JVM 堆内存主要分为新生代(Young Generation)老年代(Old Generation) ,此外还有元空间(Metaspace) (JDK8 及以后)。

1. 新生代(Young Generation)

  • Eden 区:新对象首先分配在这里。
  • Survivor 区:分为 S0(From Survivor)和 S1(To Survivor)两个区,用于对象“幸存”后的复制和晋升。

2. 老年代(Old Generation / Tenured Generation)

  • 存放生命周期较长、经过多次 GC 仍然存活的对象。

3. 元空间(Metaspace)

  • 存放类的元数据(类信息、方法等),不再属于堆内存(JDK8 之前为永久代 PermGen)。

二、对象从新生代到老年代的详细过程

  1. 对象创建

    • 新对象分配在 Eden 区。
  2. Eden 区满时,触发 Minor GC

    • GC 会扫描 Eden 和一个 Survivor 区(From Survivor),将存活对象复制到另一个 Survivor 区(To Survivor)。
    • 未被引用的对象被回收。
  3. 对象年龄增加

    • 每次 Minor GC,幸存下来的对象年龄+1。
    • 达到一定年龄(如 15,具体由 JVM 参数 MaxTenuringThreshold 控制)后,晋升到老年代。
  4. Survivor 区切换

    • 每次 Minor GC,From 和 To 区角色互换。
  5. 老年代存放长期存活对象

    • 老年代空间不足时,可能触发 Full GC。

三、GC 的触发时机

1. Minor GC(小型垃圾回收)

  • 触发条件:Eden 区满时(即新生代空间不足),会触发 Minor GC。
  • 回收范围:只回收新生代(Eden + 一个 Survivor 区)。
  • 特点:频率高,速度快,停顿时间短。

2. Major GC / Full GC(老年代/完全垃圾回收)

  • 触发条件

    • 老年代空间不足时(如晋升对象时老年代放不下)。
    • 调用 System.gc()(建议 JVM 执行 Full GC)。
    • 元空间(Metaspace)空间不足时。
    • 某些 GC 器(如 G1)会根据整体堆使用情况触发 Full GC。
  • 回收范围:回收老年代(Major GC),有时也会回收新生代和元空间(Full GC)。

  • 特点:频率低,耗时长,停顿时间长,对应用影响较大。


四、对象晋升与 GC 过程示意

  1. 新对象 → Eden 区

  2. Eden 区满 → Minor GC

    • 存活对象复制到 To Survivor 区,年龄+1
    • 其余对象被回收
  3. 多次 Minor GC 后,年龄达到阈值 → 晋升到老年代

  4. 老年代满 → Full GC

    • 回收老年代(和新生代),释放空间

五、图示(文字版)

[新生代]
 ┌─────────────┬─────────────┬─────────────┐
 │   Eden      │  S0 (From)  │  S1 (To)    │
 └─────────────┴─────────────┴─────────────┘
         │
         │ Minor GC
         ▼
[老年代] ◄─────────────── 晋升

六、总结

  • 新对象分配在 Eden 区,Eden 满时触发 Minor GC。
  • Minor GC 后,存活对象在 Survivor 区之间复制,年龄增加。
  • 对象年龄达到阈值后晋升到老年代。
  • 老年代空间不足、手动调用 System.gc()、元空间不足等会触发 Full GC。
  • Full GC 会回收整个堆(新生代+老年代),通常耗时较长。

新生代分区

1. 新生代分区及默认占比

  • **新生代(Young Generation)**分为:

    • Eden 区
    • From Survivor 区
    • To Survivor 区
默认比例(以 HotSpot JVM 为例)
  • Eden : Survivor : Survivor = 8 : 1 : 1
  • 也就是说,Eden 占新生代的 80% ,每个 Survivor 区各占 10%。
JVM 参数可调节
  • 可以通过 -XX:SurvivorRatio=8 设置 Eden 和 Survivor 的比例(默认就是 8)。
  • 例如:-XX:SurvivorRatio=8 表示 Eden : Survivor = 8 : 1。

2. 新生代与老年代在堆中的占比

  • 整个堆(Heap)  = 新生代(Young)+ 老年代(Old/Tenured)

  • 默认比例(可通过 -XX:NewRatio 设置):

    • 新生代 : 老年代 = 1 : 2
    • 即新生代占 1/3,老年代占 2/3。
  • 例如:-XX:NewRatio=2 表示新生代占 1/3,老年代占 2/3。


3. 对象创建与转移过程

  • 对象首先创建在 Eden 区。
  • Minor GC 发生时(通常是 Eden 区满了),Eden 区和 From Survivor 区的存活对象会被复制到 To Survivor 区。
  • 对象在 Survivor 区之间多次“幸存”后,年龄达到阈值(如 15),会晋升到老年代。

4. From Survivor 和 To Survivor 的关系

  • 每次 Minor GC 时,只有一个 Survivor 区作为“From”,另一个作为“To”。
  • To Survivor 区在 GC 前是空的,GC 过程中存活对象会被复制到 To 区。
  • GC 结束后,To 区变成新的 From 区,原来的 From 区被清空,角色互换。
  • 所以,Survivor 区总有一个是空的。

5. 总结

  • 新生代分为 Eden、From Survivor、To Survivor,默认比例 8:1:1。
  • 新生代和老年代默认比例 1:2(新生代 1/3,老年代 2/3)。
  • 对象先在 Eden 区创建,Minor GC 后存活对象进入 Survivor 区。
  • From Survivor 和 To Survivor 总有一个是空的,GC 后角色互换。

To Survivor 放不下存活对象怎么办?

如果 To Survivor 区放不下所有存活的对象,JVM 会将放不下的对象直接晋升(提升)到老年代(Old Generation)


详细解释

  • 在 Minor GC 过程中,Eden 区和 From Survivor 区的存活对象会被复制到 To Survivor 区。

  • 如果 To Survivor 区空间不足,无法容纳所有需要复制的对象,这些对象会被直接移动到老年代

  • 这种情况通常发生在:

    • Survivor 区设置过小;
    • 突然有大量对象“幸存”;
    • 对象年龄较大但未达到晋升阈值。

可能的后果

  • 老年代空间会被更快填满,可能更早触发 Full GC。
  • 如果老年代也放不下,可能会导致 OutOfMemoryError

总结

To Survivor 区放不下存活对象时,超出的对象会被直接晋升到老年代。

这也是为什么合理设置 Survivor 区大小和晋升阈值很重要。

Major GC 和 Full GC

Major GC 和 Full GC 并不完全一样,但在实际使用中有时会混用,下面是详细区别:


Major GC

  • 主要指老年代(Old Generation)的垃圾回收
  • 只回收老年代,不一定会回收新生代。
  • 也叫 Old GC

Full GC

  • 指**整个堆(包括新生代和老年代)以及元空间(Metaspace)**的垃圾回收。
  • 通常会回收所有区域,耗时更长。
  • Full GC 过程中也会执行 Major GC,但还会额外回收新生代和元空间。

总结

  • Major GC:只回收老年代。
  • Full GC:回收整个堆(新生代+老年代+元空间)。
  • 实际上,很多情况下 Full GC 会包含一次 Major GC,但 Full GC 的范围更大、影响更重。

关于元空间

元空间(Metaspace)自 JDK8 起确实不属于堆内存,它是 JVM 进程的本地内存空间,用于存放类的元数据。


Full GC 会不会回收元空间?

会的。

  • 虽然元空间不属于堆,但Full GC 过程中,JVM 也会尝试回收元空间中无用的类元数据(比如已经卸载的类)。
  • 这意味着 Full GC 不仅回收新生代和老年代,还会清理元空间中不再被引用的类信息。

参考说明

  • Oracle 官方文档:

    During a full GC, the JVM will also attempt to reclaim space in the Metaspace by unloading classes that are no longer referenced.


总结:
虽然元空间不属于堆,但 Full GC 依然会尝试回收元空间的无用类元数据。

Major GC

一、什么是 Major GC?

Major GC(也叫 Old GC)是指对**老年代(Old Generation/Tenured Generation)**进行的垃圾回收。
它的主要目标是回收老年代中不再被引用的对象。

  • 老年代存放的是生命周期较长、经过多次 Minor GC 仍然存活的对象。
  • Major GC 通常会导致较长的 Stop-The-World(STW)停顿,因为老年代对象多、回收复杂。

二、Major GC 的触发时机

1. 老年代空间不足

  • 当有对象要晋升到老年代(比如 To Survivor 区放不下存活对象,或对象年龄达到阈值),但老年代空间不足时,会触发 Major GC,尝试回收老年代空间。

2. 显式调用 System.gc()

  • 调用 System.gc() 时,JVM 可能会触发一次 Full GC(包含 Major GC),不过是否执行取决于 JVM 参数(如 -XX:+DisableExplicitGC)。

3. 某些 GC 器的特殊策略

  • 比如 CMS GC 在老年代使用率达到一定阈值时,会并发或并行地启动 Major GC。
  • G1 GC 会根据整体堆的使用情况,动态决定是否需要对老年代进行回收。

4. 老年代碎片过多

  • 老年代出现大量碎片,无法为大对象分配连续空间时,也可能触发 Major GC。

5. Full GC 过程中

  • Full GC 一定会包含 Major GC,但 Major GC 不一定包含 Full GC。

三、Major GC 的过程

  • 标记-清除/标记-整理:JVM 会标记老年代中所有存活对象,然后清除未被引用的对象,或将存活对象整理到一起,减少内存碎片。
  • Stop-The-World:Major GC 期间,所有应用线程会被暂停,直到回收完成。

四、Major GC 的影响

  • 停顿时间长:因为老年代对象多,回收复杂,Major GC 停顿时间通常比 Minor GC 长。
  • 频繁 Major GC 是性能瓶颈:如果老年代频繁回收,说明内存压力大,可能导致应用卡顿甚至不可用。
  • 可能导致 Full GC:如果 Major GC 后空间仍不足,JVM 可能会触发 Full GC,回收整个堆和元空间。

五、如何减少 Major GC 的发生?

  • 优化对象生命周期,减少老年代压力。
  • 合理设置堆和新生代大小,避免对象过早晋升到老年代。
  • 监控和分析 GC 日志,及时发现和调整内存参数。

六、总结

  • Major GC 是对老年代的垃圾回收,通常在老年代空间不足、对象晋升失败、显式调用 System.gc() 等情况下触发。
  • Major GC 会导致较长的应用停顿,频繁发生会严重影响服务性能。
  • 合理调优内存参数和代码结构,可以有效减少 Major GC 的发生。

Major GC 触发时机

老年代空间不足,指的是JVM 老年代(Old Generation)内存区域的可用空间无法满足新晋升对象或大对象的分配需求。当 JVM 需要将对象从新生代晋升到老年代,或者直接在老年代分配大对象时,如果老年代剩余空间不够,就会触发 Major GC,尝试回收老年代中的无用对象,释放空间。


具体多少百分比会触发 Major GC?

  • 没有一个固定的百分比标准,而是取决于具体的垃圾回收器(GC)实现和当前的内存分配需求。

  • 常见触发场景:

    • 有对象要晋升到老年代,但老年代剩余空间不足以容纳这些对象。
    • 直接在老年代分配大对象时,空间不足。
    • 某些 GC(如 CMS)可以通过参数设置阈值,比如 -XX:CMSInitiatingOccupancyFraction=75,表示老年代使用率达到 75% 时,提前启动 CMS GC。

不同 GC 策略下的行为

  • Serial/Parallel/G1 GC:通常在需要分配对象到老年代时,如果空间不足就会触发 Major GC。
  • CMS GC:可以通过参数设置老年代使用率阈值,达到阈值就会并发启动回收(如上所述)。
  • G1 GC:会根据整体堆的使用情况和预测的停顿时间动态决定是否回收老年代。

总结

  • 老年代空间不足不是指达到某个固定百分比,而是指无法满足分配需求时就会触发 Major GC。
  • 某些 GC 可以通过参数设置“使用率阈值”来提前启动回收,但本质上都是为了防止“空间不足”导致分配失败。

大对象分配

一、什么是大对象(Large Object / Humongous Object)

  • 大对象通常指的是占用内存空间较大的对象,如大型数组、长字符串、图片缓存等。
  • JVM 为了避免大对象在新生代频繁复制(Minor GC 时 Survivor 区之间的复制),会选择直接在老年代分配大对象

二、如何判断一个对象是大对象?

  • JVM 通过对象的大小来判断是否为大对象。

  • 判断标准:

    • HotSpot JVM(默认 GC) :对象大小超过新生代 Eden 区剩余空间时,会直接在老年代分配。
    • G1 GC:对象大小超过 Region 大小的一半(默认 Region 大小为 1~32MB,通常 1MB),即大于 512KB 的对象会被认为是 Humongous Object,直接分配到老年代的 Humongous 区域。
    • 具体阈值可以通过 JVM 参数调整(如 -XX:G1HeapRegionSize)。

三、对象分配在 Eden 还是老年代的规则

1. 普通对象分配流程

  • 绝大多数对象会优先分配在新生代的 Eden 区。
  • 只有在以下特殊情况下才会直接分配到老年代:

2. 直接分配到老年代的情况

  1. 大对象直接分配

    • 对象大小超过一定阈值(如 G1 GC 的 Humongous Threshold),直接分配到老年代。
  2. 长期存活对象晋升

    • 对象在 Survivor 区经过多次 Minor GC 幸存,年龄达到阈值(如 15),晋升到老年代。
  3. Survivor 区放不下存活对象

    • Minor GC 时,Survivor 区空间不足,部分对象直接晋升到老年代。
  4. Eden 区空间不足

    • Eden 区空间不足时,可能会触发 Minor GC,若仍无法分配,则尝试在老年代分配。

3. JVM 参数相关

  • -XX:PretenureSizeThreshold(仅限部分 GC,如 Parallel/Serial GC)
    超过该阈值的对象直接在老年代分配(单位:字节)。 例如:-XX:PretenureSizeThreshold=1048576(1MB)
  • G1 GC 没有 PretenureSizeThreshold,而是用 Region 大小的一半作为阈值。

四、总结

  • 大对象:通常指超过一定大小(如 512KB、1MB)的对象,JVM 会直接在老年代分配,避免新生代频繁复制。
  • 判断标准:由 JVM 参数和 GC 策略决定,G1 GC 以 Region 大小为基准,其他 GC 可用 -XX:PretenureSizeThreshold
  • 普通对象:优先分配在 Eden 区,只有大对象、长期存活对象、Survivor 区放不下时才会直接分配到老年代。

GC 风暴

GC风暴(GC Storm)是指JVM 垃圾回收(GC)频繁发生,且每次回收都伴随较长的停顿,导致应用性能严重下降甚至不可用的现象。


主要表现

  • GC 日志中频繁出现 Minor GC、Major GC 或 Full GC。
  • 每次 GC 停顿时间较长,应用响应变慢,甚至出现超时、卡死。
  • CPU 占用率升高,系统负载异常。
  • 业务线程大部分时间都在等待 GC,实际业务处理能力大幅下降。

常见原因

  • 内存泄漏:对象无法被回收,堆空间持续增长,频繁触发 GC。
  • 堆设置过小:内存不够用,导致频繁 GC。
  • 对象创建过多:短时间内大量创建对象,垃圾产生速度远超回收速度。
  • 大对象频繁分配:频繁直接在老年代分配大对象,导致老年代很快被填满。
  • 晋升失败:新生代对象大量晋升到老年代,老年代空间不足,频繁 Full GC。
  • GC 参数设置不合理:如 Survivor 区过小、晋升阈值过低等。

危害

  • 应用长时间不可用或极度卡顿。
  • 可能导致服务被健康检查摘除、自动重启,甚至 OOM 崩溃。

解决思路

  • 优化代码,减少对象创建和内存泄漏。
  • 合理调整 JVM 堆大小和 GC 参数。
  • 监控和分析 GC 日志,定位问题根源。
  • 必要时升级 GC 策略(如使用 G1、ZGC 等低停顿 GC)。