《深入解析JVM》第二章:垃圾回收机制和GC算法

本期内容为自己总结归档,基于JDK8,共分5章,本人遇到过的面试问题会重点标记

第一章:JVM架构全览

第二章:垃圾回收机制和GC算法

第三章:JVM类加载与Spring类加载

第四章:JVM 调优

第五章:最新的JDK25、JDK21带来的优化

(若有任何疑问,可在评论区告诉我,看到就回复)

第二章:垃圾回收机制和GC算法

1. 什么是“垃圾”?—— 对象的生死判定

在JVM中,“垃圾”指的是堆内存中不再被任何途径使用的对象。这些对象占用着宝贵的内存空间却已失效,JVM需要自动识别并回收它们。判断对象是否为“垃圾”是GC的第一步,主要依赖以下两种算法。

1.1 引用计数法(Reference Counting)

  • 工作原理:在对象头中添加一个引用计数器。每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。任何时刻计数器为0的对象即为“垃圾”。

  • 优点:原理简单,判定效率高。

  • 致命缺点无法解决对象之间循环引用的问题。例如,两个对象ab相互引用(a.instance = b; b.instance = a),即使它们已与外界断绝联系(a = null; b = null),其引用计数也永不为0,导致内存泄漏。因此,主流的Java虚拟机均不采用此算法。

1.2 可达性分析算法(Reachability Analysis)

这是当前主流JVM(包括HotSpot)采用的判定算法

  • 核心思想:以一系列称为 “GC Roots” 的根对象为起始点,向下搜索走过的路径称为“引用链”(Reference Chain)。如果一个对象到GC Roots间没有任何引用链相连,则证明此对象不可用,即可被回收。

GC Roots对象通常包括以下几类

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象,如当前正在运行的方法中的参数和局部变量。

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

  • 方法区中类静态属性引用的对象(static变量)。

  • 方法区中常量引用的对象(如字符串常量池中的引用)。

下图清晰地展示了可达性分析的过程与结果:

如图,从GC Roots出发,对象A、B、C、D、E都存在引用链,因此是可达的、存活的对象。而对象F和G虽互相引用,但无法从GC Roots到达,形成“孤岛”,是不可达的垃圾对象

1.3 对象回收的“缓刑”与 finalize() 方法

        即使被可达性分析判定为不可达,对象也并非“非死不可”。它会被第一次标记并进行一次筛选:如果该对象没有覆盖finalize()方法,或finalize()方法已被虚拟机调用过,则虚拟机将其视为“没有必要执行”,直接回收。

        反之,如果对象被判定为需要执行finalize()方法,它会被放入一个名为F-Queue的队列中。稍后,由一条低优先级的Finalizer线程去触发(并不保证等待其运行结束)该方法。finalize()方法是对象逃脱死亡的最后一次机会——对象只需在方法中重新与引用链上的任一对象建立关联即可“自救”。但finalize()方法运行代价高昂、不确定性大,且无法保证调用顺序,强烈不推荐在生产中依赖此方法,finalize() 机制已被官方标记为 deprecated(JDK9+)

1.4 理解对象的引用强度

Java中引用并非只有“有”或“无”两种状态,根据生命周期的不同,分为四类,这直接影响垃圾回收的行为:

引用类型被垃圾回收的时机常见用途与说明
强引用 (Strong)永不回收(即使OOM)最常见的引用,如 Object obj = new Object()
软引用 (Soft)内存不足,即将发生OOM前被回收适合实现内存敏感的缓存,如网页缓存、图片缓存。
弱引用 (Weak)无论内存是否充足,下一次GC时必被回收同样可用于缓存,但生命周期更短,如 WeakHashMap 的键引用。
虚引用 (Phantom)随时可能被回收,完全不影响对象生命周期唯一目的:对象被回收时收到一个系统通知,用于监控回收行为。

2. 如何回收垃圾?—— 经典垃圾回收算法

确定了垃圾对象后,JVM需要采用具体算法进行内存回收。以下是三种最基础的算法思想。

算法核心过程优点缺点适用场景
标记-清除 (Mark-Sweep)1. 标记所有可达对象。
2. 清除未标记的垃圾对象。
实现简单,不移动对象。1. 内存碎片化严重。
2. 标记和清除效率都不高。
早期收集器,或作为CMS等收集器在老年代的备选方案。
标记-复制 (Copying)1. 将内存分为大小相等的两块(From, To)。
2. 只使用From区,GC时将存活对象复制到To区
3. 清空整个From区,并交换两者角色。
1. 实现简单,运行高效。
2. 无内存碎片
浪费50%的内存空间新生代的典型算法。因新生代对象“朝生夕死”,存活对象少,复制成本低。
标记-整理 (Mark-Compact)1. 标记所有可达对象。
2. 将所有存活对象向内存一端移动(整理)
3. 清理掉边界外的内存
1. 无内存碎片
2. 不浪费空间。
移动存活对象成本高,需要更新引用,且会触发“Stop-The-World”。老年代的常用算法。因老年代对象存活率高,不适合复制算法。

3. 为何要分代?—— 分代收集理论

基于程序运行中对象的生命周期呈现出的统计学规律,分代收集理论应运而生,并成为现代垃圾收集器的设计基石。

  • 弱分代假说 (Weak Generational Hypothesis)绝大多数对象都是“朝生夕灭”的,即生命周期极短。

  • 强分代假说 (Strong Generational Hypothesis)熬过越多次垃圾收集的对象,就越难以消亡,即生命周期越长。

  • 跨代引用假说 (Intergenerational Reference Hypothesis):跨代引用(如老年代对象引用新生代对象)相对同代引用来说仅占极少数

基于以上假说,JVM将Java堆划分为新生代 (Young Generation) 和老年代 (Old Generation),并对这两块区域采用不同的回收策略和算法,以最大化GC效率。

3.1 新生代与Minor GC

新生代是对象诞生的地方,通常占据堆的较小部分(如1/3)。它内部又被进一步细分:

  • Eden区新创建的对象首先在此分配。

  • Survivor区 (From / To):两个大小相等的幸存者区,用于存放在Minor GC中存活下来的对象

新生代GC(Minor GC/Young GC)的“复制算法”工作流程

  1. 对象分配:绝大多数新对象在Eden区分配。

  2. 触发GC:当Eden区满时,触发一次Minor GC

  3. 复制与清除:将Eden区和其中一个Survivor(假设为From区)中所有存活的对象,一次性复制到另一个空闲的Survivor区(To区),并清空Eden和From区。在此过程中,对象每经历一次Minor GC,其“年龄”就增加1岁。

  4. 晋升与交换:当某个对象的年龄增长到一定程度(默认为15岁,可通过 -XX:MaxTenuringThreshold 设置),或To区空间不足以容纳本次所有存活对象时,对象会被晋升(Promotion)到老年代。完成后,From区和To区角色互换

⭐面试高频考点:新生代中的对象进入老年代有几种方式?共4种

  1. 年龄阈值晋升
    对象在 Survivor 区每熬过一次 MinorGC,年龄 +1;达到 -XX:MaxTenuringThreshold(默认 15)就进入老年代。
    这是最“规矩”的路线。

  2. 动态年龄判定
    如果 Survivor 区里相同年龄的所有对象大小总和 ≥ Survivor 空间的一半,则≥ 该年龄的全部对象直接晋升,无需等到 15 岁。
    目的:防止 Survivor 装不下而造成频繁拷贝。

  3. 老年代担保失败(HandlePromotionFailure)
    MinorGC 前,JVM 会检查“老年代最大可用连续空间”是否 ≥ 历次晋升平均大小;若不够,即使年龄未到,也会把新生代全部活对象提前搬进老年代。
    这就是日志里常见的 “promotion failed” → 触发 Full GC 的根源。

  4. 大对象直接进入老年代

    对象大小 ≥ -XX:PretenureSizeThreshold(默认 0,单位 byte,需手动设,如 4M)时,不在新生代分配,直接在老年代生成,避免 Eden/Survivor 来回拷贝。
    注意:该参数只对 Serial 和 ParNew 有效;G1 下需用 -XX:G1HeapRegionSize 控制巨型对象(Humongous)阈值。

3.2 老年代与Major GC / Full GC

老年代存放生命周期长的对象和大对象。

  • 老年代GC (Major GC/Old GC):指只针对老年代的垃圾收集。目前只有CMS等少数收集器有这种模式。

  • 整堆GC (Full GC):指收集整个Java堆(包括新生代、老年代)和方法区(元空间) 的垃圾收集。Full GC通常速度比Minor GC慢10倍以上,是导致应用长时间停顿(Stop-The-World)的主因,应极力避免其频繁发生

4. 谁来做回收?—— 经典垃圾收集器详解

垃圾收集器是上述算法的具体实现。在JDK 8中,存在多款经典的收集器,它们可以按特定组合搭配工作。

4.1 串行与并行收集器

收集器特点应用场景
Serial / Serial Old单线程工作,进行GC时,必须暂停所有用户线程(STW)客户端模式或资源受限的嵌入式环境。
ParNew本质是Serial的多线程并行版本,其余行为与Serial相同。Server模式下的新生代首选,主要因其是唯一能与CMS收集器配合工作的新生代收集器。
Parallel Scavenge / Parallel OldJDK 8的默认收集器组合。多线程并行,核心目标是达到一个可控制的吞吐量(Throughput) 。适合后台运算、无需太多交互,且对吞吐量有高要求的任务。

4.2 并发低延迟收集器

CMS (Concurrent Mark-Sweep)

CMS是一款以获取最短回收停顿时间为目标的老年代收集器,首次实现了GC线程与用户线程大部分时间同时工作

  • 工作原理(四个阶段)

    1. 初始标记 (Initial Mark)STW。仅标记GC Roots直接关联的对象,速度极快。

    2. 并发标记 (Concurrent Mark)并发执行。从直接关联对象开始,遍历整个对象图,耗时较长但无需停顿。

    3. 重新标记 (Remark)STW。修正并发标记期间,因用户线程运行而产生变动的标记记录,比初始标记稍长。

    4. 并发清除 (Concurrent Sweep)并发执行。清除死亡对象。

  • 优点低停顿,提升应用响应速度。

  • 缺点

    • 对CPU资源敏感(并发阶段占用线程)。

    • 无法处理“浮动垃圾”(并发阶段新产生的垃圾)。

    • 基于标记-清除算法,会产生内存碎片。可能导致触发Full GC进行内存压缩。

G1 (Garbage-First)

G1是面向服务端应用的全堆收集器,是JDK 9及之后的默认收集器。它标志着收集器设计从分代模型转向分区(Region)模型

  • 核心设计:将整个堆划分为多个大小固定(1M-32M)的Region。每个Region可扮演Eden、Survivor、Old或特殊的Humongous(大对象)区角色。G1跟踪各个Region的垃圾堆积“价值”(回收所得空间及耗时),在后台维护一个优先列表。

  • 工作模式:G1并非每次都对整个堆进行回收,而是在可预测的停顿时间模型(通过 -XX:MaxGCPauseMillis 设置) 下,优先回收垃圾价值最大的Region,从而达到“化整为零”的收集效果。

  • 优点:兼顾了高吞吐和低停顿,能有效应对大内存场景。

  • 缺点:内存占用(Remembered Set)和CPU负载略高于CMS,调优相对复杂。

5. 本章小结

核心知识点关键内容与演进
垃圾判定主流的可达性分析算法,从GC Roots(栈、静态变量、常量等)出发,不可达的对象即为垃圾。
回收算法三类基础算法:标记-清除(碎片化)、标记-复制(适合新生代)、标记-整理(适合老年代)。
分代理论基于对象生存周期的统计学规律(弱/强分代假说),将堆分为新生代老年代,采用不同回收策略以优化效率。
收集器发展从追求吞吐的 Parallel Scavenge/Old(JDK8默认),到追求低延迟的 CMS,再到分区化、可预测停顿的 G1(JDK9+默认),体现了从“怎么收得快”到“怎么让用户感觉不到在收”的设计哲学演进。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值