内存结构整体概述
一张图说清楚 JVM 内存
JVM 运行时数据区是 Java 程序运行时的「内存地图」。理解它,就理解了 Java 程序在运行时的全貌。
┌──────────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
│ │
│ ┌──────────────────────────────┐ ┌──────────────────────────┐ │
│ │ 线程私有区域 │ │ 线程共享区域 │ │
│ │ (每个线程独立拥有) │ │ (所有线程共同使用) │ │
│ │ │ │ │ │
│ │ ┌──────────────────────┐ │ │ ┌────────────────────┐ │ │
│ │ │ PC 寄存器 │ │ │ │ 堆(Heap) │ │ │
│ │ │ 当前执行指令地址 │ │ │ │ 对象实例/数组 │ │ │
│ │ └──────────────────────┘ │ │ │ GC 的主战场 │ │ │
│ │ │ │ └────────────────────┘ │ │
│ │ ┌──────────────────────┐ │ │ ┌────────────────────┐ │ │
│ │ │ 虚拟机栈 │ │ │ │ 方法区 │ │ │
│ │ │ 方法调用 → 栈帧 │ │ │ │ (JDK 8 = Metaspace)│ │ │
│ │ │ 局部变量/操作数栈 │ │ │ │ 类信息/常量/静态变量│ │ │
│ │ └──────────────────────┘ │ │ └────────────────────┘ │ │
│ │ │ │ ┌────────────────────┐ │ │
│ │ ┌──────────────────────┐ │ │ │ 直接内存 │ │ │
│ │ │ 本地方法栈 │ │ │ │ NIO 堆外内存 │ │ │
│ │ │ native 方法服务 │ │ │ └────────────────────┘ │ │
│ │ └──────────────────────┘ │ └──────────────────────────┘ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘线程私有 vs 线程共享
这是理解 JVM 内存最核心的分类:
| 类型 | 区域 | 生命周期 | GC |
|---|---|---|---|
| 线程私有 | PC 寄存器、虚拟机栈、本地方法栈 | 与线程共存亡 | 无需回收,线程结束自动释放 |
| 线程共享 | 堆、方法区、直接内存 | 与 JVM 进程共存亡 | GC 管理的主要对象 |
线程私有区域
每个线程都有自己的 PC 寄存器和虚拟机栈/本地方法栈。线程之间完全隔离,互不干扰。
这带来的一个直接好处:多线程不需要加锁就能安全地操作自己的栈。因为每个线程只能访问自己的私有区域。
public class ThreadPrivateDemo {
public static void main(String[] args) {
// main 线程的 PC 寄存器指向当前指令
// main 线程的虚拟机栈包含 main() 方法的栈帧
new Thread(() -> {
// 新线程有自己的 PC 寄存器和虚拟机栈
// 这些区域对其他线程完全不可见
doWork();
}).start();
}
}线程共享区域
堆和方法区被所有线程共享,它们的内容需要考虑并发安全。
public class ThreadSharedDemo {
// 这个静态变量存在于方法区,被所有线程共享
private static final List<String> sharedList = new ArrayList<>();
public static void main(String[] args) {
// 多个线程同时访问 sharedList
// 需要考虑线程安全(synchronized / ConcurrentHashMap)
for (int i = 0; i < 10; i++) {
new Thread(() -> sharedList.add("item")).start();
}
}
}堆:Java 内存的核心
几乎所有对象实例都分配在堆上。这是 GC 的主战场,也是大多数性能问题的根源所在。
┌─────────────────────────────────────┐
│ 堆(Heap) │
│ │
│ ┌───────────────────────────────┐ │
│ │ 老年代(Old Gen) │ │
│ │ 长期存活的对象 / 大对象 │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Survivor S0 / S1 │ │
│ │ MinorGC 后存活的对象 │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Eden 区 │ │
│ │ 新对象优先分配在这里 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘- Eden 区:新对象出生的地方
- Survivor 区:MinorGC 后存活对象的暂存区
- 老年代:长期存活或过大的对象
我们会在后续的 堆概述 和对象分配专题中详细讲解。
方法区:类信息的仓库
方法区存储的是类的元信息——类的结构、方法的字节码、常量池、静态变量。
JDK 8 之前,方法区用 PermGen(永久代) 实现;JDK 8 开始,改为 Metaspace(元空间),从堆内移到了本地内存。
┌─────────────────────────────────────┐
│ 方法区 / 元空间 │
│ │
│ ├─ 类信息(类名/修饰符/字段/方法) │
│ ├─ 运行时常量池 │
│ ├─ 静态变量(JDK 7 及之前) │
│ ├─ JIT 编译后的代码缓存 │
│ └─ 符号引用 │
└─────────────────────────────────────┘关于方法区的演进和细节,我们会在 方法区概述 深入讲解。
直接内存:NIO 的秘密武器
直接内存不是 JVM 运行时数据区的一部分,但经常与 JVM 内存打交道。它是操作系统分配的本地内存,通过 java.nio.DirectByteBuffer 与堆内对象交互。
主要用途是 Zero-Copy 场景:文件读写、网络传输时避免 JVM 堆和操作系统之间的数据复制,大幅提升 IO 性能。
典型使用场景:Netty 高性能网络框架、Kafka 的消息存储。
各区域与 Java 关键字的对应
理解了内存区域,再看 Java 关键字,会清晰很多:
| 关键字 | 内存位置 | 说明 |
|---|---|---|
new | 堆(Heap) | 创建对象,分配在堆上 |
static | 方法区 | 类变量,所有对象共享 |
final | 堆(对象头) | 对象引用不可变,不影响内容 |
局部变量 | 虚拟机栈 | 方法内部的变量,栈帧中的局部变量表 |
ThreadLocal | 线程私有 | 每个线程独立副本,不共享 |
intern() | 方法区(StringTable) | 字符串驻留 |
为什么需要分这么多区域
很多人会问:为什么不统一成一整块内存?
答案在于不同数据的生命周期不同。有些数据用完就丢(方法的局部变量),有些数据要存活很久(全局单例)。统一管理意味着要么浪费(一直保留短期数据),要么危险(过早回收长期数据)。
分代设计的思路是:根据对象的「年龄」分配到不同的区域,用不同的策略管理。这直接催生了现代 GC 的分代回收理论。
章节导航
接下来,我们按顺序深入每个区域:
让我们从 类加载过程:Loading 开始。
