一、JVM 整体架构

JVM 本质与功能

Java 虚拟机(JVM)本质上是一款跨平台的虚拟计算机,它严格遵循 Oracle 发布的《Java 虚拟机规范》,构成了 Java 技术体系的核心。JVM 的主要职责是将编译生成的字节码(.class 文件)翻译成特定平台的机器码并执行,从而实现 Java 著名的"一次编写,到处运行"(Write Once, Run Anywhere)特性。值得注意的是,JVM 不仅支持 Java 语言,任何能编译成有效字节码的语言(如 Kotlin、Scala)都能在 JVM 上运行。

整体架构

JVM 的架构设计精巧,可分为五大核心模块协同工作:

1.1 架构总览图

┌─────────────────────────────────────────────────────────────┐
│                    类加载子系统 (ClassLoader)                 │
├─────────────────────────────────────────────────────────────┤
│                  运行时数据区 (Runtime Data Area)              │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐      │
│ │  方法区   │ │  堆内存   │ │ 虚拟机栈  │ │本地方法栈 │      │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘      │
│ ┌───────────────────────────────────────────────────────┐   │
│ │        程序计数器 (Program Counter Register)           │   │
│ └───────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────┤
│                    执行引擎 (Execution Engine)               │
├─────────────────────────────────────────────────────────────┤
│                  本地方法接口 (JNI)                          │
├─────────────────────────────────────────────────────────────┤
│                    垃圾回收器 (GC)                          │
└─────────────────────────────────────────────────────────────┘

1.2 各模块核心作用

  1. 类加载子系统:

    • 负责将.class 文件加载到内存中
    • 遵循"双亲委派"机制,依次通过启动类加载器、扩展类加载器和应用类加载器进行加载
    • 完成"加载、验证、准备、解析、初始化"5 个阶段的生命周期
    • 最终生成可被 JVM 使用的 Class 对象
    • 示例:加载一个User类时,会先检查其父类是否已加载
  2. 运行时数据区:

    • JVM 的内存核心区域,包含:
      • 方法区:存储类信息、常量、静态变量等(JDK8后称为元空间)
      • 堆内存:对象实例的主要存储区域(分为新生代和老年代)
      • 虚拟机栈:线程私有,存储方法调用栈帧(包含局部变量表、操作数栈等)
      • 本地方法栈:服务于本地方法调用
      • 程序计数器:记录当前线程执行到的字节码位置
    • 所有 Java 程序的运行都依赖此区域的内存分配与回收
  3. 执行引擎:

    • 将字节码翻译成机器码并执行
    • 主要实现方式:
      • 解释器:逐行翻译字节码,启动速度快但执行效率较低
      • 即时编译器(JIT):将热点代码(HotSpot)编译为本地机器码,执行效率高
      • 现代JVM(如HotSpot)通常采用解释器和JIT混合模式
  4. 本地方法接口(JNI):

    • 提供Java调用本地方法(如C/C++)的能力
    • 常用于:
      • 操作硬件设备(如通过Java调用底层驱动)
      • 性能敏感场景(如图形处理、加密算法)
      • 复用现有本地库
    • 示例:Java标准库中的File类部分方法就是通过JNI实现的
  5. 垃圾回收器(GC):

    • 自动回收堆内存中"不再被引用"的对象
    • 采用多种算法(如标记-清除、复制、标记-整理等)
    • 主流实现包括:
      • 串行收集器(Serial GC)
      • 并行收集器(Parallel GC)
      • CMS收集器
      • G1收集器
      • ZGC(低延迟垃圾收集器)
    • 可显著减轻开发者手动管理内存的负担,避免内存泄漏
    • 示例:当对象失去所有引用时,GC会在适当时候回收其占用的内存

二、运行时数据区

2.1 线程共享区域

2.1.1 堆(Heap):对象存储的"主战场"

作用:堆是Java内存管理的核心区域,存储所有通过new关键字创建的对象实例和数组。它是JVM内存中最大的区域,也是垃圾回收(GC)的主要工作区域。堆的大小直接决定了JVM能够创建和管理多少对象。

内存划分:现代JVM采用分代收集算法优化GC效率,堆被划分为两个主要部分:

  1. 新生代(Young Generation):

    • 占堆内存的1/3(默认)
    • 存储新创建的对象,生命周期通常很短
    • 进一步划分为:
      • Eden区(80%):对象首次分配的区域
      • Survivor0区(10%):Minor GC后存活的对象
      • Survivor1区(10%):用于对象复制
    • 发生Minor GC时,存活对象在Survivor区之间复制,达到一定年龄(默认15次)后晋升到老年代
  2. 老年代(Old Generation):

    • 占堆内存的2/3(默认)
    • 存储长期存活的对象
    • 发生Major GC/Full GC时清理

关键参数:

  • -Xms和-Xmx:设置堆初始和最大内存(如-Xms2g -Xmx4g)
  • -XX:NewRatio:调整新生代与老年代比例(默认2)
  • -XX:SurvivorRatio:设置Eden与Survivor区比例(默认8)
  • -Xmn:直接设置新生代大小(如-Xmn1g)

性能调优:

  • 避免频繁Full GC:合理设置新生代大小
  • 减少对象晋升:适当增加Survivor区
  • 避免内存泄漏:监控对象生命周期

2.1.2 方法区(Method Area):类信息的"仓库"

作用:方法区存储JVM加载的类元数据,包括:

  • 类结构信息(名称、修饰符、父类、接口)
  • 字段和方法信息
  • 运行时常量池
  • 静态变量
  • JIT编译后的代码

实现演变:

  1. JDK 1.7及之前:

    • 使用"永久代"(PermGen)实现
    • 大小受限(-XX:PermSize和-XX:MaxPermSize)
    • 常见问题:字符串常量池导致OOM
  2. JDK 1.8及之后:

    • 使用"元空间"(Metaspace)替代永久代
    • 使用本地内存而非JVM内存
    • 自动扩展(受-XX:MetaspaceSize和-XX:MaxMetaspaceSize限制)
    • 优点:减少OOM风险,支持动态扩展

常见问题排查:

  • 类加载过多:检查动态代理、反射使用
  • 元数据泄漏:监控Metaspace增长
  • 调优建议:设置合理的初始大小

2.2 线程私有区域

2.2.1 虚拟机栈(VM Stack):方法执行的"调用栈"

结构:每个线程拥有独立的虚拟机栈,栈由多个栈帧(Stack Frame)组成,每个方法调用创建一个栈帧。

栈帧组成:

  1. 局部变量表:

    • 存储方法参数和局部变量
    • 基本类型直接存储值
    • 引用类型存储引用地址
    • 大小在编译期确定
  2. 操作数栈:

    • 用于方法执行时的计算
    • 存储中间计算结果
  3. 动态链接:

    • 指向运行时常量池的方法引用
  4. 方法返回地址:

    • 记录方法执行完成后的返回位置

异常处理:

  • StackOverflowError:典型场景是无限递归
  • OOM:线程创建过多导致栈内存耗尽

调优参数:

  • -Xss:设置线程栈大小(如-Xss1m)

2.2.2 本地方法栈(Native Method Stack)

特点:

  • 服务于JNI调用的本地方法
  • 在HotSpot中与虚拟机栈合并
  • 不同JVM实现差异较大
  • 同样可能出现StackOverflowError和OOM

2.2.3 程序计数器(Program Counter Register):线程执行的"导航仪"

详细功能:

  • 每个线程独立拥有
  • 记录当前线程执行的字节码指令地址
  • 线程切换时保存执行状态
  • 执行本地方法时值为undefined
  • 唯一不会出现OOM的区域
  • 对程序性能无直接影响

实现特点:

  • 占用空间极小
  • 快速访问
  • 线程私有,无需同步
  • JVM内部实现细节,开发者不可见

三、类加载机制

3.1 类加载的5个阶段

3.1.1 加载(Loading):找到并读取.class文件

详细过程:

  1. 查找.class文件:JVM根据类的全限定名(如com.example.User)查找对应的.class文件,查找路径包括:

    • 本地文件系统
    • JAR/ZIP包
    • 网络资源(如Applet)
    • 运行时生成的类(动态代理)
    • 其他自定义来源(如数据库)
  2. 读取字节流:将.class文件转换为二进制字节流,这个过程可能涉及:

    • 文件IO操作
    • 网络传输
    • 解密操作(如加密的类文件)
  3. 创建Class对象:在堆中生成java.lang.Class对象,作为类元数据的访问入口。这个对象包含:

    • 类名
    • 修饰符
    • 父类信息
    • 实现的接口
    • 方法信息
    • 字段信息

3.1.2 验证(Verification):确保.class文件合法

详细验证步骤:

  1. 文件格式验证:

    • 检查魔数是否为0xCAFEBABE
    • 检查主次版本号是否在当前JVM支持范围内
    • 检查常量池中的常量是否有不被支持的类型
    • 检查指向常量的索引是否指向了不存在的常量
  2. 元数据验证:

    • 检查类是否有父类(除Object外)
    • 检查父类是否允许继承(如final类)
    • 检查字段/方法是否与父类冲突(如覆盖final方法)
    • 检查类是否实现了所有抽象方法
  3. 字节码验证(最复杂):

    • 检查操作数栈类型与指令是否匹配
    • 检查跳转指令是否指向合法位置
    • 检查类型转换是否合法
    • 检查方法调用是否匹配参数类型
  4. 符号引用验证:

    • 检查引用的类能否被找到
    • 检查字段/方法是否存在于对应类中
    • 检查访问权限是否允许(如private方法)

3.1.3 准备(Preparation):为静态变量分配内存并赋默认值

详细说明:

  1. 内存分配:

    • 静态变量存储在方法区
    • 为每个静态变量分配内存空间
    • 基本类型分配固定大小空间(如int=4字节)
    • 引用类型分配指针大小空间
  2. 默认值赋值:

    • int/long/char/short/byte:0
    • float/double:0.0
    • boolean:false
    • 引用类型:null
  3. 特殊处理:

    • 常量(final static)会在准备阶段直接赋值
    • 非final的static变量维持默认值,直到初始化阶段

3.1.4 解析(Resolution):将符号引用转为直接引用

详细解析过程:

  1. 类/接口解析:

    • 检查符号引用的全限定名
    • 检查访问权限
    • 加载引用的类(如果未加载)
    • 返回直接引用(类元数据指针)
  2. 字段解析:

    • 查找字段所属的类
    • 检查字段是否存在
    • 检查访问权限
    • 返回字段偏移量或访问方法
  3. 方法解析:

    • 查找方法所属的类
    • 检查方法是否存在
    • 检查访问权限
    • 返回方法入口地址
  4. 接口方法解析:

    • 查找接口方法
    • 检查实现类是否实现了该方法
    • 返回方法入口地址

3.1.5 初始化(Initialization):执行静态代码块和静态变量赋值

详细初始化过程:

  1. 触发条件(主动使用):

    • new关键字创建实例
    • 调用静态方法
    • 访问静态字段(非final)
    • 反射调用(Class.forName)
    • 初始化子类(会先初始化父类)
    • 包含main()方法的启动类
  2. 初始化顺序:

    • 父类先于子类初始化
    • 静态变量和静态代码块按代码顺序执行
    • 静态代码块只执行一次
  3. 线程安全:

    • 初始化过程是线程安全的
    • JVM会加锁确保只有一个线程执行初始化
    • 其他线程会阻塞等待初始化完成

3.2 类加载器体系

3.2.1 三层默认类加载器

扩展说明:

  1. 启动类加载器(Bootstrap ClassLoader):

    • 加载路径:$JAVA_HOME/jre/lib目录下的核心库
    • 特有特性:是唯一没有父加载器的加载器
    • 识别方式:String.class.getClassLoader()返回null
  2. 扩展类加载器(Extension ClassLoader):

    • 加载路径:$JAVA_HOME/jre/lib/ext目录
    • 父加载器:启动类加载器
    • 可配置:通过-Djava.ext.dirs修改加载路径
  3. 应用程序类加载器(Application ClassLoader):

    • 加载路径:classpath指定的所有路径
    • 父加载器:扩展类加载器
    • 默认加载器:Thread.currentThread().getContextClassLoader()通常返回此加载器

3.2.2 双亲委派模型

工作机制详解:

  1. 委派流程:

    • 类加载请求首先交给当前类加载器的缓存检查
    • 缓存未命中,则委派给父加载器
    • 父加载器递归执行相同逻辑
    • 所有父加载器都无法加载时,才由当前加载器尝试加载
  2. 代码实现:

    protected Class<?> loadClass(String name, boolean resolve) {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查是否已加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    // 2. 父加载器不为null则委派给父加载器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 3. 父加载器为null则委派给启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                
                // 4. 父加载器都找不到时自己加载
                if (c == null) {
                    c = findClass(name);
                }
            }
            return c;
        }
    }
    

  3. 破坏双亲委派:

    • 历史案例:JDBC SPI(Service Provider Interface)
    • 原因:基础类需要回调用户代码
    • 解决方案:使用线程上下文类加载器
    • 现代方式:模块化系统(Java 9+)提供了更灵活的机制

四、垃圾回收

GC 概述

GC(Garbage Collection)是 JVM 自动内存管理的核心机制,主要负责回收堆内存中"不再被引用"的对象。要深入理解 GC,需要明确以下三个核心问题:

  1. 哪些对象需要回收(对象存活判定)
  2. 如何回收(垃圾回收算法)
  3. 何时回收(GC 触发时机)

4.1 如何判断对象"已死亡"?

GC 的第一步是识别"无用对象",主流判断算法有两种:

4.1.1 引用计数法(Reference Counting)

原理:为每个对象维护一个"引用计数器",当对象被引用时计数器 +1,引用失效时计数器 -1;当计数器为 0 时,认为对象可回收。

具体实现:

  • 创建对象时初始化计数器为 1
  • 当对象被赋值给变量时计数器 +1
  • 当引用超出作用域或被置为 null 时计数器 -1

示例:

Object objA = new Object(); // objA 引用计数=1
Object objB = objA;         // objA 引用计数=2
objB = null;               // objA 引用计数=1
objA = null;               // objA 引用计数=0(可回收)

缺点:

  1. 无法解决循环引用问题:如 A 引用 B,B 引用 A,两者计数器均为 1,但均无其他外部引用,无法被回收
  2. 性能开销:每次引用变更都需要更新计数器
  3. 线程安全问题:多线程环境下需要同步操作计数器

因此 JVM 未采用此算法,而是使用更为可靠的可达性分析算法。

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

原理:以"GC Roots"为起点,向下遍历引用链(对象间的引用关系),若某个对象无法通过任何 GC Roots 到达(引用链断裂),则认为该对象可回收。

算法流程:

  1. 枚举所有 GC Roots 对象
  2. 从这些根对象开始,递归遍历所有引用关系
  3. 标记所有被访问到的对象为存活
  4. 未被标记的对象即为可回收对象

GC Roots 的常见来源:

  1. 虚拟机栈中局部变量表引用的对象:如当前正在执行的方法中的局部变量
    void method() {
        Object localObj = new Object(); // localObj 是 GC Root
    }
    

  2. 方法区中静态变量和常量引用的对象:
    static Object staticObj = new Object(); // staticObj 是 GC Root
    

  3. 本地方法栈中本地方法引用的对象
  4. JVM 内部的引用:如类加载器、GC 线程等
  5. 同步锁持有的对象(synchronized 关键字关联的对象)

示例分析:

class User {
    private Address address;
    // getter/setter...
}

User user = new User();  // user 是 GC Root
user.setAddress(new Address());
user = null;  // 原先的 User 和 Address 对象都不可达

对象引用强度分类:

  1. 强引用(Strong Reference):普通对象引用,不会被 GC
  2. 软引用(Soft Reference):内存不足时会被回收
  3. 弱引用(Weak Reference):GC 时立即被回收
  4. 虚引用(Phantom Reference):无法通过引用获取对象,用于跟踪对象被回收的状态

4.2 常见垃圾回收算法

识别出无用对象后,GC 需要通过算法将其回收并释放内存,主流算法包括:

4.2.1 标记 - 清除算法(Mark-Sweep)

步骤:

  1. 标记阶段:通过可达性分析标记所有可回收对象
    • 遍历所有 GC Roots,标记可达对象
    • 通常使用位图或对象头标记
  2. 清除阶段:遍历堆内存,直接回收标记对象的内存空间
    • 将空闲内存记录到空闲列表
    • 新对象分配时从空闲列表查找合适空间

优点:

  • 实现简单
  • 不需要移动对象(适用于存活对象多的情况)

缺点:

  1. 内存碎片化严重:回收后的内存块大小不一,可能导致无法分配大对象
    • 示例:堆中有 100MB 空闲内存,但分散为 10 个 10MB 的块,无法分配 20MB 的对象
  2. 效率问题:
    • 标记和清除都需要遍历全堆
    • 随着堆增大,GC 时间线性增长

应用场景:

  • 老年代回收(配合其他算法使用)
  • CMS 收集器的并发清除阶段

4.2.2 复制算法(Copying)

步骤:

  1. 将堆内存分为两个大小相等的区域(如新生代的 Eden 区和 Survivor 区)
  2. 只使用其中一个区域(如 Eden+S0)分配对象
  3. 当该区域内存满时:
    • 标记存活对象
    • 将存活对象按顺序复制到另一个空闲区域(S1)
    • 清空原区域的所有对象
  4. 角色互换:将 S1 作为新的使用区域

内存布局:

新生代典型划分:
+--------+---------+---------+
| Eden   | From(S0)| To(S1)  |
+--------+---------+---------+
(默认比例:Eden:S0:S1 = 8:1:1)

优点:

  1. 无内存碎片化:存活对象连续存储
  2. 效率高:
    • 只需复制存活对象
    • 不需要遍历全堆
  3. 分配简单:使用指针碰撞(bump-the-pointer)快速分配

缺点:

  1. 内存利用率低:仅使用一半内存
  2. 存活对象不能太多:适合新生代(98%的对象朝生夕死)
  3. 需要额外空间处理担保失败(当存活对象超过To区容量时)

优化:

  • 多 Survivor 区(如 S0、S1)
  • 动态调整 Survivor 区大小
  • 结合逃逸分析优化复制策略

应用场景:

  • 新生代回收(Minor GC)
  • Serial、ParNew、G1 等收集器的新生代回收

4.2.3 标记 - 整理算法(Mark-Compact)

步骤:

  1. 标记阶段:与标记-清除算法一致,标记可回收对象
  2. 整理阶段:
    • 将存活对象向堆内存的一端移动
    • 更新所有引用这些对象的指针
    • 直接清空另一端的所有可回收对象

内存变化示例:

回收前:
[存活][垃圾][存活][垃圾][存活]
回收后:
[存活][存活][存活][空闲][空闲]

优点:

  1. 无内存碎片化
  2. 内存利用率高(优于复制算法)
  3. 适合存活对象多的情况

缺点:

  1. 效率低:
    • 需要移动存活对象
    • 需要更新所有引用
  2. STW 时间长:不适合对延迟敏感的应用

优化技术:

  • 增量整理
  • 并行整理
  • 滑动整理 vs 压缩整理

应用场景:

  • 老年代回收(Full GC)
  • Serial Old、Parallel Old 收集器
  • G1 的老年区回收

4.3 主流垃圾收集器

垃圾收集器是 GC 算法的具体实现,不同收集器适用于不同场景。HotSpot 虚拟机提供了多种收集器,常见组合如下:

收集器组合适用区域核心特点适用场景
Serial + Serial Old新生代 + 老年代单线程收集,STW客户端应用、小内存环境
ParNew + CMS新生代 + 老年代多线程并发收集,低延迟服务端应用,对延迟敏感
Parallel Scavenge + Parallel Old新生代 + 老年代多线程并行收集,高吞吐量后台计算型应用
G1(Garbage-First)全堆分区收集,可预测停顿大堆内存,平衡吞吐和延迟
ZGC全堆并发收集,超低延迟(<10ms)超大堆内存,极致延迟要求
Shenandoah全堆并发收集,低延迟与 ZGC 类似

4.3.1 各收集器详细解析

(1)Serial 收集器(新生代)

工作模式:

  • 单线程收集
  • GC 时会暂停所有用户线程(STW,Stop The World)
  • 采用复制算法

实现原理:

  1. 暂停所有应用线程
  2. 从 GC Roots 开始标记存活对象
  3. 将 Eden 区和 S0 区的存活对象复制到 S1 区
  4. 清空 Eden 和 S0 区
  5. 恢复应用线程

适用场景:

  • 客户端应用(如桌面程序)
  • 内存较小的环境(如嵌入式设备)
  • 单核 CPU 环境

启动参数:

-XX:+UseSerialGC  # 新生代用 Serial,老年代用 Serial Old

优点:

  • 简单高效
  • 单线程开销低
  • 没有线程交互开销
(2)Serial Old 收集器(老年代)

工作模式:

  • 单线程收集
  • STW 机制
  • 采用标记-整理算法

适用场景:

  1. 与 Serial 收集器搭配使用
  2. 作为 CMS 收集器的后备收集器(当 CMS 出现 Concurrent Mode Failure 时)
  3. 用于 JDK 1.5 及之前版本的客户端应用

执行流程:

  1. 暂停所有应用线程
  2. 标记所有存活对象
  3. 将存活对象向堆的一端移动
  4. 清理边界外的内存
  5. 恢复应用线程
(3)ParNew 收集器(新生代)

工作模式:

  • Serial 收集器的多线程版本
  • STW 机制
  • 采用复制算法
  • 默认 GC 线程数等于 CPU 核心数

核心特点:

  1. 多线程并行回收
    -XX:ParallelGCThreads=4  # 设置 GC 线程数
    

  2. 唯一能与 CMS 收集器配合的新生代收集器
  3. 在单核 CPU 上性能可能不如 Serial 收集器

适用场景:

  • 服务端应用(如 Web 服务)
  • 多核 CPU 环境
  • 与 CMS 收集器搭配使用

启动参数:

-XX:+UseParNewGC  # 启用 ParNew 收集器

性能考量:

  • 在多核环境下能显著减少 STW 时间
  • 线程调度有一定开销
  • 适合中等规模的新生代
(4)CMS 收集器(老年代)

工作模式:

  • 并发标记清除收集器
  • 大部分阶段与用户线程并行
  • 低延迟设计
  • 采用标记-清除算法

执行流程:

  1. 初始标记(Initial Mark,STW):

    • 标记 GC Roots 直接引用的对象
    • 时间很短(通常 10-100ms)
  2. 并发标记(Concurrent Mark):

    • 遍历整个老年代对象图
    • 与用户线程并行执行
    • 耗时较长(占整个 GC 时间的 80%)
  3. 重新标记(Remark,STW):

    • 修正并发标记期间的变化
    • 使用增量更新或原始快照技术
    • 比初始标记长,但远短于并发标记
  4. 并发清除(Concurrent Sweep):

    • 回收标记的垃圾对象
    • 与用户线程并行执行

内存布局:

老年代内存使用 CMS 后的典型状态:
+--------+--------+--------+--------+
| 已用   | 空闲   | 碎片   | 已用   |
+--------+--------+--------+--------+

核心问题与解决方案:

  1. Concurrent Mode Failure(CMF):

    • 原因:并发清除期间老年代空间不足
    • 表现:触发 Full GC,使用 Serial Old 收集器
    • 解决方案:
      -XX:CMSInitiatingOccupancyFraction=75  # 提前触发 CMS
      -XX:+UseCMSInitiatingOccupancyOnly
      

  2. 内存碎片化:

    • 原因:标记-清除算法不整理内存
    • 解决方案:
      -XX:+UseCMSCompactAtFullCollection  # Full GC 后整理
      -XX:CMSFullGCsBeforeCompaction=4    # 每 4 次 Full GC 整理一次
      

  3. 浮动垃圾:

    • 原因:并发标记期间新产生的垃圾
    • 影响:需要预留足够空间(通常 20-30%)

适用场景:

  • 对延迟敏感的服务端应用
  • 老年代不特别大的情况(<8GB)
  • 能容忍偶尔的 Full GC

启动参数:

-XX:+UseConcMarkSweepGC  # 启用 CMS
-XX:+UseParNewGC         # 自动启用 ParNew
-XX:CMSInitiatingOccupancyFraction=70  # 建议 70-80%

(5)G1 收集器(全堆)

工作模式:

  • 分Region的内存布局
  • 可预测停顿模型
  • 并行与并发混合回收
  • 标记-整理+复制混合算法

核心概念:

  1. Region 划分:

    • 堆被划分为多个大小相等的 Region(默认约 2048 个)
    • 每个 Region 可以是 Eden、Survivor、Old 或 Humongous
    • Humongous 区存储大对象(>50% Region 大小)
  2. 记忆集(Remembered Set):

    • 每个 Region 有自己 RSet
    • 记录其他 Region 对本 Region 的引用
    • 避免全堆扫描
  3. 收集集合(Collection Set,CSet):

    • 每次 GC 要回收的 Region 集合
    • 根据垃圾比例优先选择

执行流程:

  1. 初始标记(Initial Mark,STW):

    • 标记 GC Roots 直接关联的对象
    • 与年轻代 GC 一起执行
  2. 并发标记(Concurrent Mark):

    • 遍历对象图
    • 与用户线程并行
  3. 最终标记(Final Mark,STW):

    • 处理 SATB(Snapshot-At-The-Beginning)记录
    • 完成标记
  4. 筛选回收(Live Data Counting and Evacuation,STW):

    • 计算 Region 的回收价值
    • 复制存活对象到空 Region

关键参数:

-XX:+UseG1GC  # 启用 G1
-XX:MaxGCPauseMillis=200  # 目标停顿时间(毫秒)
-XX:G1HeapRegionSize=4m   # Region 大小(1-32MB,2的幂)
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占比

适用场景:

  • 堆内存较大(>6GB)
  • 要求平衡吞吐量和延迟
  • JDK 9+ 的默认收集器

优势:

  1. 并行与并发处理能力
  2. 分代收集但不分代内存
  3. 可预测的停顿模型
  4. 高效的整理算法

不足:

  1. 内存占用较高(RSet 等数据结构)
  2. 写屏障开销
  3. 小堆场景可能不如 CMS

4.4 GC 调优基础

4.4.1 关键性能指标

  1. 吞吐量:应用运行时间 / (应用运行时间 + GC 时间)
    • 目标:通常 >95%
  2. 延迟:GC 导致的 STW 时间
    • 目标:取决于应用需求(如 <200ms)
  3. 内存占用:完成功能所需的最小堆大小

4.4.2 通用调优步骤

  1. 监控分析:

    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    

    或使用可视化工具(如 GCViewer、Grafana)

  2. 设置基础参数:

    -Xms4g -Xmx4g  # 堆大小(生产环境建议相同)
    -XX:NewRatio=2  # 老年代/新生代比例
    

  3. 选择收集器:

    • 吞吐量优先:Parallel Scavenge + Parallel Old
    • 低延迟优先:ParNew + CMS 或 G1
    • 超大堆(>8GB):G1 或 ZGC
  4. 优化分代大小:

    -XX:SurvivorRatio=8  # Eden/Survivor 比例
    -XX:MaxTenuringThreshold=15  # 晋升年龄
    

  5. 调整高级参数:

    • CMS 相关参数
    • G1 的 MaxGCPauseMillis
    • 并行 GC 线程数

4.4.3 常见问题解决

  1. 频繁 Full GC:

    • 检查内存泄漏
    • 增大老年代或调整晋升阈值
    • 优化对象分配
  2. 长 GC 停顿:

    • 考虑切换到 G1 或 ZGC
    • 减少每次回收的区域大小
    • 优化引用结构
  3. 内存碎片化:

    • 使用标记-整理算法
    • 定期 Full GC 整理
    • 考虑使用 G1

4.5 未来发展趋势

  1. 低延迟 GC:

    • ZGC(JDK 11+):目标 <10ms 停顿
    • Shenandoah(JDK 12+):与 ZGC 竞争
  2. 云原生适配:

    • 容器内存感知
    • 弹性堆大小调整
  3. AI 辅助调优:

    • 基于机器学习的自动参数调整
    • 动态 GC 策略切换
  4. 异构内存支持:

    • 持久内存(PMEM)支持
    • 分级存储管理

选择合适的 GC 需要综合考虑应用特点、硬件环境和性能需求。随着 Java 发展,GC 技术也在不断进步,为不同场景提供更优解决方案。

五、GC 日志分析

GC 日志是排查 JVM 内存问题、优化 GC 性能的核心依据。通过分析GC日志,开发人员可以了解内存使用情况、垃圾回收效率以及潜在的性能瓶颈。JVM 默认不输出详细 GC 日志,需通过参数开启,不同收集器(如Serial、Parallel、CMS、G1等)的日志格式略有差异,但核心信息基本一致。

5.1 开启 GC 日志的核心参数

参数作用示例说明
-XX:+PrintGCDetails输出详细 GC 日志(包含内存区域变化)必加参数详细记录每次GC前后各内存区域(如Eden、Survivor、Old)的使用情况
-XX:+PrintGCTimeStamps输出 GC 发生的时间戳(相对于 JVM 启动时间)便于定位 GC 发生时机格式为"123.456: [GC...",表示JVM启动123.456秒后发生GC
-XX:+PrintGCDateStamps输出 GC 发生的具体日期时间(如 2025-09-10T15:30:00)便于关联业务日志与系统日志时间对齐,方便问题排查
-Xloggc:./gc.log将 GC 日志输出到指定文件(避免控制台刷屏)生产环境必加,便于后续分析建议路径为日志专用目录,如/var/log/gc.log
-XX:+UseGCLogFileRotation开启 GC 日志轮转(避免单个日志文件过大)配合以下参数使用当日志达到指定大小时自动创建新文件
-XX:NumberOfGCLogFiles=5日志文件数量保留 5 个日志文件按时间顺序保留最近的5个日志
-XX:GCLogFileSize=100M单个日志文件大小每个文件最大 100MB超过100MB时触发轮转

生产环境推荐配置示例:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/myapp/gc.log 
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M

5.2 常见 GC 日志解析示例

5.2.1 ParNew + CMS 日志(新生代 Minor GC)

2025-09-10T15:30:00.123+0800: 10.456: [GC (Allocation Failure) [ParNew: 204800K->20480K(230400K), 0.0123450 secs] 204800K->40960K(983040K), 0.0124560 secs] [Times: user=0.04 sys=0.01, real=0.01 secs]

核心信息拆解:

  1. 时间信息:

    • 2025-09-10T15:30:00.123+0800:GC 发生的具体时间(ISO8601格式,东八区)
    • 10.456:GC 发生时 JVM 已启动 10.456 秒
  2. GC类型:

    • GC (Allocation Failure):表示这是一次 Minor GC,触发原因是 "新生代分配内存失败"(Eden区空间不足)
  3. 内存变化:

    • ParNew: 204800K->20480K(230400K):
      • ParNew:新生代收集器名称
      • 204800K->20480K:GC前200MB → GC后20MB
      • (230400K):新生代总容量225MB
    • 204800K->40960K(983040K):
      • 堆内存从200MB → 40MB
      • 堆总大小960MB
  4. 耗时统计:

    • 0.0123450 secs:新生代GC实际耗时12毫秒(STW时间)
    • [Times: user=0.04 sys=0.01, real=0.01 secs]:
      • user:所有GC线程消耗的CPU时间总和
      • sys:系统调用耗时
      • real:实际STW时间(多线程并行时,real < user)

5.2.2 G1 收集器日志(Mixed GC)

2025-09-10T15:35:00.678+0800: 40.987: [GC pause (G1 Evacuation Pause) (mixed), 0.0234560 secs]
   [Parallel Time: 20.1 ms, GC Workers: 4]
      [GC Worker Start (ms): 40987.1, 40987.2, 40987.3, 40987.4]
      [Ext Root Scanning (ms): 5.2, 5.1, 5.0, 4.9]
      [Update RS (ms): 2.1, 2.2, 2.3, 2.0]
         [Processed Buffers: 10, 8, 9, 11]
      [Scan RS (ms): 1.0, 1.1, 0.9, 1.2]
      [Code Root Scanning (ms): 0.5, 0.4, 0.6, 0.5]
      [Object Copy (ms): 10.3, 10.2, 10.4, 10.1]
      [Termination (ms): 0.0, 0.0, 0.0, 0.0]
   [Code Root Fixup: 0.2 ms]
   [Code Root Purge: 0.1 ms]
   [Clear CT: 0.3 ms]
   [Other: 3.0 ms]
   [Heap Before GC: 800.0M(1024.0M) -> Heap After GC: 400.0M(1024.0M)]
   [Young Regions: 10->2 (total 20, 1024.0K each)]
   [Old Regions: 50->30 (total 100, 1024.0K each)]
   [Humongous Regions: 5->3 (total 10, 1024.0K each)]

核心信息拆解:

  1. GC类型:

    • GC pause (G1 Evacuation Pause) (mixed):G1的混合回收,同时处理新生代和部分老年代Region
  2. 时间统计:

    • 0.0234560 secs:总STW时间23毫秒
    • Parallel Time: 20.1 ms, GC Workers: 4:4个GC线程并行工作耗时20.1毫秒
  3. 各阶段耗时:

    • Ext Root Scanning:扫描根对象(如栈、寄存器等)
    • Update RS:更新Remembered Set
    • Object Copy:复制存活对象的关键阶段
    • Termination:GC线程结束工作
  4. 内存变化:

    • Heap Before GC: 800.0M(1024.0M) -> Heap After GC: 400.0M(1024.0M):堆内存减少400MB
    • Region统计:
      • Young Regions: 10->2:新生代Region从10个减少到2个
      • Old Regions: 50->30:老年代Region回收20个
      • Humongous Regions:大对象Region变化
  5. 其他信息:

    • Processed Buffers:每个GC线程处理的RSet缓冲区数量
    • Code Root相关:处理代码缓存的时间

六、JVM 性能调优

核心目标与原则

JVM 调优的核心目标是:减少 GC 停顿时间、降低 GC 频率、避免 OOM 异常,最终提升应用的吞吐量和稳定性。调优需遵循"先监控,后调优"的原则,避免盲目修改参数。

在实际生产环境中,JVM调优是一个持续优化的过程,需要根据应用特性、业务量和硬件环境进行针对性调整。典型的应用场景包括:

  • 高并发Web服务:关注低延迟和快速响应
  • 大数据处理应用:关注高吞吐量
  • 金融交易系统:关注极端低延迟
  • 后台批处理作业:关注资源利用率

6.1 调优前的准备:关键监控指标

需通过工具监控以下指标,定位性能瓶颈:

内存指标

  • 堆内存各区域使用情况:
    • Eden区:新对象分配的主要区域
    • Survivor区:存放Minor GC后存活的对象
    • Old区:存放长期存活的对象
  • 内存分配速率:每秒分配的内存量
  • 晋升速率:对象从新生代晋升到老年代的速率
  • 元空间使用情况:类元数据存储区域

GC 指标

  • Minor GC频率:通常应控制在10-30秒一次
  • Minor GC停顿时间:理想情况下应小于100ms
  • Full GC频率:应控制在1小时以内
  • Full GC停顿时间:单次停顿不应超过1秒
  • GC吞吐量:GC时间占总运行时间的比例

线程指标

  • 活跃线程数:反映当前并发处理能力
  • 阻塞线程数:可能指示锁竞争或I/O瓶颈
  • 死锁线程数:严重影响系统可用性
  • 线程创建/销毁频率:过高可能影响性能

6.2 核心调优参数(按场景分类)

6.2.1 内存分配优化

堆内存设置
  • -Xms与-Xmx:

    • 建议设置为相同值(如-Xms4g -Xmx4g)
    • 避免JVM频繁扩容/缩容带来的性能开销
    • 典型配置:生产环境4GB-16GB,根据物理内存调整
  • 堆大小选择原则:

    • 物理内存8GB:堆可设为4GB-6GB
    • 物理内存16GB:堆可设为8GB-12GB
    • 预留20%-30%内存给操作系统和其他进程
新生代优化
  • -Xmn:

    • 新生代大小,建议占堆的1/3-1/2
    • 对于创建对象频繁的应用(如Web服务),可适当增大
    • 示例:堆4GB,新生代可设为1.5GB
    • 过小会导致频繁Minor GC,过大会减少老年代空间
  • -XX:SurvivorRatio:

    • Eden与Survivor区比例,默认8(Eden:S0:S1=8:1:1)
    • 一般无需修改,除非有特殊需求
    • 调整原则:
      • 增大可容纳更多新对象
      • 减小可延长对象在Survivor区的存活时间
老年代优化
  • 大小由堆总大小和新生代大小决定
  • 频繁Full GC可能原因:
    • 大对象直接进入老年代
    • 对象晋升过快
    • 内存泄漏
  • 调优策略:
    • 检查对象生命周期
    • 适当增大堆总大小
    • 优化缓存策略
元空间优化(JDK 1.8+)
  • -XX:MetaspaceSize:
    • 元空间初始大小,默认约21MB
    • 建议设置为256MB-512MB
  • -XX:MaxMetaspaceSize:
    • 元空间最大大小
    • 防止无限制占用本地内存
  • Metaspace OOM解决方案:
    • 检查类加载器泄漏
    • 增大元空间大小
    • 优化动态类生成逻辑

6.2.2 GC 收集器选择与优化

收集器选择策略
应用类型推荐收集器适用场景
客户端应用Serial + Serial Old单线程、低开销
服务端应用(低延迟)ParNew + CMS响应时间敏感型
服务端应用(大堆)G1平衡吞吐量与延迟
大数据处理Parallel Scavenge + Parallel Old高吞吐量优先
CMS收集器调优
  • -XX:+UseConcMarkSweepGC:启用CMS
  • -XX:CMSInitiatingOccupancyFraction=75:
    • 老年代使用率达75%时触发CMS
    • 避免Concurrent Mode Failure
  • -XX:+UseCMSCompactAtFullCollection:
    • Full GC后整理内存碎片
  • -XX:CMSFullGCsBeforeCompaction=3:
    • 每3次Full GC后整理一次
G1收集器调优
  • -XX:+UseG1GC:启用G1收集器
  • -XX:MaxGCPauseMillis=100:
    • 设置目标停顿时间
    • 金融场景建议50ms
  • -XX:G1HeapRegionSize=4m:
    • Region大小设置
    • 堆16GB以下建议4MB-8MB
  • -XX:InitiatingHeapOccupancyPercent=45:
    • 堆使用率触发阈值
    • 大堆可适当降低

6.2.3 其他关键调优参数

大对象处理
  • -XX:PretenureSizeThreshold=1048576:
    • 超过1MB的对象直接进入老年代
    • 避免大对象在新生代频繁复制
    • 需根据应用特点调整阈值
对象晋升年龄
  • -XX:MaxTenuringThreshold=15:
    • 对象晋升年龄阈值
    • 可通过-XX:+PrintTenuringDistribution监控
    • 观察对象年龄分布调整
显式GC控制
  • -XX:+DisableExplicitGC:
    • 禁止System.gc()调用
    • 生产环境建议开启
    • 避免误触发Full GC

6.3 常见问题排查案例

案例1:频繁Full GC排查

现象:

  • Full GC频率超过1次/分钟
  • 单次停顿超过1秒
  • 接口响应时间从50ms升至500ms+

排查步骤:

  1. 分析GC日志:
    • 确认触发原因
    • 检查内存不足模式
  2. 堆内存分析:
    • 使用jmap生成堆dump
    • MAT工具分析对象分布
  3. 元空间检查:
    • jstat监控元空间使用
    • 确认是否达到阈值

解决方案:

  1. 缓存优化:
    • 引入Redis
    • 设置合理过期时间
  2. 内存泄漏修复:
    • 检查线程持有情况
    • 清理无用引用
  3. 参数调整:
    • 增大堆内存
    • 调整元空间大小

案例2:G1停顿时间超标

现象:

  • 设置MaxGCPauseMillis=100
  • 实际STW超过200ms

排查步骤:

  1. GC日志分析:
    • 确认耗时阶段
    • 检查Region回收情况
  2. Region分布:
    • 打印Region存活信息
    • 分析对象分布
  3. 并发标记:
    • 检查标记耗时
    • 确认堆使用率

解决方案:

  1. 参数调整:
    • 调整停顿时间目标
    • 降低并发标记阈值
  2. Region优化:
    • 调整Region大小
    • 平衡回收粒度

6.4 JVM调优标准化流程

1. 确定目标

  • 明确关键指标:
    • GC频率
    • 停顿时间
    • 吞吐量
    • 内存占用

2. 监控基准状态

  • 收集基础数据:
    • GC日志
    • 堆内存统计
    • 线程状态
  • 使用分析工具:
    • GCEasy
    • VisualVM
    • MAT

3. 定位瓶颈

  • 高频GC:
    • 检查新生代大小
    • 优化对象分配
  • 长停顿:
    • 评估收集器选择
    • 调整GC线程
  • OOM:
    • 分析堆dump
    • 检查内存泄漏

4. 参数调整与验证

  • 增量调整:
    • 每次改1-2个参数
    • 记录修改影响
  • 效果验证:
    • 运行足够时间
    • 对比基准数据

5. 参数固化

  • 更新启动脚本
  • 文档记录:
    • 调优过程
    • 参数含义
    • 效果对比
  • 建立监控机制:
    • 持续跟踪
    • 异常告警

更多推荐