03. JVM 内存模型与类加载机制

深入 Java 虚拟机运行时数据区、类加载器双亲委派模型、字节码执行引擎与内存屏障

JVM 是 Java 生态的基石。理解其内存布局、类加载机制与执行引擎,不仅是性能调优的前提,更是排查 OOM、类冲突、类泄露等疑难杂症的必备技能。

1. JVM 运行时数据区总览

┌─────────────────────────────────────────────────────┐
│                  JVM Runtime                         │
├─────────────────────────────────────────────────────┤
│  线程私有(随线程生灭)                               │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │
│  │  PC 寄存器   │  │ 虚拟机栈    │  │ 本地方法栈  │ │
│  │ (计数指令)   │  │ (Stack)     │  │ (Native)    │ │
│  └─────────────┘  └─────────────┘  └─────────────┘ │
├─────────────────────────────────────────────────────┤
│  线程共享(所有线程可见)                              │
│  ┌─────────────────────────────────────────────┐   │
│  │              堆 (Heap)                       │   │
│  │   ┌─────────────┐  ┌──────────────────────┐│   │
│  │   │ 年轻代      │  │ 老年代               ││   │
│  │   │ Eden / S0/S1│  │ (Old Gen)            ││   │
│  │   └─────────────┘  └──────────────────────┘│   │
│  └─────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────┐   │
│  │        元空间 (Metaspace, Java 8+)          │   │
│  │   类元数据 / 方法元数据 / 常量池 / 符号引用   │   │
│  └─────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────┤
│  直接内存(堆外)                                    │
│  ┌─────────────────────────────────────────────┐   │
│  │ 直接堆外内存 (Direct Buffer / Unsafe.allocate) │  │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

1.1 各区域详解

区域用途异常参数
PC 寄存器当前线程执行字节码行号无(Native 方法时为 Undefined)
虚拟机栈方法调用栈帧(局部变量、操作数栈、动态链接、返回地址)StackOverflowError / OutOfMemoryError-Xss
本地方法栈Native 方法调用同上
对象实例与数组OutOfMemoryError: Java heap space-Xms / -Xmx / -Xmn
元空间类元数据(Java 8+ 替代永久代)OutOfMemoryError: Metaspace-XX:MaxMetaspaceSize
直接内存NIO 缓冲区、Netty 等OutOfMemoryError: Direct buffer memory-XX:MaxDirectMemorySize

1.2 堆:对象分配与晋升

新对象 → Eden 区
  ├─ Minor GC 后存活 → Survivor 区 (S0 或 S1)
  │   ├─ 交替复制到另一 Survivor
  │   └─ 每经历一次 Minor GC,年龄 +1
  │       └─ 年龄 ≥ -XX:MaxTenuringThreshold 或 Survivor 50% 同龄对象 → 晋升老年代
  └─ 大对象(-XX:PretenureSizeThreshold) → 直接分配老年代

-XX:TargetSurvivorRatio = 50  (Survivor 目标使用率)

空间分配担保:Minor GC 前检查老年代最大连续空间是否大于新生代所有对象总空间;否则查看 HandlePromotionFailure。JDK 6u24 后此参数废弃,默认开启。

2. 对象内存布局

对象实例在堆中的结构(HotSpot,64 位,默认开启压缩指针):

┌─────────────────┐  0 mark word          (8 bytes)
│  对象头          │    - 哈希码 / GC 年龄 / 锁状态
│  ─────────────  │    - 类型指针 Klass pointer
│  Mark Word      │      (压缩后 4 bytes / 64 位 8 bytes)
│  Klass Pointer  │    (数组对象还有 4 bytes 数组长度)
├─────────────────┤
│  实例数据        │  字段值(父类字段在前,按对齐填充排序)
│  (Instance Data)│
├─────────────────┤
│  对齐填充        │  8 字节对齐
│  (Padding)      │
└─────────────────┘

2.1 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

对象创建 → 无锁(001)
  → 第一个线程获取 → 偏向锁(101)→ Mark Word 记录线程 ID
    → 出现另一个线程竞争 → 轻量级锁(00)→ CAS 自旋
      → 自旋超限(默认 10 次 / 自适应) → 重量级锁(10)→ Monitor

JVM 参数控制

参数说明
-XX:+UseBiasedLocking启用偏向锁(JDK 15+ 废弃)
-XX:BiasedLockingStartupDelay=0启动 0s 后启用

3. 类加载机制

3.1 类生命周期

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
  ↑______________________________________________|

各阶段职责

阶段内容
加载读取 .class → 字节数组 → 生成 Class 对象
验证格式/元数据/字节码/符号引用验证
准备静态变量分配内存 + 零值初始化(final 直接赋值常量)
解析符号引用 → 直接引用(类、字段、方法、接口)
初始化执行 <clinit>()(静态变量赋值 + 静态代码块)
卸载GC 回收(需满足:ClassLoader 无引用、Class 无实例、无反射引用)

3.2 类加载器层级(双亲委派)

        JVM 内建                         应用层
  ┌────────────────────┐      ┌─────────────────────────────────┐
  │ Bootstrap CL       │      │                                 │
  │  (C++ 实现)         │      │  Application ClassLoader        │
  │  rt.jar / jmod      │      │  classpath / jar                │
  │    ↑               │      │    ↑                            │
  │ Ext CL (Platform)  │      │  Extension 模块                 │
  │  jre/lib/ext       │◄─────┤    ↑                            │
  │    ↑               │  委托 │  Custom ClassLoader             │
  │ App CL (System)    │◄─────┤  Tomcat/Spring Boot Loader      │
  │  classpath         │      │                                 │
  └────────────────────┘      └─────────────────────────────────┘

委托顺序:Custom → App → Ext → Bootstrap
加载顺序:Bootstrap → Ext → App → Custom(先找到先加载)

双亲委派的意义

  • 安全:防止核心类被篡改(String、Object 等只能由 Bootstrap 加载)
  • 避免重复加载:保证同一份字节码在 JVM 中只有一个 Class 实例

3.3 打破双亲委派

场景 1:SPI 机制(JDBC、JNDI)

// ServiceLoader 由 Bootstrap ClassLoader 调用
// 但需要加载厂商实现(如 MySQL driver)在 classpath
// 解决方案:Thread Context ClassLoader (TCCL)
Class.forName("com.mysql.cj.jdbc.Driver", true, Thread.currentThread().getContextClassLoader());

场景 2:Tomcat 隔离

  • 每个 Web 应用独立 WebAppClassLoader
  • 优先加载 WEB-INF/classesWEB-INF/lib,不向上委托
  • common/lib 共享类由 CommonClassLoader 加载

场景 3:OSGi / 模块化

  • 每个 Bundle 独立 ClassLoader
  • 复杂的导入/导出依赖图

3.4 自定义 ClassLoader

public class CustomClassLoader extends ClassLoader {
    private final String basePath;
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = loadClassData(name);
        return defineClass(name, bytes, 0, bytes.length);
    }
    
    // 打破双亲委派:先自己加载,找不到再委托
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if (name.startsWith("com.myapp.plugin")) {
            return findClass(name);
        }
        return super.loadClass(name, resolve);
    }
}

4. Java 内存模型(JMM)

4.1 主内存与工作内存

主内存(Main Memory)←→ 工作内存(线程私有)
                      ┃
        ┌─────────────┼─────────────┐
        ↓             ↓             ↓
   线程 A 工作内存  线程 B 工作内存  线程 C 工作内存

规定:所有变量存储在主内存;每条线程有自己的工作内存,保存了被线程使用到的变量的副本。线程对变量的操作必须在工作内存中进行

4.2 happens-before 规则

无需同步即可保证有序性的场景:

  1. 程序次序规则:同一线程内,书写在前面的操作 happens-before 后面的
  2. 锁定规则:解锁 happens-before 后续对同锁的加锁
  3. volatile 规则:写 happens-before 后续读
  4. 传递性:A → B, B → C ⇒ A → C
  5. 线程启动Thread.start() happens-before 线程内操作
  6. 线程终止:线程内操作 happens-before 检测终止(Thread.join() / isAlive()
  7. 中断规则interrupt() happens-before 检测到中断
  8. 对象终结:构造函数执行 happens-before finalize()

4.3 volatile 语义

语义实现
可见性写操作立刻刷回主内存;读操作从主内存刷新
禁止重排序插入内存屏障,阻止指令重排
private volatile boolean flag = false;

// 线程 A
flag = true;  // StoreStore + StoreLoad 屏障

// 线程 B
while (!flag) {}  // LoadLoad + LoadStore 屏障
  // flag 为 true → 能看到线程 A 之前的所有写

volatile 不保证原子性i++ 需配合 synchronizedAtomicInteger

5. 字节码执行引擎

5.1 运行时栈帧结构

局部变量表 (Local Variable Table)
  ┌── Slot 0: this (实例方法) / 静态方法跳过
  ├── Slot 1: 参数/局部变量 1
  ├── Slot 2: 参数/局部变量 2  (long/double 占 2 slots)
  └── ...

操作数栈 (Operand Stack)
  ┌── 栈顶
  ├── 操作数
  └── 栈底

动态链接 (Dynamic Linking): 指向运行时常量池的方法引用

返回地址 (Return Address): 方法出口,异常处理表

5.2 方法调用指令

指令绑定时机类型
invokestatic编译期静态方法
invokespecial编译期构造器/private/super
invokevirtual运行时虚方法(动态分派)
invokeinterface运行时接口方法
invokedynamic运行时Lambda、反射(引导方法确定)

5.3 动态类型语言支持(invokedynamic)

Java 8 的 Lambda 表达式并非语法糖,而是利用 invokedynamic + LambdaMetafactory.metafactory() 在运行时生成调用点。

// 源码
Supplier<String> s = () -> "hello";

// 字节码(简化)
invokedynamic #0:get:()Ljava/util/function/Supplier;

// 运行时:LambdaMetafactory 生成适配类
// class Lambda$1 implements Supplier { String get() { return "hello"; } }

6. 实用工具

工具用途
javap -c -v ClassName反编译字节码
javap -constants ClassName查看常量池
jclasslibGUI 字节码查看器
HSDB (HotSpot Debugger)查看对象头、Klass 结构

7. JVM 参数速查

# 内存区域
-Xms512m -Xmx2g -Xmn768m
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m

# GC 打印
-XX:+PrintGCDetails              # JDK 8
-Xlog:gc*:file=gc.log:time,uptime:filecount=10,filesize=100m  # JDK 9+

# 调试
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:OnOutOfMemoryError="kill -9 %p"

# 类加载
-XX:+TraceClassLoading
-verbose:class

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析