JVM垃圾回收机制深度解析

发布于:2025-07-11 ⋅ 阅读:(11) ⋅ 点赞:(0)

🗑️ JVM垃圾回收机制深度解析



🔍 垃圾判定算法

在JVM中,垃圾回收的第一步是确定哪些对象是"垃圾"。JVM主要采用两种算法来判断对象是否可以被回收。

🔢 引用计数法

引用计数法是一种直观的垃圾判定方法,其核心思想是:为每个对象添加一个引用计数器,当有引用指向该对象时计数器加1,引用失效时计数器减1,计数器为0时即可回收

public class ReferenceCountingExample {
    public Object instance = null;
    
    public static void main(String[] args) {
        ReferenceCountingExample objA = new ReferenceCountingExample();
        ReferenceCountingExample objB = new ReferenceCountingExample();
        
        // 对象之间相互引用
        objA.instance = objB;
        objB.instance = objA;
        
        // 将objA和objB置为null,断开外部引用
        objA = null;
        objB = null;
        
        // 此时objA和objB指向的对象虽然已经不可能再被访问,
        // 但由于它们相互引用,引用计数都不为0,导致无法被回收
        System.gc(); // 触发垃圾回收
    }
}

引用计数法的优缺点:

优点 缺点
实现简单,判定效率高 无法解决循环引用问题
对象可以很快被回收 计数器增减操作带来额外开销
内存管理的实时性较高 需要额外的空间存储计数器

💡 注意:虽然引用计数法简单直观,但由于无法解决循环引用问题,现代JVM(如HotSpot)并不使用此算法作为主要的垃圾判定方法。

🌐 可达性分析算法

可达性分析算法是现代JVM采用的主要垃圾判定算法,其核心思想是:通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链"。如果某个对象到GC Roots之间没有任何引用链相连,则证明此对象是不可能再被使用的,可以被回收

GC Roots
对象B
对象C
对象D
对象E
对象F
对象G
对象H

在Java中,可作为GC Roots的对象包括:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象
  5. 活跃线程
public class GCRootsExample {
    // 静态属性引用的对象作为GC Roots
    private static Object staticObject;
    
    // 常量引用的对象作为GC Roots
    private static final Object CONST_OBJECT = new Object();
    
    public void method() {
        // 虚拟机栈中引用的对象作为GC Roots
        Object localObject = new Object();
        
        // 使用本地方法
        nativeMethod();
    }
    
    private native void nativeMethod(); // 本地方法栈中引用的对象作为GC Roots
    
    public static void main(String[] args) {
        // main方法是一个活跃线程,其中引用的对象作为GC Roots
        Object mainObject = new Object();
        
        // 创建一个不可达对象
        Object unreachableObject = new Object();
        unreachableObject = null; // 断开引用,此对象变为不可达
        
        System.gc(); // 触发垃圾回收
    }
}

可达性分析算法的优缺点:

优点 缺点
能解决循环引用问题 需要STW(Stop-The-World),暂停所有用户线程
判定更加精确 实现复杂
被大多数现代JVM采用 可能造成较长时间的停顿

💡 注意:可达性分析算法执行时必须保证整个分析过程的一致性,因此需要STW。JVM后续的优化方向之一就是如何减少STW的时间。

🔄 垃圾回收算法

确定了哪些对象需要回收后,接下来就是如何高效地回收这些对象。JVM主要采用以下几种垃圾回收算法。

🏷️ 标记-清除算法

标记-清除(Mark-Sweep)算法是最基础的垃圾回收算法,分为两个阶段:

  1. 标记阶段:标记出所有需要回收的对象
  2. 清除阶段:统一回收所有被标记的对象
graph LR
    subgraph 标记前
        A1[对象A] --- B1[对象B] --- C1[对象C] --- D1[对象D] --- E1[对象E]
    end
    
    subgraph 标记后
        A2[对象A] --- B2[对象B 标记] --- C2[对象C] --- D2[对象D 标记] --- E2[对象E 标记]
    end
    
    subgraph 清除后
        A3[对象A] --- C3[对象C] --- 空1((空闲)) --- 空2((空闲)) --- 空3((空闲))
    end
    
    标记前 --> 标记后 --> 清除后
    
    classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;
    classDef marked fill:#f9d4d4,stroke:#333,stroke-width:1px;
    classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;
    
    class A1,B1,C1,D1,E1,A2,C2,A3,C3 normal;
    class B2,D2,E2 marked;
    class 空1,空2,空3 empty;

标记-清除算法的优缺点:

优点 缺点
实现简单 效率不高,标记和清除两个过程效率都不高
是其他算法的基础 产生大量内存碎片,导致无法分配大对象

📋 复制算法

复制(Copying)算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。

graph LR
    subgraph 复制前
        subgraph From空间
            A1[对象A] --- B1[对象B] --- C1[对象C] --- D1[对象D] --- E1[对象E]
        end
        
        subgraph To空间
            空1((空闲)) --- 空2((空闲)) --- 空3((空闲)) --- 空4((空闲)) --- 空5((空闲))
        end
    end
    
    subgraph 复制后
        subgraph To空间变为From空间
            A2[对象A] --- C2[对象C] --- 空6((空闲)) --- 空7((空闲)) --- 空8((空闲))
        end
        
        subgraph From空间变为To空间
            空9((空闲)) --- 空10((空闲)) --- 空11((空闲)) --- 空12((空闲)) --- 空13((空闲))
        end
    end
    
    复制前 --> 复制后                                                                                                                                                                                                                                                                                                                                                  
    
    classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;
    classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;
    
    class A1,B1,C1,D1,E1,A2,C2 normal;
    class 空1,空2,空3,空4,空5,空6,空7,空8,空9,空10,空11,空12,空13 empty;

复制算法的优缺点:

优点 缺点
实现简单,运行高效 内存利用率低,只有一半内存可用
没有内存碎片 对象存活率高时,复制操作开销大
适合新生代回收 需要额外空间做分配担保

💡 实际应用:HotSpot VM的新生代使用了复制算法的变种 - Eden和Survivor区的比例是8:1:1,每次只有10%的内存会被"浪费"。

🔧 标记-整理算法

标记-整理(Mark-Compact)算法是针对老年代对象存活率高的特点设计的。标记过程与标记-清除算法一样,但后续步骤不是直接清理,而是让所有存活的对象都向内存空间一端移动,然后清理掉边界以外的内存。

graph LR
    subgraph 标记前
        A1[对象A] --- B1[对象B] --- C1[对象C] --- D1[对象D] --- E1[对象E]
    end
    
    subgraph 标记后
        A2[对象A] --- B2[对象B 标记] --- C2[对象C] --- D2[对象D 标记] --- E2[对象E 标记]
    end
    
    subgraph 整理后
        A3[对象A] --- C3[对象C] --- 空1((空闲)) --- 空2((空闲)) --- 空3((空闲))
    end
    
    标记前 --> 标记后 --> 整理后
    
    classDef normal fill:#d4f9d4,stroke:#333,stroke-width:1px;
    classDef marked fill:#f9d4d4,stroke:#333,stroke-width:1px;
    classDef empty fill:#d4d4f9,stroke:#333,stroke-width:1px;
    
    class A1,B1,C1,D1,E1,A2,C2,A3,C3 normal;
    class B2,D2,E2 marked;
    class 空1,空2,空3 empty;

标记-整理算法的优缺点:

优点 缺点
不会产生内存碎片 移动对象需要更新引用,效率较低
内存利用率高 需要STW,停顿时间可能较长
适合老年代回收 实现复杂

🏗️ 分代收集算法

分代收集算法并不是一种具体的垃圾回收算法,而是根据对象的生命周期特征,将内存划分为几个区域,并在不同区域采用不同的收集算法。

graph TD
    subgraph JVM堆内存
        subgraph 新生代
            E[Eden区] --- S1[Survivor 1区]
            E --- S2[Survivor 2区]
        end
        
        subgraph 老年代
            O[Old区]
        end
    end
    
    新生代 -.-> |对象晋升| 老年代
    
    classDef eden fill:#f9d4d4,stroke:#333,stroke-width:1px;
    classDef survivor fill:#d4f9d4,stroke:#333,stroke-width:1px;
    classDef old fill:#d4d4f9,stroke:#333,stroke-width:1px;
    
    class E eden;
    class S1,S2 survivor;
    class O old;

分代收集的策略:

  1. 新生代:大多数对象朝生夕灭,存活率低,采用复制算法
  2. 老年代:对象存活率高,采用标记-清除或标记-整理算法

分代收集的对象晋升过程:

public class GenerationalGCExample {
    public static void main(String[] args) {
        // 1. 新对象优先在Eden区分配
        byte[] allocation1 = new byte[30900*1024];
        
        // 2. Eden区满,触发Minor GC,存活对象复制到Survivor区
        byte[] allocation2 = new byte[900*1024];
        
        // 3. 多次GC后,对象年龄达到阈值,晋升到老年代
        for (int i = 0; i < 15; i++) {
            byte[] allocation3 = new byte[1000*1024];
            allocation3 = null; // 使对象变为垃圾
            System.gc(); // 建议JVM进行垃圾回收
        }
        
        // 4. 大对象直接进入老年代
        byte[] allocation4 = new byte[10*1024*1024];
    }
}

分代收集算法的优缺点:

优点 缺点
针对不同代的特点采用最合适的算法 实现复杂
提高了垃圾回收效率 需要维护多个内存区域
减少了内存碎片和停顿时间 各代之间的对象引用需要特殊处理

🛠️ 常见垃圾收集器

JVM提供了多种垃圾收集器,每种收集器都有其特点和适用场景。

🔄 Serial 收集器

Serial收集器是最基本、历史最悠久的垃圾收集器,它是一个单线程收集器,在进行垃圾收集时,必须暂停所有用户线程。

用户线程 Serial收集器线程 运行 等待 触发GC STW开始 暂停 垃圾收集 完成GC STW结束 恢复运行 等待 用户线程 Serial收集器线程

Serial收集器的特点:

  • 单线程收集
  • 简单高效,对于单CPU环境来说是首选
  • 收集过程中需要STW,停顿时间较长
  • 新生代采用复制算法,老年代采用标记-整理算法
  • 适用于客户端应用,如桌面应用程序

启用参数: -XX:+UseSerialGC

⚡ ParNew 收集器

ParNew收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和Serial收集器完全一样。

用户线程 ParNew收集器线程1 ParNew收集器线程2 ParNew收集器线程n 运行 等待 等待 等待 触发GC STW开始 暂停 垃圾收集 垃圾收集 垃圾收集 完成GC STW结束 恢复运行 等待 等待 等待 用户线程 ParNew收集器线程1 ParNew收集器线程2 ParNew收集器线程n

ParNew收集器的特点:

  • 多线程收集,充分利用多核CPU优势
  • 收集过程中需要STW,但停顿时间比Serial短
  • 新生代采用复制算法
  • 是许多服务端应用首选的新生代收集器
  • 可与CMS收集器配合使用

启用参数: -XX:+UseParNewGC

🚀 Parallel 收集器

Parallel收集器(也称为Parallel Scavenge收集器)是一个新生代收集器,使用复制算法,也是并行的多线程收集器。

public class ParallelGCExample {
    public static void main(String[] args) {
        // 设置Parallel收集器参数
        // -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:GCTimeRatio=19
        
        // 创建大量对象触发GC
        List<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            byte[] bytes = new byte[1024 * 1024]; // 1MB
            list.add(bytes);
            if (i % 10 == 0) {
                list.clear(); // 释放引用,触发GC
            }
        }
    }
}

Parallel收集器的特点:

  • 多线程收集,注重吞吐量
  • 可设置最大垃圾收集停顿时间和吞吐量
  • 自适应调节策略,动态调整参数
  • 新生代采用复制算法,老年代采用标记-整理算法
  • 适用于后台运算而不需要太多交互的应用

启用参数:

  • -XX:+UseParallelGC:新生代使用Parallel Scavenge,老年代使用Serial Old
  • -XX:+UseParallelOldGC:新生代使用Parallel Scavenge,老年代使用Parallel Old

🔧 CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常适合互联网站或者B/S系统的服务端,这类应用通常重视服务的响应速度,希望系统停顿时间最短。

用户线程 CMS收集器线程 运行 等待 触发GC 初始标记(STW) 短暂暂停 标记GC Roots 并发标记 继续运行 并发标记存活对象 重新标记(STW) 短暂暂停 修正标记结果 并发清除 继续运行 并发清除垃圾对象 完成GC 运行 等待 用户线程 CMS收集器线程

CMS收集器的工作流程:

  1. 初始标记:标记GC Roots能直接关联到的对象,速度很快,需要STW
  2. 并发标记:进行GC Roots Tracing,与用户线程并发执行
  3. 重新标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要STW
  4. 并发清除:清除标记为垃圾的对象,与用户线程并发执行

CMS收集器的特点:

  • 并发收集,低停顿
  • 采用标记-清除算法,会产生内存碎片
  • 对CPU资源敏感
  • 无法处理浮动垃圾(并发清除阶段产生的垃圾)
  • 需要预留一部分内存作为并发收集时的预留空间

启用参数: -XX:+UseConcMarkSweepGC

🌟 G1 收集器

G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器,它是JDK 9的默认垃圾收集器。G1收集器的设计目标是取代CMS收集器,它同样具有并发和并行、低停顿的特点,同时兼顾了高吞吐量。

G1堆内存
Region 2: Eden
Region 1: Eden
Region 3: Survivor
Region 4: Old
Region 5: Old
Region 6: Humongous
Region 7: Eden
Region 8: Old

G1收集器的工作原理:

  1. 将整个Java堆划分为多个大小相等的独立区域(Region)
  2. 保留分代概念,但不再是物理隔离
  3. 建立可预测的停顿时间模型
  4. 采用"标记-整理"算法,降低内存碎片
  5. 采用"复制"算法,提高回收效率
public class G1GCExample {
    public static void main(String[] args) {
        // 设置G1收集器参数
        // -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=2m
        
        // 创建大量对象触发GC
        List<byte[]> list = new ArrayList<>();
        Random random = new Random();
        
        while (true) {
            int size = random.nextInt(1024 * 1024); // 0-1MB
            byte[] bytes = new byte[size];
            list.add(bytes);
            
            if (list.size() > 10000) {
                list.subList(0, 5000).clear(); // 释放一部分引用
                System.gc(); // 建议JVM进行垃圾回收
            }
            
            try {
                Thread.sleep(1); // 控制速度
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

G1收集器的特点:

  • 并发与并行收集
  • 分代收集
  • 空间整合(标记-整理+复制算法,无内存碎片)
  • 可预测的停顿时间模型
  • 适用于大内存、多核CPU的服务器环境

启用参数: -XX:+UseG1GC

⚡ 垃圾回收调优

垃圾回收调优是JVM性能优化的重要部分,合理的GC参数配置可以显著提升应用性能。

🔧 常用 JVM 调优参数

参数 说明 示例值
-Xms 初始堆大小 -Xms4g
-Xmx 最大堆大小 -Xmx4g
-Xmn 新生代大小 -Xmn1g
-XX:SurvivorRatio Eden区与Survivor区的比例 -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold 对象晋升老年代的年龄阈值 -XX:MaxTenuringThreshold=15
-XX:ParallelGCThreads 并行GC线程数 -XX:ParallelGCThreads=4
-XX:ConcGCThreads 并发GC线程数 -XX:ConcGCThreads=2
-XX:InitiatingHeapOccupancyPercent 触发并发GC的堆占用率阈值 -XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis 最大GC停顿时间 -XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize G1收集器的Region大小 -XX:G1HeapRegionSize=4m

不同场景的JVM参数配置示例:

  1. 高吞吐量场景(后台批处理):
java -Xms4g -Xmx4g -Xmn1g -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:+UseNUMA -jar app.jar
  1. 低延迟场景(交互式应用):
java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45 -jar app.jar
  1. 内存受限场景(嵌入式或容器环境):
java -Xms256m -Xmx512m -XX:+UseSerialGC -jar app.jar

🛠️ 调优工具使用:JConsole、VisualVM

JConsole

JConsole是JDK自带的图形化监控工具,可以监控本地和远程的Java应用程序。

使用步骤:

  1. 启动JConsole:在命令行中输入jconsole
  2. 选择要监控的Java进程
  3. 查看内存、线程、类、VM摘要等信息

JConsole监控内存的关键指标:

  • 堆内存使用情况
  • 非堆内存使用情况
  • 各代内存使用情况
  • GC次数和时间
VisualVM

VisualVM是一个功能更强大的监控和分析工具,它集成了多种JDK命令行工具的功能。

使用步骤:

  1. 启动VisualVM:在命令行中输入jvisualvm
  2. 选择要监控的Java进程
  3. 查看概述、监视、线程、抽样器、分析器等信息

VisualVM的主要功能:

  • 监控CPU、堆内存、类、线程
  • 执行堆转储和线程转储
  • 分析性能热点
  • 安装插件扩展功能
public class GCTuningExample {
    public static void main(String[] args) {
        // 启动参数: -Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
        
        System.out.println("GC调优示例启动,进程ID: " + ManagementFactory.getRuntimeMXBean().getName());
        System.out.println("使用JConsole或VisualVM连接此进程进行监控");
        
        List<byte[]> list = new ArrayList<>();
        int count = 0;
        
        try {
            while (true) {
                // 创建1MB的对象
                byte[] bytes = new byte[1024 * 1024];
                list.add(bytes);
                count++;
                
                if (count % 10 == 0) {
                    // 每创建10个对象,清理一半对象
                    int half = list.size() / 2;
                    list.subList(0, half).clear();
                    System.out.println("已创建对象数: " + count + ", 当前列表大小: " + list.size());
                }
                
                Thread.sleep(100); // 控制速度
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

🔬 实战案例分析

案例一:内存泄漏排查

问题描述:一个Web应用在运行一段时间后,内存占用持续增加,最终导致OutOfMemoryError。

排查步骤

  1. 收集证据:使用-XX:+HeapDumpOnOutOfMemoryError参数获取堆转储文件
  2. 分析堆转储:使用MAT(Memory Analyzer Tool)分析堆转储文件
  3. 定位问题:发现大量的Session对象未被释放
  4. 解决方案:修复Session管理代码,确保不再使用的Session被及时释放
public class MemoryLeakExample {
    // 问题代码:使用静态集合存储对象,导致内存泄漏
    private static final Map<String, Object> cache = new HashMap<>();
    
    public void addToCache(String key, Object value) {
        cache.put(key, value); // 对象被添加后永远不会被移除
    }
    
    // 修复后的代码:使用WeakHashMap或添加过期机制
    private static final Map<String, Object> fixedCache = new WeakHashMap<>();
    
    public void addToFixedCache(String key, Object value) {
        fixedCache.put(key, value); // 当key不再被引用时,对应的entry会被自动移除
    }
}

案例二:GC停顿时间过长

问题描述:一个交易系统在高峰期出现间歇性响应缓慢,监控发现是GC停顿时间过长导致的。

排查步骤

  1. 收集GC日志:使用-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log参数
  2. 分析GC日志:使用GCViewer等工具分析GC日志
  3. 定位问题:发现Full GC频繁发生,停顿时间长
  4. 解决方案:从Serial Old收集器切换到G1收集器,并调整参数

优化前的JVM参数

-Xms2g -Xmx2g -XX:+UseParallelGC

优化后的JVM参数

-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45

案例三:频繁的Young GC

问题描述:一个数据处理应用频繁出现Young GC,影响整体吞吐量。

排查步骤

  1. 监控GC活动:使用VisualVM观察GC频率和内存使用情况
  2. 分析对象分配:使用-XX:+PrintTenuringDistribution参数查看对象年龄分布
  3. 定位问题:发现大量短生命周期的小对象被频繁创建
  4. 解决方案:优化代码,减少对象创建,增加对象复用
public class FrequentYoungGCExample {
    // 问题代码:在循环中频繁创建对象
    public void processData(List<String> data) {
        for (String item : data) {
            // 每次迭代都创建新的StringBuilder
            StringBuilder builder = new StringBuilder();
            builder.append("Processing: ").append(item);
            System.out.println(builder.toString());
        }
    }
    
    // 优化后的代码:复用StringBuilder对象
    public void processDataOptimized(List<String> data) {
        StringBuilder builder = new StringBuilder();
        for (String item : data) {
            builder.setLength(0); // 清空StringBuilder
            builder.append("Processing: ").append(item);
            System.out.println(builder.toString());
        }
    }
}

📊 总结与展望

垃圾回收技术的发展趋势

  1. 低延迟垃圾收集器:如ZGC(Z Garbage Collector)和Shenandoah,它们的目标是将GC停顿时间控制在10ms以内,无论堆的大小如何

  2. 并发收集的改进:减少或消除STW时间,提高并发收集效率

  3. 自适应调优:更智能的GC参数自动调整,减少人工干预

  4. 大内存优化:针对TB级别内存的优化,支持超大堆

  5. 非易失性内存支持:利用新型存储技术,如Intel的Optane持久内存

最佳实践总结

  1. 选择合适的垃圾收集器

    • 吞吐量优先:Parallel收集器
    • 响应时间优先:CMS或G1收集器
    • 大内存低延迟:ZGC或Shenandoah收集器
  2. 合理设置内存大小

    • 避免设置过大的堆内存,增加GC压力
    • 避免设置过小的堆内存,导致频繁GC
    • 新生代与老年代的比例通常为1:2或1:3
  3. 优化对象生命周期

    • 减少临时对象的创建
    • 使用对象池复用对象
    • 注意集合类的使用,避免内存泄漏
  4. 监控与分析

    • 定期检查GC日志和内存使用情况
    • 使用专业工具分析性能瓶颈
    • 建立性能基准,及时发现异常

如果这篇博客对你有帮助,不要忘记点赞、收藏和分享哦!


网站公告

今日签到

点亮在社区的每一天
去签到