JVM垃圾回收算法与调优实战

JVM垃圾回收算法与调优实战

前言

Java的自动内存管理是Java语言最核心的优势之一,而垃圾回收(Garbage Collection,GC)机制则是自动内存管理的核心。理解JVM的垃圾回收机制,对于编写高性能Java应用、排查线上问题至关重要。本文将全面解析JVM的内存模型、垃圾回收算法以及调优实战技巧。

一、JVM内存模型

1.1 运行时数据区

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        JVM 进程内存                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    Method Area (方法区)                   │  │
│  │  - 类信息                                                 │  │
│  │  - 静态变量                                                 │  │
│  │  - 常量池                                                  │  │
│  │  - JIT编译后的代码                                          │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │  JDK 1.7: PermGen (有大小限制,需手动设置)              │  │  │
│  │  │  JDK 1.8+: Metaspace (使用Native Memory,自动扩展)     │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                       Heap (堆)                          │  │
│  │  ┌────────────────┐     ┌────────────────┐              │  │
│  │  │   Young Gen     │     │    Old Gen      │              │  │
│  │  │  ┌────┬────┐   │     │                 │              │  │
│  │  │  │Eden│ S0 │S1 │     │                 │              │  │
│  │  │  │    │    │   │     │                 │              │  │
│  │  │  └────┴────┘   │     │                 │              │  │
│  │  └────────────────┘     └─────────────────┘              │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                   Native Method Stack                    │  │
│  │                      (本地方法栈)                          │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                      Java Stack                          │  │
│  │                    (Java虚拟机栈)                         │  │
│  │   ┌─────────┐  ┌─────────┐  ┌─────────┐                  │  │
│  │   │ Thread1 │  │ Thread2 │  │ Thread3 │                  │  │
│  │   │  Stack  │  │  Stack  │  │  Stack  │                  │  │
│  │   └─────────┘  └─────────┘  └─────────┘                  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                  Program Counter Register                 │  │
│  │                     (程序计数器)                          │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.2 堆内存详解

复制代码
// JVM堆内存配置示例
// -Xms512m          初始堆大小 512MB
// -Xmx1024m         最大堆大小 1GB
// -Xmn256m          新生代大小 256MB
// -XX:MetaspaceSize=128m   元空间初始大小
// -XX:MaxMetaspaceSize=512m 元空间最大大小

public class HeapMemoryDemo {
    public static void main(String[] args) {
        // 查看JVM内存信息
        Runtime runtime = Runtime.getRuntime();

        System.out.println("JVM总内存: " + runtime.totalMemory() / 1024 / 1024 + "MB");
        System.out.println("JVM最大内存: " + runtime.maxMemory() / 1024 / 1024 + "MB");
        System.out.println("空闲内存: " + runtime.freeMemory() / 1024 / 1024 + "MB");
        System.out.println("可用处理器: " + runtime.availableProcessors());
    }
}

1.3 对象分配流程

复制代码
对象分配流程图:

    ┌─────────────────┐
    │   申请新内存    │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │ 是否在TLAB分配?│
    └────────┬────────┘
             │
    ┌────────┴────────┐
    │    Yes         │    No
    ▼                ▼
┌─────────┐    ┌─────────────────┐
│TLAB分配 │    │ Eden区CAS分配   │
│(线程本地│    │ (失败则加锁)    │
│缓冲区)  │    └────────┬────────┘
└────┬────┘             │
     │                  ▼
     │         ┌─────────────────┐
     │         │  Eden空间足够?  │
     │         └────────┬────────┘
     │                  │
     │          ┌───────┴───────┐
     │          │ Yes            │ No
     │          ▼                ▼
     │    ┌──────────┐    ┌─────────────┐
     │    │ 分配成功  │    │ 触发Minor GC│
     │    └──────────┘    └──────┬──────┘
     │                            │
     │                            ▼
     │                   ┌─────────────────┐
     └──────────────────►│  GC后空间足够?  │
                         └────────┬────────┘
                                  │
                          ┌───────┴───────┐
                          │ Yes           │ No
                          ▼               ▼
                    ┌──────────┐    ┌─────────────┐
                    │ 分配成功  │    │ 触发Full GC │
                    └──────────┘    └──────┬──────┘
                                           │
                                           ▼
                                    ┌─────────────┐
                                    │ 空间仍不足? │
                                    │ (OOM)       │
                                    └─────────────┘

二、垃圾回收算法

2.1 引用计数算法

复制代码
// 引用计数算法原理
// 每个对象有一个引用计数器
// 每当有引用指向它时,计数器+1
// 每当引用失效时,计数器-1
// 计数器为0时,对象可被回收

public class ReferenceCounting {
    public static void main(String[] args) {
        // a引用计数 = 1
        Object a = new Object();  

        // b引用计数 = 1
        Object b = new Object();

        // a引用计数 = 2
        // b引用计数 = 1
        Object c = a;  

        // a引用计数 = 1
        a = null;

        // b引用计数 = 0,可以回收
        b = null;  

        // c引用计数 = 0,可以回收
        c = null;
    }
}

// 引用计数无法解决的问题:循环引用
class Node {
    Node ref;  // 引用计数永远不为0
}

Node n1 = new Node();  // n1: count=1
Node n2 = new Node();  // n2: count=1
n1.ref = n2;            // n2: count=2
n2.ref = n1;            // n1: count=2
n1 = null;              // n1: count=1 (循环引用导致内存泄漏)
n2 = null;              // n2: count=1

2.2 可达性分析算法

JVM采用可达性分析算法(根搜索算法)来判断对象是否存活:

复制代码
// 可达性分析中的"GC Roots"包括:
// 1. 虚拟机栈中引用的对象
// 2. 方法区中静态属性引用的对象
// 3. 方法区中常量引用的对象
// 4. 本地方法栈中JNI引用的对象
// 5. JVM内部引用(Class对象、异常对象等)
// 6. 同步锁持有的对象
// 7. JVM的一些临时对象

public class GC Roots Demo {
    // GC Root: 静态变量
    private static Object staticObj = new Object();

    // GC Root: 常量
    private static final Object constObj = new Object();

    public void method() {
        // GC Root: 局部变量
        Object localObj = new Object();

        // GC Root: 锁对象
        synchronized (localObj) {
            System.out.println("locked");
        }
    }
}

2.3 四种引用类型

复制代码
public class ReferenceTypes {
    public static void main(String[] args) {
        // 1. 强引用 - 即使OOM也不会回收
        Object strongRef = new Object();  // 永远不会被GC

        // 2. 软引用 - 内存不足时回收
        SoftReference<Object> softRef = new SoftReference<>(new Object());
        // 可用于缓存,内存不足时自动清理

        // 3. 弱引用 - 下次GC必回收
        WeakReference<Object> weakRef = new WeakReference<>(new Object());
        // 适合存储元数据、ThreadLocal

        // 4. 虚引用 - 随时可能被回收
        PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), 
            new ReferenceQueue<>());
        // 用于跟踪对象被回收的时间

        // 5. ReferenceQueue - 监控引用状态
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        WeakReference<Object> trackedRef = new WeakReference<>(new Object(), queue);

        // 检查对象是否被回收
        Reference<? extends Object> ref = queue.poll();
        if (ref != null) {
            System.out.println("对象已被回收");
        }
    }
}

2.4 标记-清除算法

复制代码
算法流程:

初始状态:        标记阶段:          清除阶段:
┌───────────┐    ┌───────────┐      ┌───────────┐
│  □  □  ■  │    │  □  □  ■  │      │  □  □     │
│  □  ■  □  │ => │  □  ■  □  │ =>   │  □        │
│  ■  □  □  │    │  ■  □  □  │      │           │
│  □  □  ■  │    │  □  □  ■  │      │           │
└───────────┘    └───────────┘      └───────────┘
■ = 存活对象      ■ = 标记的对象     □ = 可回收空间
□ = 可回收对象

缺点:
1. 效率不稳定(对象多时标记时间长)
2. 内存碎片化

2.5 复制算法

复制代码
算法流程(新生代使用):

┌──────────────────┐        ┌──────────────────┐
│     Eden         │        │    Survivor S0    │
│   (8/10)         │   =>   │    (1/10)        │
│ 存活对象        │        │                  │
│                  │        │                  │
│ ┌──────────────┐ │        │ ┌──────────────┐ │
│ │  S1 (1/10)    │ │        │ │   S2 (1/10)  │ │
│ │   (To)        │ │        │ │   (From)     │ │
│ └──────────────┘ │        │ └──────────────┘ │
└──────────────────┘        └──────────────────┘

优点:没有内存碎片
缺点:可用内存减半

实际应用:
- 新生代 8:1:1 分区
- 存活对象少(通常<10%),复制开销小
- 适合短生命周期对象

2.6 标记-整理算法

复制代码
算法流程(老年代使用):

标记阶段:           整理阶段:          清理阶段:
┌───────────┐      ┌───────────┐      ┌───────────┐
│  ■  □  ■  │      │  ■  ■  ■  │      │  ■  ■  ■  │
│  □  □  ■  │  =>  │           │  =>  │           │
│  ■  □  □  │      │           │      │           │
│  □  □  ■  │      │           │      │           │
└───────────┘      └───────────┘      └───────────┘

优点:
1. 没有内存碎片
2. 内存连续

缺点:
1. 移动对象需要更新引用
2. STOP THE WORLD 时间长

2.7 分代收集算法

复制代码
// JVM分代策略
// 
// 新生代 (Young Generation)
// ├── Eden区 (80%)
// ├── Survivor S0 (10%)
// └── Survivor S1 (10%)
//     - 回收频率:频繁
//     - 算法:复制算法
//     - Minor GC
//
// 老年代 (Old/Tenured Generation)
//     - 回收频率:较低
//     - 算法:标记-清除/标记-整理
//     - Full GC / Major GC
//
// 元数据区 (Metaspace)
//     - 存储类信息、常量、JIT编译代码
//     - Full GC时可能回收

public class GCGenerationDemo {
    public static void main(String[] args) {
        // 这些对象会分配在 Eden 区
        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(new byte[1024 * 1024]);  // 1MB
        }

        // 如果Survivor区装不下,进入老年代
        // 大对象直接进入老年代 (-XX:PretenureSizeThreshold)

        // 长期存活的对象进入老年代
        // -XX:MaxTenuringThreshold=15 (默认)
    }
}

三、垃圾收集器

3.1 收集器关系图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     Young Generation                            │
│                                                                 │
│  ┌─────────┐   ┌────────────┐   ┌───────────┐   ┌─────────┐   │
│  │ Serial  │──►│ ParNew      │──►│ Parallel  │──►│ G1      │   │
│  │ (STW)   │   │ (并行)      │   │ Scavenge  │   │ (并行)  │   │
│  └─────────┘   └────────────┘   └───────────┘   └─────────┘   │
│                    │                                    │      │
│                    └────────────┐                        │      │
│                               ┌─▼─────────────────────────┘      │
│                               │                                  │
│                               ▼                                  │
│                   ┌───────────────────────┐                     │
│                   │       Old Generation   │                     │
│                   ├───────────────────────┤                     │
│                   │  Serial Old (MSC)      │                     │
│                   │  Parallel Old          │                     │
│                   │  CMS                   │                     │
│                   │  G1 (并发)              │                     │
│                   │  ZGC (并发)             │                     │
│                   │  Shenandoah            │                     │
│                   └───────────────────────┘                     │
└─────────────────────────────────────────────────────────────────┘

3.2 Serial收集器

复制代码
# 配置 Serial 收集器
-XX:+UseSerialGC

# 特点:
# - 单线程收集
# - STW (Stop The World) 时间长
# - 简单高效,没有线程开销
# - 适用于Client模式、小内存应用

3.3 Parallel收集器

复制代码
# 配置 Parallel 收集器
-XX:+UseParallelGC          # 新生代
-XX:+UseParallelOldGC        # 老年代

# 关键参数
-XX:ParallelGCThreads=4      # 并行GC线程数
-XX:MaxGCPauseMillis=200     # 最大GC停顿时间(目标)
-XX:GCTimeRatio=19           # GC时间占比 = 1/(1+GCTimeRatio)

# 特点:
# - 多线程并行收集
# - 吞吐量优先
# - 适用于后台批处理任务

3.4 CMS收集器

复制代码
# 配置 CMS 收集器
-XX:+UseConcMarkSweepGC

# 收集阶段:
# 1. 初始标记 (Initial Mark) - STW, 标记GC Roots
# 2. 并发标记 (Concurrent Mark) - 并发追踪引用链
# 3. 重新标记 (Remark) - STW, 修正并发标记期间的变动
# 4. 并发清除 (Concurrent Sweep) - 并发清除垃圾

# 关键参数
-XX:CMSInitiatingOccupancyFraction=68  # 老年代占用率触发CMS
-XX:+UseCMSCompactAtFullCollection    # Full GC时整理内存
-XX:CMSFullGCsBeforeCompaction=5      # 多少次Full GC后整理

# 缺点:
# - CPU敏感(与用户线程并发)
# - 无法处理浮动垃圾
# - 内存碎片

3.5 G1收集器

复制代码
# 配置 G1 收集器
-XX:+UseG1GC

# 特点:
# - 面向服务端应用的收集器
# - 将堆划分为多个大小相等的Region
# - 可以设置最大停顿时间
# - 支持并发标记
# - 整理空闲时间可控

# 关键参数
-XX:MaxGCPauseMillis=200    # 最大停顿时间目标
-XX:G1HeapRegionSize=1~32MB # Region大小,必须是2的幂
-XX:InitiatingHeapOccupancyPercent=45  # 触发GC的堆占用率

# 收集阶段:
# 1. Young GC - 收集年轻代Region
# 2. Mixed GC - 收集年轻代+老年代
# 3. Full GC - (单线程或Parallel)

3.6 ZGC收集器

复制代码
# 配置 ZGC 收集器 (JDK 11+)
-XX:+UseZGC

# 特点:
# - 停顿时间 < 10ms
# - 停顿时间与堆大小无关
# - 支持TB级堆内存
# - 并发收集
# - 着色指针技术

# 关键参数
-XX:MaxGCPauseMillis=2      # 最大停顿时间(亚毫秒级)
-XX:+UseZGC                 # 启用ZGC

四、GC调优实战

4.1 GC日志分析

复制代码
# 开启GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M

# 日志解读
[GC (Allocation Failure) [ParNew: 6144K->512K(6144K), 0.0125663 secs] 8192K->1024K(16384K), 0.0128034 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]

# 解释:
# Allocation Failure - 分配失败触发Minor GC
# ParNew - 新生代收集器
# 6144K->512K(6144K) - Eden区 6144K->512K, 总容量6144K
# 8192K->1024K(16384K) - 堆内存 8192K->1024K, 总容量16384K
# 0.0128034 secs - 停顿时间

4.2 常见GC问题与解决方案

复制代码
// 问题1:频繁Minor GC
// 原因:Eden区太小,对象分配频繁
// 解决:增大Eden区
// -XX:NewSize=512m -XX:MaxNewSize=512m

// 问题2:频繁Full GC
// 原因:大对象、老年代碎片化、Metaspace不足
// 解决:
// -XX:PretenureSizeThreshold=1024*1024 (大对象直接进老年代)
// -XX:+UseCMSCompactAtFullCollection
// -XX:MetaspaceSize=256m

// 问题3:GC停顿时间长
// 原因:老年代空间不足、大对象分配
// 解决:选择低延迟收集器
// -XX:+UseZGC 或 -XX:+UseShenandoahGC

// 问题4:OOM
// 排查步骤:
// 1. jmap -heap <pid> 查看堆使用情况
// 2. jmap -dump:format=b,file=heap.hprof <pid> 导出堆dump
// 3. MAT/JProfiler 分析内存占用

4.3 完整调优配置示例

复制代码
# 通用服务器配置
JAVA_OPTS="
  -server
  -Xms4g -Xmx4g                    # 堆大小
  -Xmn2g                           # 新生代
  -XX:MetaspaceSize=256m
  -XX:MaxMetaspaceSize=512m

  # G1收集器配置
  -XX:+UseG1GC
  -XX:MaxGCPauseMillis=200
  -XX:G1HeapRegionSize=4m
  -XX:InitiatingHeapOccupancyPercent=45
  -XX:G1ReservePercent=10

  # GC日志
  -XX:+PrintGCDetails
  -XX:+PrintGCDateStamps
  -Xloggc:/var/log/app-gc.log
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/var/log/heap.hprof

  # OOM时打印详细信息
  -XX:+PrintCommandLineFlags
  -XX:+PrintTenuringDistribution
"

4.4 Arthas排查工具

复制代码
# 使用Arthas排查GC问题

# 1. 查看JVM内存
dashboard

# 2. 查看GC情况
jvm | grep -i gc

# 3. 生成堆dump
heapdump /path/to/heap.hprof

# 4. 查看对象占用
ognl '@java.lang.Runtime@getRuntime().totalMemory()'

# 5. 监控GC频率
monitor -c 5 com.xxx.MyClass method

总结

JVM垃圾回收是Java工程师必须掌握的核心知识:

  1. 理解内存模型:堆分为新生代和老年代,合理的分代是GC的基础
  2. 掌握核心算法:标记-清除、复制、标记-整理各有优劣
  3. 选择合适的收集器:吞吐量优先选Parallel,低延迟选G1/ZGC
  4. 学会分析GC日志:通过日志发现问题
  5. 实战调优:根据业务特点调整参数

GC调优没有银弹,需要根据具体业务场景和监控数据来调整。

相关推荐
喜欢流萤吖~2 小时前
Nacos 配置中心:微服务的配置管家
java·运维·微服务
逻辑驱动的ken2 小时前
Java高频面试考点场景题10
java·开发语言·深度学习·求职招聘·春招
weixin_458580122 小时前
php怎么处理跨域请求_php如何设置header解决跨域问题详解
jvm·数据库·python
m0_734949792 小时前
CSS 背景图片无法加载的常见原因与正确写法详解
jvm·数据库·python
程序员晨曦2 小时前
理解函数调用Function Call
java·运维·服务器
2301_815279522 小时前
mysql如何使用yum安装mysql_配置官方yum源与自动安装
jvm·数据库·python
weixin_458580122 小时前
MySQL跨版本迁移数据格式不兼容_使用mysqldump全量导出导入
jvm·数据库·python
Greyson12 小时前
SQL触发器在导入大文件时如何跳过_使用禁用触发器语句导入
jvm·数据库·python
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot微服务在电商场景中的应用与挑战
java·spring boot·redis·面试·kafka·oauth2·microservices