深入理解 Java JVM,搞定知识点

一、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. 参数固化

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值