--- title: "12-垃圾回收与GC算法" created: 2025-11-26 tags: - 博客 aliases: - 垃圾回收与GC算法 --- # 垃圾回收与GC算法 ## **核心概念** > **垃圾回收(Garbage Collection, GC)** 是 JVM 自动管理内存的机制,通过「**判断对象是否存活 → 选择回收算法 → 使用垃圾收集器**」三个步骤,自动释放不再使用的对象占用的内存,避免内存泄漏和溢出。 > **🎯 核心公式**: > > GC 性能 = f(吞吐量, 停顿时间, 内存占用) > > 最优解:根据应用场景选择合适的收集器组合 --- ## **一、为什么需要垃圾回收?** ### **1.1 问题背景(没有 GC 会怎样)** - **内存泄漏**:不再使用的对象无法释放,最终导致 `OutOfMemoryError` - **野指针问题**:手动释放后仍被引用,访问已释放内存导致崩溃(C/C++ 常见) - **双重释放**:同一块内存被释放多次,破坏内存管理结构 - **开发效率低**:程序员需要手动管理内存的分配和释放 ### **1.2 设计目标** | **目标** | **说明** | **权衡** | | --- | --- | --- | | **吞吐量** | 运行用户代码时间 / 总运行时间 | 与停顿时间冲突 | | **低延迟** | 减少 GC 停顿时间(STW) | 可能降低吞吐量 | | **内存占用** | 堆内存和 GC 元数据占用 | 空间换时间 | **不可能三角**:无法同时优化三个目标,必须根据场景选择侧重点。 --- ## **二、对象存活判定算法** ### **2.1 引用计数法(Reference Counting)❌** #### **原理** 为每个对象维护一个引用计数器: - 被引用时 `+1` - 引用失效时 `-1` - 计数为 `0` 时回收 #### **优缺点** | **优点** | **缺点** | | --- | --- | | ✅ 实现简单 | ❌ **无法解决循环引用**(致命缺陷) | | ✅ 实时性好,垃圾立即回收 | ❌ 每次引用变化都要修改计数器(性能开销) | | ✅ 没有 STW | ❌ 计数器本身占用额外内存 | #### **循环引用问题** ```java public class CircularReference { Object instance = null; public static void main(String[] args) { CircularReference objA = new CircularReference(); CircularReference objB = new CircularReference(); objA.instance = objB; // A 引用 B objB.instance = objA; // B 引用 A objA = null; // 外部引用断开 objB = null; // 但 A 和 B 互相引用,计数器永远不为 0 // 引用计数法无法回收,导致内存泄漏! } } ``` **结论**:主流 JVM(HotSpot)**不使用**引用计数法,而使用可达性分析。 --- ### **2.2 可达性分析算法(Reachability Analysis)✅** #### **原理** 从 **GC Roots** 出发,通过引用链向下搜索: - **可达对象**:存在从 GC Roots 到该对象的引用链 → **存活** - **不可达对象**:不存在任何引用链 → **可回收** ``` GC Roots ↓ [对象A] → [对象B] → [对象C] ← 可达(存活) [对象X] → [对象Y] ← 不可达(垃圾) ``` #### **GC Roots 包括哪些?** | **GC Roots 类型** | **说明** | **示例** | | --- | --- | --- | | **虚拟机栈中引用的对象** | 方法局部变量、参数 | `void method() { User user = new User(); }` | | **方法区中类静态属性引用** | 类的 `static` 字段 | `public static User user;` | | **方法区中常量引用** | `final static` 常量 | `public static final String NAME = "abc";` | | **本地方法栈中 JNI 引用** | Native 方法引用的对象 | `native void nativeMethod();` | | **活跃线程** | 正在运行的线程对象 | `Thread.currentThread()` | | **被 Synchronized 锁持有的对象** | 锁对象 | `synchronized(obj)` | #### **完整回收流程** ``` 1. 标记阶段(Marking Phase) ├─ 从 GC Roots 开始遍历对象图 ├─ 标记所有可达对象 └─ 未被标记的对象 → 不可达对象 2. 第一次标记 + 筛选 ├─ 对象是否重写了 finalize() 且未被调用过? │ ├─ 是 → 加入 F-Queue,等待 Finalizer 线程执行 │ └─ 否 → 直接回收 3. 第二次标记(finalize 执行后) └─ 如果对象在 finalize() 中重新建立引用链 → 逃脱回收 否则 → 真正回收 ``` #### **finalize() 自救机制(不推荐使用)** ```java public class FinalizeEscape { public static FinalizeEscape SAVE_HOOK = null; public void isAlive() { System.out.println("我还活着"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize() 被执行"); // 自救:重新建立引用链 FinalizeEscape.SAVE_HOOK = this; } public static void main(String[] args) throws InterruptedException { SAVE_HOOK = new FinalizeEscape(); // 第一次自救成功 SAVE_HOOK = null; System.gc(); Thread.sleep(500); // 等待 Finalizer 线程执行 if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); // 输出:我还活着 } // 第二次自救失败(finalize 只会被调用一次) SAVE_HOOK = null; System.gc(); Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("我死了"); // 输出:我死了 } } } ``` **⚠️ 警告**: - `finalize()` 运行代价高、不确定性大、无法保证调用顺序 - Java 9 后已标记为 `@Deprecated` - 推荐使用 `try-with-resources` 或 `Cleaner` 机制 --- ### **2.3 四种引用类型** | **引用类型** | **回收时机** | **典型应用场景** | **创建方式** | | --- | --- | --- | --- | | **强引用(Strong)** | 永不回收(即使 OOM) | 普通对象引用 | `Object obj = new Object();` | | **软引用(Soft)** | 内存不足时回收 | 内存敏感的缓存(图片缓存) | `SoftReference ref = new SoftReference<>(obj);` | | **弱引用(Weak)** | 下次 GC 时回收 | ThreadLocal、WeakHashMap | `WeakReference ref = new WeakReference<>(obj);` | | **虚引用(Phantom)** | 随时可能回收,无法通过引用获取对象 | 对象回收跟踪、堆外内存管理 | `PhantomReference ref = new PhantomReference<>(obj, queue);` | #### **实战案例:软引用实现缓存** ```java public class ImageCache { private Map> cache = new HashMap<>(); public Image getImage(String path) { SoftReference ref = cache.get(path); // 检查软引用是否被回收 if (ref != null) { Image img = ref.get(); if (img != null) { return img; // 缓存命中 } } // 缓存失效,重新加载 Image img = loadImageFromDisk(path); cache.put(path, new SoftReference<>(img)); return img; } } ``` --- ## **三、垃圾回收算法** ### **3.1 算法对比总览** | **算法** | **核心思想** | **优点** | **缺点** | **适用区域** | | --- | --- | --- | --- | --- | | **标记-清除** Mark-Sweep | 标记垃圾对象,然后清除 | 简单,不移动对象 | **内存碎片**严重 | 老年代(CMS) | | **标记-复制** Mark-Copy | 将存活对象复制到另一块区域 | **无碎片**,分配快 | **浪费空间**(需要额外空间) | 新生代 | | **标记-整理** Mark-Compact | 标记后移动对象到一端 | 无碎片,空间利用率高 | **移动对象成本高** | 老年代(Serial Old、Parallel Old) | | **分代收集** Generational | 根据对象生命周期分代管理 | 综合利用各算法优势 | 实现复杂 | 现代 JVM 标配 | --- ### **3.2 标记-清除算法(Mark-Sweep)** #### **工作流程** ``` 执行前: [对象A][对象B][对象C][对象D][对象E] ✓ × ✓ × ✓ (存活) (垃圾) (存活) (垃圾) (存活) 执行后: [对象A][空闲][对象C][空闲][对象E] ↑碎片 ↑碎片 ``` #### **两个阶段** 1. **标记阶段**:从 GC Roots 出发,标记所有可达对象 2. **清除阶段**:遍历堆,回收所有未被标记的对象 #### **问题** - ❌ **内存碎片**:频繁回收后,空闲内存不连续,大对象无法分配 - ❌ **效率问题**:标记和清除两个阶段效率都不高 - ❌ **空间局部性差**:存活对象分散,缓存命中率低 **改进版**:CMS 收集器使用此算法,通过并发标记减少停顿。 --- ### **3.3 标记-复制算法(Mark-Copy)** #### **工作流程(Appel 式回收)** ``` 新生代分为:Eden(80%) + Survivor0(10%) + Survivor1(10%) Minor GC 前: Eden S0 S1 [对象ABCDE] [空] [空] ↓ 只有 A、C 存活 Minor GC 后: Eden S0 S1 [空] [空] [对象AC] ← 复制到 S1 ``` #### **优缺点** | **优点** | **缺点** | | --- | --- | | ✅ **无内存碎片** | ❌ **浪费空间**(需要预留复制区域) | | ✅ **分配简单**:只需移动指针(bump-the-pointer) | ❌ 存活对象多时效率低 | | ✅ 适合"朝生夕死"的对象 | ❌ 需要额外空间作为担保(Survivor 不够时进入老年代) | #### **为什么新生代使用 8:1:1 的比例?** **IBM 研究表明**:新生代中 98% 的对象是"朝生夕死"的,所以: - **Eden 占 80%**:大部分对象分配在 Eden - **Survivor 占 10% × 2**:只有少量对象存活,10% 足够 - **空间利用率 = 90%**(Eden + 1 个 Survivor) **极端情况处理**: ```java if (Survivor 空间不足) { // 担保机制:直接进入老年代 进入老年代 (Old Generation); } ``` --- ### **3.4 标记-整理算法(Mark-Compact)** #### **工作流程** ``` 执行前: [对象A][对象B][对象C][对象D][对象E] ✓ × ✓ × ✓ 执行后(压缩): [对象A][对象C][对象E][---空闲空间---] ← 所有存活对象移到一端 ``` #### **两个阶段** 1. **标记阶段**:同标记-清除 2. **整理阶段**:将所有存活对象向内存一端移动,清理边界外的内存 #### **优缺点** | **优点** | **缺点** | | --- | --- | | ✅ 无内存碎片 | ❌ **移动对象成本高**(需要更新所有引用) | | ✅ 空间利用率高 | ❌ **STW 时间长**(移动期间必须暂停用户线程) | | ✅ 适合老年代(对象存活率高) | ❌ 效率低于标记-清除 | #### **优化:滑动式整理(Lisp2 算法)** 1. 标记阶段:标记所有存活对象 2. 计算阶段:计算每个对象的新地址 3. 更新引用:更新所有指向移动对象的引用 4. 移动对象:将对象移动到新地址 --- ### **3.5 分代收集算法(Generational Collection)⭐** #### **核心思想** 根据对象生命周期特点,将堆分为不同区域,使用不同的回收算法: ``` Java 堆内存结构(JDK 8) ┌─────────────────────────────────────────────────────┐ │ 新生代 (Young Generation) 1/3 │ │ ┌────────┬──────┬──────┐ │ │ │ Eden │ S0 │ S1 │ ← 标记-复制算法 │ │ │ 8 │ 1 │ 1 │ │ │ └────────┴──────┴──────┘ │ ├─────────────────────────────────────────────────────┤ │ 老年代 (Old Generation) 2/3 │ │ ┌──────────────────────────────────────┐ │ │ │ 标记-清除 或 标记-整理算法 │ │ │ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ 元空间 (Metaspace) - 本地内存 ``` #### **分代假说(Generational Hypothesis)** | **假说** | **内容** | **应对策略** | | --- | --- | --- | | **弱分代假说** | 绝大多数对象都是朝生夕死 | 新生代频繁 GC,使用复制算法 | | **强分代假说** | 熬过多次 GC 的对象越难消亡 | 老年代较少 GC,使用标记-整理 | | **跨代引用假说** | 跨代引用相对于同代引用来说占极少数 | 使用记忆集(Remembered Set)避免全堆扫描 | #### **对象晋升规则** ```java // 规则1:年龄达到阈值(默认 15 次 Minor GC) -XX:MaxTenuringThreshold=15 // 规则2:大对象直接进入老年代 -XX:PretenureSizeThreshold=1M // 超过 1M 直接进老年代 // 规则3:动态年龄判定 // 如果 Survivor 中相同年龄对象大小总和 > Survivor 空间的一半 // 则年龄 >= 该年龄的对象直接进入老年代 // 规则4:空间分配担保 // Minor GC 前检查:老年代最大可用连续空间 > 新生代所有对象总大小 // 不满足则触发 Full GC ``` #### **Minor GC vs Full GC** | **GC 类型** | **触发时机** | **回收区域** | **停顿时间** | **频率** | | --- | --- | --- | --- | --- | | **Minor GC** | Eden 区满 | 新生代(Eden + Survivor) | 短(几十毫秒) | 频繁 | | **Major GC** | 老年代满 | 老年代 | 长 | 较少 | | **Full GC** | ① System.gc() ② 老年代空间不足 ③ 元空间不足 ④ Minor GC 晋升担保失败 | 整个堆 + 元空间 | 很长(秒级) | 很少 | ## **四、垃圾收集器详解** ### **4.1 收集器演进历史** ``` 时间线: 1999 ─────→ 2004 ─────→ 2012 ─────→ 2017 ─────→ 2018 Serial CMS G1 ZGC Shenandoah Parallel (JDK 9 默认) (JDK 11) (JDK 12) ``` ### **4.2 七大经典收集器总览** ``` 新生代收集器 老年代收集器 ┌──────────┐ ┌──────────┐ │ Serial │───────→│Serial Old│ └──────────┘ └──────────┘ ┌──────────┐ ┌──────────┐ │ ParNew │───────→│ CMS │ └──────────┘ └──────────┘ ┌──────────┐ ┌──────────┐ │ Parallel │───────→│Parallel │ │ Scavenge │ │ Old │ └──────────┘ └──────────┘ 跨代收集器 ┌──────────┐ │ G1 │ ← JDK 9+ 默认 └──────────┘ ┌──────────┐ │ ZGC │ ← 低延迟(< 10ms) └──────────┘ ┌──────────┐ │Shenandoah│ ← 低延迟 └──────────┘ ``` --- ### **4.3 Serial / Serial Old 收集器** #### **特点** - **单线程**:只用一个线程进行 GC - **Stop-The-World**:GC 时必须暂停所有用户线程 - **新生代**:标记-复制算法 - **老年代**:标记-整理算法 #### **工作流程** ``` 用户线程 ────────┐ STW ┌───────── │ │ ▼ ▼ [Serial GC 线程执行] 时间: 0ms 50ms 100ms ``` #### **JVM 参数** ``` # 新生代使用 Serial -XX:+UseSerialGC # 查看使用的收集器 java -XX:+PrintCommandLineFlags -version ``` #### **适用场景** - ✅ **单核 CPU 环境** - ✅ **客户端模式**(桌面应用) - ✅ **几十 MB 的小堆** - ✅ **对停顿时间不敏感** **优点**:简单高效,单线程时没有线程交互开销,适合小应用。 --- ### **4.4 ParNew 收集器** #### **特点** - **Serial 的多线程版本** - **新生代收集器**,必须配合 CMS 使用 - **标记-复制算法** #### **工作流程** ``` 用户线程 ────────┐ STW ┌───────── │ │ ▼ ▼ [GC线程1][GC线程2][GC线程3][GC线程4] └────────并行执行────────┘ ``` #### **JVM 参数** ``` # 启用 ParNew(JDK 8 默认配合 CMS) -XX:+UseParNewGC # 设置并行 GC 线程数(默认 = CPU 核心数) -XX:ParallelGCThreads=4 ``` #### **适用场景** - ✅ **多核 CPU 服务器** - ✅ **配合 CMS 使用**(CMS 只能与 ParNew 或 Serial 配合) - ✅ **新生代对象多、GC 频繁** --- ### **4.5 Parallel Scavenge / Parallel Old 收集器⭐** #### **特点(吞吐量优先)** - **新生代**:标记-复制(Parallel Scavenge) - **老年代**:标记-整理(Parallel Old) - **多线程并行** - **自适应调节策略**(GC Ergonomics) #### **核心目标:最大化吞吐量** ``` 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC 时间) ``` 例如: - 程序运行 100 分钟,GC 耗时 1 分钟 - 吞吐量 = 99 / 100 = 99% #### **JVM 参数(关键配置)** ``` # 启用 Parallel 收集器(JDK 8 Server 模式默认) -XX:+UseParallelGC # 新生代 -XX:+UseParallelOldGC # 老年代(自动启用) # 吞吐量控制(核心参数) -XX:GCTimeRatio=99 # 吞吐量目标 = 99%(GC 时间占比 1%) -XX:MaxGCPauseMillis=200 # 最大停顿时间目标(毫秒) # 自适应调节(推荐开启) -XX:+UseAdaptiveSizePolicy # 自动调整 Eden、Survivor、晋升阈值 # 并行线程数 -XX:ParallelGCThreads=8 # 默认 = CPU 核心数 ``` #### **自适应调节策略示例** ```java // JVM 会自动调整以下参数: -Xmn # 新生代大小 -XX:SurvivorRatio # Eden 和 Survivor 比例 -XX:MaxTenuringThreshold # 对象晋升年龄阈值 // 目标:在满足停顿时间的前提下,最大化吞吐量 ``` #### **适用场景** - ✅ **后台计算任务**(批处理、数据分析) - ✅ **科学计算、大数据处理** - ✅ **对停顿时间不敏感,追求吞吐量** - ✅ **多核服务器** --- ### **4.6 CMS 收集器(Concurrent Mark Sweep)⭐** #### **特点(低延迟优先)** - **老年代收集器** - **标记-清除算法** - **并发标记**(大部分时间与用户线程并发执行) - **目标**:最短 GC 停顿时间 #### **四个阶段(核心)** ``` 1. 初始标记 (Initial Mark) - STW ⚡ └─ 标记 GC Roots 直接关联的对象(速度快) 2. 并发标记 (Concurrent Mark) - 并发执行 🚀 └─ 从 GC Roots 遍历整个对象图(耗时最长) 3. 重新标记 (Remark) - STW ⚡ └─ 修正并发标记期间用户程序运行导致的标记变动 4. 并发清除 (Concurrent Sweep) - 并发执行 🚀 └─ 清除标记为垃圾的对象 ``` #### **时间线图** ``` 用户线程 ████████████████████████████████████████ GC 线程 ██ ██████████ ██████ ↑ STW ↑ 并发 ↑ STW ↑ 并发 初始标记 并发标记 重新标记 并发清除 ``` #### **JVM 参数** ``` # 启用 CMS(JDK 9 后弃用,JDK 14 移除) -XX:+UseConcMarkSweepGC # 触发阈值(老年代使用 68% 时触发 GC) -XX:CMSInitiatingOccupancyFraction=68 -XX:+UseCMSInitiatingOccupancyOnly # 只使用设定阈值 # 并发线程数(默认 = (CPU核心数 + 3) / 4) -XX:ConcGCThreads=2 # 碎片整理 -XX:+UseCMSCompactAtFullCollection # Full GC 时整理碎片 -XX:CMSFullGCsBeforeCompaction=5 # 5 次 Full GC 后整理一次 ``` #### **三大缺点** | **缺点** | **说明** | **解决方案** | | --- | --- | --- | | **CPU 资源敏感** | 并发阶段占用 CPU 导致应用变慢 | 增量式 CMS(已弃用) | | **浮动垃圾** | 并发清除时产生的新垃圾无法清理 | 预留足够空间,降低触发阈值 | | **内存碎片** | 标记-清除算法导致碎片 | 定期 Full GC 整理 | #### **浮动垃圾问题** ```java // 并发标记阶段: 对象 A 被标记为存活 ↓ // 用户线程继续运行: 对象 A 的引用被清除(A 变成垃圾) ↓ // 但本次 GC 不会清理 A(浮动垃圾) // 需要等到下次 GC 才能回收 ``` #### **Concurrent Mode Failure** ``` # 现象: Java HotSpot(TM) 64-Bit Server VM warning: Concurrent mode failure # 原因: - 并发清除时,老年代空间不足以容纳新晋升的对象 - 触发 Full GC(使用 Serial Old 收集器) # 解决: -XX:CMSInitiatingOccupancyFraction=70 # 提前触发 GC -Xmx4G # 增大堆内存 ``` #### **适用场景** - ✅ **互联网应用**(要求低延迟) - ✅ **B/S 架构服务端** - ✅ **注重用户体验的应用** - ✅ **老年代空间充足** ### **4.7 G1 收集器(Garbage First)⭐⭐⭐** #### **核心创新(革命性变化)** - **不再区分新生代/老年代**(逻辑上仍存在) - 堆划分为多个**Region**(每个 Region 大小相等,1-32MB) - **优先回收价值最大的 Region**(Garbage First 的由来) - **可预测的停顿时间模型** - #### **Region 结构** - ``` Java 堆(G1 视角) ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ E │ E │ S │ O │ O │ H │ E │ O │ └────┴────┴────┴────┴────┴────┴────┴────┘ Eden Survivor Old Humongous(大对象) ``` 每个 Region: - 大小相等(1MB、2MB、4MB...32MB) - 动态角色:可以是 Eden、Survivor、Old、Humongous - 大对象(≥ Region 50%)直接分配到 Humongous Region - #### **四个阶段** - ``` 1. 初始标记 (Initial Marking) - STW └─ 标记 GC Roots 直接关联对象 └─ 借用 Minor GC 的 STW,几乎无额外停顿 2. 并发标记 (Concurrent Marking) - 并发 └─ 从 GC Roots 遍历对象图 └─ 耗时最长,但与用户线程并发 3. 最终标记 (Final Marking) - STW └─ 处理并发阶段遗留的标记变动 └─ 使用 SATB(Snapshot-At-The-Beginning)算法 4. 筛选回收 (Live Data Counting and Evacuation) - STW └─ 根据停顿时间目标,选择价值最大的 Region └─ 将存活对象复制到空 Region,清理旧 Region ``` - #### **关键技术** ##### **1. 记忆集(Remembered Set, RSet)** **问题**:如何避免全堆扫描来处理跨 Region 引用? ``` Region A Region B ┌─────────┐ ┌─────────┐ │ 对象1 │──────→│ 对象2 │ ← 跨 Region 引用 └─────────┘ └─────────┘ ``` Region B 的 RSet 记录: “Region A 中的对象1 引用了我” 回收 Region B 时: 只需扫描 Region B 的 RSet,无需扫描整个堆 - **实现**: 每个 Region 维护一个 RSet 使用**写屏障**(Write Barrier)记录跨 Region 引用 RSet 占用堆内存的 5% 左右 - ##### **2. SATB 算法(解决并发标记时的对象消失问题)** ```java // 问题场景(三色标记): 黑色对象(已标记完成) ↓ 添加引用 白色对象(未标记)← 应该存活但会被误回收 ↑ 删除引用 灰色对象(标记中) // SATB 解决方案: 记录"初始快照",认为快照时刻的对象都是活的 → 宁可错杀(浮动垃圾),不可漏标 ``` - ##### **3. 停顿预测模型** ```java // G1 维护统计信息: - 每个 Region 的回收耗时 - 每个 Region 的垃圾占比 // 计算公式: 回收价值 = 垃圾字节数 / 回收耗时 // 选择策略: 在目标停顿时间内,优先选择价值最大的 Region ``` - #### **JVM 参数(核心配置)** ``` # 启用 G1(JDK 9+ 默认) -XX:+UseG1GC # ⭐ 最重要参数:设置停顿时间目标(毫秒) -XX:MaxGCPauseMillis=200 # 默认 200ms # Region 大小(1MB-32MB,必须是 2 的幂) -XX:G1HeapRegionSize=4M # 默认根据堆大小自动计算 # 触发并发标记的堆占用阈值 -XX:InitiatingHeapOccupancyPercent=45 # 堆使用 45% 时触发 # 新生代占比(动态调整) -XX:G1NewSizePercent=5 # 最小 5% -XX:G1MaxNewSizePercent=60 # 最大 60% # 混合 GC 参数 -XX:G1MixedGCCountTarget=8 # 混合 GC 最多执行 8 次 -XX:G1HeapWastePercent=10 # 可容忍的堆浪费百分比 ``` - #### **G1 的三种 GC 模式** - | **模式** | **触发时机** | **回收范围** | **停顿时间** | | --- | --- | --- | --- | | **Young GC** | Eden Region 满 | 所有 Eden + Survivor | 短(< 200ms) | | **Mixed GC** | 并发标记完成后 | 所有 Eden + Survivor + **部分 Old Region** | 中等 | | **Full GC** | ① Mixed GC 无法跟上分配速度 ② Humongous 对象分配失败 ③ 晋升失败 | 整个堆 | 长(秒级,⚠️ 需要避免) | - #### **典型日志分析** ``` # Young GC 日志 [GC pause (G1 Evacuation Pause) (young), 0.0234567 secs] [Parallel Time: 20.1 ms, GC Workers: 4] [Eden: 512M(512M)->0B(480M) Survivors: 32M->64M Heap: 1.2G->768M] # Mixed GC 日志 [GC pause (G1 Evacuation Pause) (mixed), 0.0456789 secs] [Eden: 480M->0B Old: 2G->1.8G] ← 回收了部分老年代 # Full GC 日志(需要优化) [Full GC (Allocation Failure) 4G->2.5G, 2.3456 secs] ``` - #### **何时会触发 Full GC?(需要避免)** ``` # 场景1:分配巨型对象失败 [Full GC (G1 Humongous Allocation)] # 场景2:晋升失败(老年代碎片化) [Full GC (G1 Evacuation Pause)] # 场景3:并发标记失败 [Full GC (Concurrent Mode Failure)] # 优化方案: -Xmx8G -Xms8G # 增大堆内存 -XX:G1HeapRegionSize=16M # 增大 Region 减少巨型对象 -XX:ConcGCThreads=4 # 增加并发标记线程 ``` - #### **适用场景** - ✅ **6GB+ 大堆内存** - ✅ **需要可预测的低延迟**(200ms 以内) - ✅ **堆中存活对象占比 50% 以下** - ✅ **对象分配速率变化大** - ✅ **不希望出现长时间 GC 停顿** - #### **与 CMS 的对比** - | **对比项** | **CMS** | **G1** | | --- | --- | --- | | **算法** | 标记-清除(碎片) | 标记-复制(整理,无碎片) | | **内存布局** | 连续的新生代/老年代 | Region 化(可预测停顿) | | **停顿可预测性** | ❌ 不可预测 | ✅ 可通过 `MaxGCPauseMillis` 控制 | | **Full GC** | 频繁(碎片导致) | 很少(除非参数不当) | | **适用堆大小** | < 4GB | > 6GB | | **JDK 默认** | JDK 8(配合 ParNew) | JDK 9+ | ### **4.8 ZGC 收集器(Z Garbage Collector)🚀** #### **革命性特点(JDK 11 实验,JDK 15 正式)** - **超低延迟**:停顿时间不超过 **10ms** - **支持 TB 级堆**(最大 16TB) - **并发执行所有阶段**(包括移动对象) - **单代收集器**(不分代) #### **核心技术** - ##### **1. 着色指针(Colored Pointers)** ``` 64 位指针布局(Linux x86-64): ┌─────────┬──────┬──────┬──────┬──────┬─────────────────────┐ │ 未使用 │Finalizable│Remapped│Marked1│Marked0│ 对象地址 (42 位) │ │ 16 位 │ 1 位 │ 1 位 │ 1 位 │ 1 位 │ 支持 4TB 堆 │ └─────────┴──────┴──────┴──────┴──────┴─────────────────────┘ ``` - 通过指针中的标志位,直接判断对象状态: - Marked0/Marked1:标记信息(双缓冲) - Remapped:是否已重定向到新地址 - Finalizable:是否只能通过 finalize 访问 - ##### **2. 读屏障(Load Barrier)** ```java // 普通对象访问: Object obj = fieldA; // ZGC 插入读屏障: Object obj = barrier(fieldA); // 自动处理对象移动 // 读屏障逻辑: if (指针颜色不对) { 修正指针 → 指向对象的新地址 } ``` ##### - ##### **3. 并发移动对象** ``` 并发重定向(Concurrent Relocation): 用户线程访问旧地址 ↓ 读屏障拦截 转发表查找新地址 ↓ 返回新地址的对象 ↓ 后台线程并发 逐步更新所有引用 ``` - #### **GC 阶段(全部并发)** ``` 1. 并发标记 (Concurrent Mark) └─ 标记存活对象 2. 并发预备重分配 (Concurrent Prepare for Relocate) └─ 统计需要移动的 Region 3. 并发重分配 (Concurrent Relocate) └─ 移动对象到新 Region 4. 并发重映射 (Concurrent Remap) └─ 修正所有指向旧对象的引用 ⚡ 仅在初始标记和初始重分配有短暂 STW(< 1ms) ``` - #### **JVM 参数** ``` # 启用 ZGC(JDK 15+) -XX:+UseZGC # 堆大小设置 -Xmx16G -Xms16G # 建议固定大小 # 并发 GC 线程数(默认自动计算) -XX:ConcGCThreads=4 # 不常用参数 -XX:ZCollectionInterval=120 # 强制 GC 间隔(秒) -XX:ZAllocationSpikeTolerance=2 # 分配速率峰值容忍度 ``` - #### **适用场景** ✅ **超低延迟要求**(金融交易、游戏服务器) ✅ **大堆内存**(几十 GB 到 TB 级) ✅ **JDK 15+** ⚠️ **吞吐量会降低 10-15%**(代价) --- ### **4.9 Shenandoah GC** #### **特点(与 ZGC 类似)** - **超低延迟**(< 10ms) - **并发整理**(包括并发移动对象) - **不分代** - **OpenJDK 独有**(Oracle JDK 不包含) #### **与 ZGC 的区别** - | **对比项** | **ZGC** | **Shenandoah** | | --- | --- | --- | | **并发移动技术** | 读屏障 + 着色指针 | **读写屏障** + 转发指针 | | **支持堆大小** | 16TB | 相对较小(几百 GB) | | **厂商** | Oracle 官方 | Red Hat 主导 | | **JDK 版本** | JDK 11+ | JDK 12+ | #### **JVM 参数** ``` # 启用 Shenandoah -XX:+UseShenandoahGC # 停顿时间目标(实验性) -XX:ShenandoahGCHeuristics=adaptive # 自适应 ``` --- ## **五、收集器选择策略** ### **5.1 决策树** ``` 堆内存大小? ↙ ↘ < 100MB > 100MB ↓ ↓ Serial GC 是否需要低延迟? ↙ ↘ 是 否 ↓ ↓ 堆大小? Parallel GC ↙ ↘ (吞吐量优先) < 6GB > 6GB ↓ ↓ CMS G1 / ZGC / Shenandoah (已弃用) (根据 JDK 版本和延迟要求) ``` ### 5.2 场景推荐表 | 应用场景 | 推荐收集器 | 关键参数 | JDK 版本 | | --- | --- | --- | --- | | **单核/客户端应用** | Serial GC | `-XX:+UseSerialGC` | All | | **多核/后台批处理** | Parallel GC | `-XX:+UseParallelGC` `-XX:GCTimeRatio=99` | All | | **Web 应用(堆 < 6GB)** | CMS(已弃用)→ G1 | `-XX:+UseConcMarkSweepGC` `-XX:+UseG1GC` `-XX:MaxGCPauseMillis=200` | JDK 8 | | **Web 应用(堆 > 6GB)** | G1 | `-XX:+UseG1GC` `-XX:MaxGCPauseMillis=200` `-XX:G1HeapRegionSize=16M` | JDK 9+ | | **大堆低延迟(电商秒杀)** | ZGC | `-XX:+UseZGC` `-Xmx32G -Xms32G` | JDK 15+ | | **超低延迟(金融交易)** | ZGC / Shenandoah | `-XX:+UseZGC` 或 `-XX:+UseShenandoahGC` | JDK 15+ | | **Kubernetes 容器** | G1 | `-XX:+UseG1GC` `-XX:MaxRAMPercentage=75.0` | JDK 11+ | ### **5.3 不同 JDK 版本的默认收集器** | **JDK 版本** | **默认收集器** | **说明** | | --- | --- | --- | | JDK 7-8 | Parallel GC | Server 模式默认,吞吐量优先 | | JDK 9+ | G1 GC | 官方推荐,平衡吞吐量和延迟 | | JDK 11+ | G1 GC | ZGC 作为实验特性可用 | | JDK 15+ | G1 GC | ZGC 正式发布,生产可用 | | JDK 17 (LTS) | G1 GC | 长期支持版本,推荐使用 | --- ## **六、GC 调优实战** ### **6.1 GC 日志分析** #### **开启 GC 日志** ``` # JDK 8 及之前 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M # JDK 9+ 统一日志框架 -Xlog:gc*:file=/path/to/gc.log:time,level,tags:filecount=5,filesize=20M ``` #### **典型 GC 日志解读** ``` # G1 Young GC 日志 [2024-01-20T10:30:45.123+0800][0.234s][info][gc] GC(10) Pause Young (Normal) (G1 Evacuation Pause) 512M->128M(2048M) 23.456ms 解读: - GC(10):第 10 次 GC - Pause Young:新生代 GC - 512M->128M:堆内存从 512M 降到 128M - (2048M):堆总大小 2GB - 23.456ms:停顿时间 23.456 毫秒 # Full GC 日志(警告信号) [2024-01-20T10:35:50.789+0800][300.567s][info][gc] GC(50) Pause Full (Allocation Failure) 3800M->2100M(4096M) 2345.678ms ⚠️ 问题: - Full GC 停顿 2.3 秒(影响用户体验) - 原因:Allocation Failure(内存分配失败) ``` #### **GC 日志可视化工具** | **工具** | **特点** | **网址** | | --- | --- | --- | | **GCeasy** | 在线分析,生成报告(推荐) | | | **GCViewer** | 开源桌面工具 | | | **GCPlot** | 实时监控 | | | **JClarity Censum** | 商业工具,功能强大 | 已被 Microsoft 收购 | ### **6.2 常见 GC 问题诊断** #### **问题 1:频繁 Minor GC** **现象**: ``` [GC (Allocation Failure) ... ] 0.010s [GC (Allocation Failure) ... ] 0.012s [GC (Allocation Failure) ... ] 0.011s # 几秒内多次 Minor GC ``` **原因**: - 新生代空间太小 - 对象分配速率过快 **解决方案**: ``` # 增大新生代 -Xmn1G # 或 -XX:NewRatio=2(老年代:新生代 = 2:1) # 调整 Eden 和 Survivor 比例 -XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1 # 使用 G1 的动态新生代 -XX:+UseG1GC -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 ``` --- #### **问题 2:频繁 Full GC** **现象**: ``` [Full GC (Allocation Failure) ... ] 2.345s [Full GC (Allocation Failure) ... ] 2.456s # Full GC 频繁且耗时长 ``` **原因分析**: | **原因** | **检查方法** | **解决方案** | | --- | --- | --- | | **老年代空间不足** | 查看老年代使用率 | 增大堆内存 `-Xmx` | | **元空间不足** | `java.lang.OutOfMemoryError: Metaspace` | `-XX:MetaspaceSize=256M` | | **大对象直接进入老年代** | 查看对象大小分布 | `-XX:PretenureSizeThreshold=1M` | | **内存泄漏** | 使用 MAT 分析堆转储 | 修复代码逻辑 | | **System.gc() 显式调用** | 搜索代码 | `-XX:+DisableExplicitGC` | **诊断命令**: ``` # 查看堆内存使用情况 jstat -gcutil 1000 10 # 输出示例: S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 95.23 78.45 89.67 94.12 93.45 100 1.234 10 12.345 13.579 # 关键指标: - O (Old):89.67% ← 老年代使用率过高 - FGC:10 次 Full GC - FGCT:12.345 秒(Full GC 总耗时) # 生成堆转储文件 jmap -dump:format=b,file=heap.hprof # 使用 MAT (Eclipse Memory Analyzer) 分析 # 找出内存占用最大的对象 ``` --- #### **问题 3:GC 停顿时间过长** **现象**: ``` [GC pause (young) 512M->128M, 0.5678 secs] # 停顿超过 500ms ``` **原因与解决**: ``` # 原因1:堆内存过大(单次扫描时间长) # 解决:使用低延迟收集器 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # 设置停顿目标 # 原因2:存活对象过多(复制耗时) # 解决:优化对象生命周期,减少长生命周期对象 # 原因3:并发线程数不足 # 解决:增加 GC 线程 -XX:ParallelGCThreads=8 # 并行 GC 线程 -XX:ConcGCThreads=4 # 并发标记线程 # 原因4:NUMA 架构未优化 # 解决:启用 NUMA 支持 -XX:+UseNUMA ``` --- #### **问题 4:内存泄漏(Memory Leak)** **现象**: ``` # 老年代持续增长,Full GC 后也无法降低 Old Generation: 80% -> 85% -> 90% -> 95% -> OOM ``` **排查步骤**: ``` # 1. 监控堆内存趋势 jstat -gccause 1000 # 2. 对比两次堆转储 jmap -dump:live,format=b,file=heap1.hprof # 等待一段时间后 jmap -dump:live,format=b,file=heap2.hprof # 3. 使用 MAT 对比分析 # Histogram 视图 -> 按大小排序 # Dominator Tree -> 找出支配对象 # 4. 常见泄漏场景 ``` **常见内存泄漏 Case**: ```java // Case 1: 集合类未清理 public class LeakExample1 { private static List list = new ArrayList<>(); public void addObject() { list.add(new Object()); // 持续增加,永不清理 } } // Case 2: 监听器未移除 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // 处理逻辑 } }); // 解决:button.removeActionListener(listener); // Case 3: ThreadLocal 未清理 public class LeakExample3 { private static ThreadLocal> threadLocal = new ThreadLocal<>(); public void process() { threadLocal.set(new ArrayList<>()); // 处理逻辑 // ⚠️ 忘记调用 threadLocal.remove(); } } // Case 4: 数据库连接/IO 流未关闭 Connection conn = DriverManager.getConnection(url); // 使用连接 // ⚠️ 忘记 conn.close(); // 正确做法:使用 try-with-resources try (Connection conn = DriverManager.getConnection(url)) { // 使用连接 } // 自动关闭 ``` --- ### **6.3 GC 调优的黄金法则** #### **法则 1:不要过早优化** ``` 1. 先用默认配置运行 2. 监控 GC 指标(吞吐量、停顿时间) 3. 确认存在性能问题 4. 再进行针对性调优 ``` #### **法则 2:明确优化目标** | **目标** | **关键指标** | **调优方向** | | --- | --- | --- | | **高吞吐量** | GC 时间占比 < 5% | Parallel GC,增大堆内存 | | **低延迟** | P99 停顿 < 100ms | G1 / ZGC,减少单次 GC 停顿 | | **低内存占用** | 堆内存使用率 | 减小堆大小,使用 Serial GC | #### **法则 3:合理设置堆内存** ``` # ⚠️ 错误示例 -Xms512m -Xmx4g # 初始和最大差距过大,导致频繁扩容 # ✅ 推荐做法 -Xms4g -Xmx4g # 固定堆大小,避免扩容开销 # 容器环境 -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 # 使用容器内存的 75% ``` #### **法则 4:避免过度调优** ``` # ❌ 过度优化(参数过多,互相冲突) -Xms4g -Xmx4g -Xmn2g -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+UseAdaptiveSizePolicy=false ...(还有 20 个参数) # ✅ 简洁配置(让 JVM 自适应) -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ``` --- ### **6.4 调优工具箱** #### **命令行工具** | **工具** | **功能** | **示例** | | --- | --- | --- | | **jstat** | 实时监控 GC 统计 | `jstat -gcutil 1000` | | **jmap** | 生成堆转储 | `jmap -dump:format=b,file=heap.hprof ` | | **jstack** | 线程堆栈 | `jstack > thread.txt` | | **jinfo** | 查看/修改 JVM 参数 | `jinfo -flag MaxHeapSize ` | | **jcmd** | 多功能诊断 | `jcmd GC.heap_info` | #### **可视化工具** | **工具** | **特点** | **推荐场景** | | --- | --- | --- | | **JVisualVM** | JDK 自带,实时监控 | 开发测试 | | **JProfiler** | 商业工具,功能全面 | 性能分析 | | **Arthas** | 阿里开源,无需重启 | 线上诊断(⭐ 推荐) | | **Async-profiler** | 低开销火焰图 | CPU 分析 | #### **Arthas 实战示例** ``` # 下载启动 curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar # 监控 GC dashboard # 实时仪表盘 # 查看堆内存 memory # 生成火焰图 profiler start profiler stop --format html # 查看对象分布 heapdump /tmp/heap.hprof ``` ## **七、常见面试题** ### **Q1:为什么新生代使用复**制算法,老年代使用标记-清除/标记-整理? **答案**: **新生代特点**: - 对象朝生夕死(98% 对象在第一次 GC 时死亡) - 每次 GC 后存活对象很少(约 10%) **复制算法优势**: ``` 假设新生代 100 个对象: - 标记-清除:需要扫描 100 个 + 清除 90 个 = 190 次操作 - 复制算法:只需复制 10 个存活对象 = 10 次操作 效率对比:复制算法快 19 倍! ``` **老年代特点**: - 对象生命周期长(存活率 60-80%) - 空间大(占堆内存 2/3) **不用复制算法的原因**: ``` 假设老年代 100 个对象,70 个存活: - 复制 70 个对象 + 需要额外 50% 空间(浪费) - 标记-清除:只需标记 100 个 + 清除 30 个 结论:复制算法在老年代既慢又浪费空间 ``` 结论:复制算法在老年代既慢又浪费空间 **为什么用标记-整理而非标记-清除**: - 标记-清除会产生内存碎片 - 老年代对象大,碎片会导致提前 Full GC - 标记-整理虽慢,但消除碎片,长期性能更好 --- ### **Q2:什么是 Stop-The-World (STW)?如何减少 STW 时间?** **STW 定义**: GC 时暂停所有用户线程(应用程序停顿) **为什么需要 STW**: ```java // 场景:不 STW 会导致的问题 A -> B -> C // 标记阶段 // 用户线程修改引用: A -> D B 断开 C // 结果:C 被误回收(对象消失问题) ``` **减少 STW 的策略**: | **层面** | **方法** | **示例** | | --- | --- | --- | | **算法层** | 使用并发算法 | CMS、G1、ZGC 的并发标记 | | **收集器层** | 选择低延迟收集器 | G1(< 200ms)、ZGC(< 10ms) | | **参数层** | 减少单次回收量 | `-XX:MaxGCPauseMillis=100` | | **代码层** | 优化对象生命周期 | 对象池、减少临时对象 | | **架构层** | 垂直拆分服务 | 单个 JVM 堆不要超过 32GB | **实战优化**: ``` # 优化前(Parallel GC) -XX:+UseParallelGC Young GC: 200ms, Full GC: 3s # 优化后(G1) -XX:+UseG1GC -XX:MaxGCPauseMillis=50 Young GC: 50ms, Mixed GC: 100ms, Full GC: 很少发生 ``` --- ### **Q3:对象一定分配在堆上吗?什么是栈上分配和标量替换?** **答案**: 不一定!JVM 有三种优化手段。 #### **1. 栈上分配(Stack Allocation)** **前提条件**: - 对象作用域不会逃逸出方法 - 对象较小 ```java public void method() { User user = new User(); // 对象未逃逸(方法内创建并使用) user.setName("Alice"); System.out.println(user.getName()); // 方法结束,user 不再使用 } // JVM 优化:直接在栈上分配 user 对象 // 好处:方法结束自动回收,无需 GC ``` #### **2. 标量替换(Scalar Replacement)** **概念**: - 标量:不可再分的数据(int, long, reference) - 聚合量:可分解的对象(User 对象) ```java public void method() { User user = new User(); user.name = "Alice"; user.age = 18; } // JVM 优化为: public void method() { String name = "Alice"; // 标量 int age = 18; // 标量 // 不创建 User 对象,直接使用局部变量 } ``` #### **3. TLAB(Thread Local Allocation Buffer)** **问题**:多线程在 Eden 区分配对象需要加锁。 **解决**: ``` 每个线程在 Eden 区预先分配一小块缓冲区(TLAB): Eden 区 ┌────────────────────────────────────┐ │ [线程1 TLAB] [线程2 TLAB] [线程3...]│ └────────────────────────────────────┘ 线程在自己的 TLAB 内分配对象,无需加锁 ``` **JVM 参数**: ``` # 启用逃逸分析(JDK 8+ 默认开启) -XX:+DoEscapeAnalysis # 启用标量替换 -XX:+EliminateAllocations # TLAB 配置 -XX:+UseTLAB # 默认开启 -XX:TLABSize=256k ``` --- ### **Q4:如何判断一个对象是否可以被回收?** **两种算法对比**: #### **1. 引用计数法(Reference Counting)** ``` A -> B // B 的引用计数 = 1 C -> B // B 的引用计数 = 2 A 断开 // B 的引用计数 = 1 C 断开 // B 的引用计数 = 0 → 可回收 ``` 缺陷:无法解决循环引用 ```java A.ref = B; B.ref = A; // A 和 B 的引用计数都是 1,但实际上都是垃圾 ``` #### **2. 可达性分析(Reachability Analysis)⭐** **GC Roots 包括**: - 虚拟机栈中的引用(局部变量表) - 方法区中静态属性引用 - 方法区中常量引用 - 本地方法栈中 JNI 引用 - JVM 内部引用(Class 对象、异常对象、类加载器) - 被同步锁持有的对象 ``` GC Roots ├─ 栈变量 A ──> 对象1 ──> 对象2 ├─ 静态变量 B ──> 对象3 └─ 常量 C ──> 对象4 对象5 <──> 对象6 ← 无法从 GC Roots 到达,可回收 ``` **实战示例**: ```java public class GCRootsDemo { private static Object staticObj = new Object(); // GC Root(静态变量) public void method() { Object localObj = new Object(); // GC Root(栈变量) // localObj 和 staticObj 引用的对象不会被回收 } } ``` --- ### **Q5:四种引用类型有什么区别?** | **引用类型** | **回收时机** | **使用场景** | **示例** | | --- | --- | --- | --- | | **强引用** | 永远不回收 | 普通对象引用 | `Object obj = new Object()` | | **软引用** | 内存不足时回收 | 缓存(图片缓存) | `SoftReference cache` | | **弱引用** | 下次 GC 时回收 | 缓存(ThreadLocalMap) | `WeakReference ref` | | **虚引用** | 任何时候(用于跟踪回收) | 堆外内存回收 | `PhantomReference ref` | **代码示例**: ```java // 1. 强引用(默认) Object obj = new Object(); // 只要 obj 可达,永不回收 // 2. 软引用(内存敏感缓存) SoftReference softRef = new SoftReference<>(new byte[1024 * 1024]); byte[] data = softRef.get(); // 可能返回 null(已被回收) // 3. 弱引用(临时缓存) WeakReference weakRef = new WeakReference<>(new User()); User user = weakRef.get(); // 可能返回 null // 4. 虚引用(追踪对象回收) ReferenceQueue queue = new ReferenceQueue<>(); PhantomReference phantomRef = new PhantomReference<>(new Object(), queue); // phantomRef.get() 永远返回 null // 对象被回收时,phantomRef 会被加入 queue ``` ThreadLocal 内存泄漏分析: ```java ThreadLocal threadLocal = new ThreadLocal<>(); // ThreadLocalMap 结构: Entry extends WeakReference> { Object value; // 强引用 } // 问题: threadLocal = null; // ThreadLocal 被回收(弱引用) // 但 value 仍被 Entry 强引用 → 内存泄漏 // 解决: threadLocal.remove(); // 手动清理 ``` --- ### **Q6:什么时候会触发 Full GC?如何避免?** **触发条件(8 种场景)**: | **触发场景** | **说明** | **避免方法** | | --- | --- | --- | | **1. 老年代空间不足** | 对象晋升时老年代放不下 | 增大堆内存 `-Xmx` | | **2. 元空间不足** | 类加载过多 | `-XX:MetaspaceSize=256M` | | **3. System.gc()** | 代码显式调用 | `-XX:+DisableExplicitGC` | | **4. CMS Concurrent Mode Failure** | 并发清除时空间不足 | 提前触发 GC(降低阈值) | | **5. 晋升担保失败** | Minor GC 前检查失败 | 增大老年代 | | **6. 大对象分配失败** | 老年代碎片化 | 使用 G1(无碎片) | | **7. JDK 7 永久代满** | 永久代不足 | 升级 JDK 8+ | | **8. GC Ergonomics** | 自适应策略触发 | 调整停顿时间目标 | **诊断和优化流程**: ``` # 1. 监控 Full GC 频率 jstat -gccause 1000 # 2. 查看触发原因 [Full GC (Allocation Failure) ...] # 分配失败 [Full GC (Ergonomics) ...] # 自适应策略 [Full GC (System.gc()) ...] # 显式调用 # 3. 针对性优化 # 场景1:频繁 Allocation Failure -Xmx8G -Xms8G # 增大堆 # 场景2:元空间问题 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M # 场景3:禁用显式 GC -XX:+DisableExplicitGC # 场景4:切换到 G1 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 ``` --- ### **Q7:G1 为什么能实现可预测的停顿时间?** **核心机制**: #### **1. Region 化设计** ``` 传统分代:必须回收整个新生代/老年代 G1:选择部分 Region 回收 ``` #### **2. 停顿预测模型** ```java // G1 维护每个 Region 的统计信息: class RegionStats { long gcTime; // 历史 GC 耗时 long garbageBytes; // 垃圾对象字节数 double value = garbageBytes / gcTime; // 回收价值 } // 选择算法(贪心): 在停顿时间预算内,选择价值最大的 Region 组合 ``` #### **3. 增量回收** ``` Young GC:只回收所有 Eden + Survivor Region Mixed GC:回收所有新生代 + 部分老年代 Region(价值高的) 不需要一次回收整个堆 → 停顿时间可控 ``` **配置示例**: ``` -XX:+UseG1GC -XX:MaxGCPauseMillis=100 # 目标停顿 100ms # G1 内部决策: 如果回收 10 个 Region 需要 80ms:继续 如果回收 11 个 Region 需要 105ms:停止,留到下次 ``` --- ### **Q8:ZGC 如何实现 10ms 内的超低延迟?** **三大核心技术**: #### **1. 着色指针(Colored Pointers)** ``` 传统 GC:标记信息存储在对象头(需要访问对象) ZGC:标记信息存储在指针中(无需访问对象) 64 位指针布局: [unused 16bit][Marked1 1bit][Marked0 1bit][Remapped 1bit][地址 42bit] 判断对象状态:直接检查指针的标志位(极快) ``` #### **2. 读屏障(Load Barrier)** ```java // 用户代码: Object obj = field.someObject; // ZGC 插入读屏障: Object obj = loadBarrier(field.someObject); // 屏障逻辑(硬件层面优化): if (指针颜色不匹配当前阶段) { slowPath(); // 处理重定位、标记等 } // 快速路径几乎无开销(1-2 个 CPU 周期) ``` #### **3. 并发移动对象** ``` 传统 GC:移动对象时必须 STW(防止引用失效) ZGC:通过转发表 + 读屏障实现并发移动 并发重定位过程: ┌─────────────────────────────────────┐ │ 1. 标记阶段(并发) │ │ 标记存活对象 │ ├─────────────────────────────────────┤ │ 2. 重定位准备(短暂 STW < 1ms) │ │ 选择需要移动的 Region │ ├─────────────────────────────────────┤ │ 3. 并发重定位 │ │ 后台线程:移动对象到新地址 │ │ 转发表:记录 旧地址 -> 新地址 │ ├─────────────────────────────────────┤ │ 4. 用户线程访问对象 │ │ 读屏障检测到旧指针 │ │ 查询转发表 -> 返回新地址 │ │ ⚡ 用户线程协助修正引用(自愈) │ └─────────────────────────────────────┘ ``` **关键点:转发表(Forwarding Table)** ``` 转发表存储在每个 Region 内: ┌──────────┬──────────┐ │ 旧地址 │ 新地址 │ ├──────────┼──────────┤ │ 0x1000 │ 0x5000 │ │ 0x1100 │ 0x5100 │ └──────────┴──────────┘ 用户线程访问旧地址时: 读屏障拦截 查转发表获取新地址 自愈(修正引用为新地址) 下次访问直接命中新地址(无开销) ``` **为什么只有 < 10ms 停顿:** ``` ZGC 的 STW 只发生在: 初始标记:扫描 GC Roots(< 1ms) 重定位准备:统计 Region(< 1ms) 重映射准备(可选,< 1ms) 其他所有阶段(标记、移动、重映射)都是并发的 ``` --- ### **Q9:JVM 内存模型(JMM)与 GC 的关系?** **澄清概念**: - **JVM 内存结构**:堆、栈、方法区等(本文讨论的) - **Java 内存模型(JMM)**:线程间通信规范(happens-before) **JMM 对 GC 的影响**: #### **1. 写屏障(Write Barrier)** ```java // G1/ZGC 使用写屏障记录跨 Region 引用 obj.field = anotherObj; // 编译后插入写屏障代码: obj.field = anotherObj; if (obj 和 anotherObj 在不同 Region) { 记录到 Remembered Set; } ``` #### **2. 读屏障(Load Barrier)** ```java // ZGC 使用读屏障处理对象重定位 Object obj = field.someObject; // 插入读屏障: Object obj = loadBarrier(field.someObject); // 检查指针颜色,必要时重定位 ``` #### **3. 内存屏障(Memory Barrier)** ```java // GC 的 STW 过程需要内存屏障确保可见性 // 1. GC 线程发起 STW 请求 // 2. 插入内存屏障,确保所有线程看到最新状态 // 3. 各线程到达安全点后停止 ``` ---