Appearance
第35课:JVM 内存模型
🎯 学习目标
- 理解 JVM 内存结构
- 掌握堆、栈、方法区的作用
- 理解对象的创建和分配
- 理解内存溢出的原因和排查
📖 一、JVM 架构概览
┌─────────────────────────────────────────┐
│ JVM (Java Virtual Machine) │
├─────────────────────────────────────────┤
│ Class Loader (类加载器) │
│ ├── Bootstrap ClassLoader │
│ ├── Extension ClassLoader │
│ └── Application ClassLoader │
├─────────────────────────────────────────┤
│ Runtime Data Area (运行时数据区) │
│ ├── Heap (堆) - 所有线程共享 │
│ │ ├── Young Generation (年轻代) │
│ │ │ ├── Eden │
│ │ │ ├── Survivor 0 │
│ │ │ └── Survivor 1 │
│ │ └── Old Generation (老年代) │
│ ├── Method Area (方法区) - 所有线程共享 │
│ │ └── Metaspace (元空间, JDK 8+) │
│ ├── Stack (栈) - 每个线程独有 │
│ ├── Program Counter (程序计数器) │
│ └── Native Method Stack (本地方法栈) │
├─────────────────────────────────────────┤
│ Execution Engine (执行引擎) │
│ ├── Interpreter (解释器) │
│ ├── JIT Compiler (即时编译器) │
│ └── Garbage Collector (垃圾回收器) │
└─────────────────────────────────────────┘📖 二、堆(Heap)- 对象存储
1. 堆的结构
Heap
├── Young Generation (年轻代) - 约1/3
│ ├── Eden (伊甸园) - 80%
│ ├── Survivor 0 (幸存者0) - 10%
│ └── Survivor 1 (幸存者1) - 10%
└── Old Generation (老年代) - 约2/32. 对象分配流程
java
// 创建对象
Person person = new Person();
/*
分配流程:
1. 新对象先在 Eden 区分配
2. Minor GC 后,存活对象移到 Survivor
3. 对象在 Survivor 中每经历一次 GC,年龄+1
4. 年龄达到15(默认),晋升到老年代
5. 大对象直接分配到老年代
*/3. 堆内存设置
bash
# 设置堆大小
java -Xms512m -Xmx2g MyApp
# -Xms: 初始堆大小
# -Xmx: 最大堆大小
# 建议: Xms = Xmx(避免频繁扩容)
# 年轻代和老年代比例
java -XX:NewRatio=2 MyApp # 老年代:年轻代 = 2:1
# Eden 和 Survivor 比例
java -XX:SurvivorRatio=8 MyApp # Eden:Survivor = 8:14. 堆内存溢出
java
/**
* 演示堆内存溢出
* VM参数:-Xms10m -Xmx10m
*/
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次1MB
}
}
}
// 抛出:java.lang.OutOfMemoryError: Java heap space📖 三、栈(Stack)- 方法调用
1. 栈的结构
Thread Stack (每个线程独有)
├── Stack Frame 1 (栈帧1 - 当前方法)
│ ├── 局部变量表 (Local Variables)
│ ├── 操作数栈 (Operand Stack)
│ ├── 动态链接 (Dynamic Linking)
│ └── 方法出口 (Return Address)
├── Stack Frame 2 (栈帧2)
└── Stack Frame 3 (栈帧3)2. 方法调用示例
java
public class StackDemo {
public static void main(String[] args) {
method1();
}
static void method1() {
int a = 10;
method2(a);
}
static void method2(int x) {
int b = x + 5;
method3(b);
}
static void method3(int y) {
int c = y * 2;
}
}
/*
栈的变化:
1. main() 入栈
2. method1() 入栈
3. method2() 入栈
4. method3() 入栈
5. method3() 出栈
6. method2() 出栈
7. method1() 出栈
8. main() 出栈
*/3. 栈内存设置
bash
# 设置栈大小(每个线程)
java -Xss256k MyApp
# 默认:1MB(Linux/Mac)、320KB(Windows 32位)4. 栈溢出
java
/**
* 演示栈溢出
* VM参数:-Xss256k
*/
public class StackOverflowDemo {
private static int count = 0;
public static void recursion() {
count++;
recursion(); // 无限递归
}
public static void main(String[] args) {
try {
recursion();
} catch (StackOverflowError e) {
System.out.println("递归深度: " + count);
e.printStackTrace();
}
}
}
// 抛出:java.lang.StackOverflowError📖 四、方法区(Method Area)
1. 方法区存储内容
Method Area (元空间 Metaspace, JDK 8+)
├── 类信息 (Class metadata)
├── 常量池 (Constant Pool)
├── 静态变量 (Static variables)
└── 即时编译器编译后的代码 (JIT compiled code)2. 字符串常量池
java
// 字符串常量池在堆中(JDK 7+)
String s1 = "hello"; // 常量池
String s2 = "hello"; // 引用常量池
String s3 = new String("hello"); // 堆中新对象
System.out.println(s1 == s2); // true(同一对象)
System.out.println(s1 == s3); // false(不同对象)
// intern() - 将字符串放入常量池
String s4 = s3.intern();
System.out.println(s1 == s4); // true3. 元空间设置
bash
# JDK 8+ 使用元空间(Metaspace)代替永久代(PermGen)
java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m MyApp
# 优点:元空间使用本地内存,不受堆大小限制4. 元空间溢出
java
/**
* 演示元空间溢出
* VM参数:-XX:MaxMetaspaceSize=10m
*/
public class MetaspaceOOM {
public static void main(String[] args) {
while (true) {
// 动态生成类
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor)
(obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
enhancer.create();
}
}
}
// 抛出:java.lang.OutOfMemoryError: Metaspace📖 五、直接内存(Direct Memory)
1. 什么是直接内存?
java
// NIO 中的 DirectByteBuffer 使用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 直接内存不在 JVM 堆中,而在操作系统内存中
// 优点:减少数据复制,提高 IO 性能
// 缺点:分配和释放开销大2. 直接内存设置
bash
# 设置最大直接内存
java -XX:MaxDirectMemorySize=512m MyApp📖 六、内存分配示例
java
public class MemoryAllocationDemo {
// 静态变量 - 方法区
private static int staticVar = 10;
// 实例变量 - 堆
private int instanceVar = 20;
public void method() {
// 局部变量(基本类型) - 栈
int localVar = 30;
// 局部变量(引用类型):引用在栈,对象在堆
Person person = new Person();
// 字符串常量 - 字符串常量池(堆中)
String str1 = "hello";
// new 创建的字符串 - 堆
String str2 = new String("world");
}
}内存分布:
Stack (栈)
├── localVar = 30
├── person (引用) ─────────┐
└── str2 (引用) ──────────┐│
││
Heap (堆) ││
├── Person对象 <──────────┘│
├── String对象 <───────────┘
└── 字符串常量池
└── "hello"
Method Area (方法区/元空间)
├── MemoryAllocationDemo 类信息
└── staticVar = 10📖 七、内存溢出排查
1. 常见 OOM 类型
| 错误 | 原因 | 排查 |
|---|---|---|
| Java heap space | 堆内存不足 | 增大堆、排查内存泄漏 |
| GC overhead limit exceeded | GC 占用时间过多 | 增大堆、优化代码 |
| Metaspace | 元空间不足 | 增大元空间、减少类加载 |
| StackOverflowError | 栈溢出 | 增大栈、检查递归 |
| Direct buffer memory | 直接内存不足 | 增大直接内存 |
2. 排查工具
bash
# 查看 JVM 参数
java -XX:+PrintFlagsFinal -version | grep HeapSize
# 生成堆转储
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof MyApp
# 查看堆使用情况
jmap -heap <pid>
# 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
# 分析堆转储(MAT、VisualVM)⚠️ 常见陷阱
陷阱1:把 JVM 内存都叫“堆”
堆只是对象主要分配区域。线程栈、方法区、直接内存、元空间都可能成为故障来源。
陷阱2:看到 OOM 就只调大 -Xmx
OOM 可能来自堆、元空间、直接内存、线程数过多或本地内存不足。必须先看异常类型和 dump。
陷阱3:忽略线程栈成本
每个线程都有栈。线程数过多时,即使堆不大,也可能消耗大量内存。
陷阱4:误解直接内存
直接内存不在 Java 堆里,但仍占用进程内存。Netty、NIO、文件传输场景要特别关注。
陷阱5:只看内存使用率,不看 GC 后回落
判断泄漏要看 GC 后基线是否持续上涨,而不是某一时刻内存高不高。
🆚 Java vs C 对比
| 特性 | C | Java |
|---|---|---|
| 对象分配 | malloc/free | new + GC |
| 栈内存 | 函数调用栈 | Java 虚拟机栈 |
| 代码元数据 | 编译链接产物 | 方法区/元空间 |
| 堆外内存 | 手动管理 | DirectByteBuffer / JNI |
| 泄漏表现 | 指针丢失无法 free | 对象仍被引用,GC 无法回收 |
对 C 程序员来说,Java 并不是“不需要关心内存”,而是把手动释放转成了“引用关系管理”。对象只要仍可达,GC 就不会回收。
💡 最佳实践
1. 合理设置堆大小
bash
# 生产环境推荐
java -Xms4g -Xmx4g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m MyApp
# Xms = Xmx:避免动态扩容2. 避免内存泄漏
java
// ❌ 内存泄漏:静态集合持有大量对象
public class MemoryLeakDemo {
private static List<Object> list = new ArrayList<>();
public void add() {
list.add(new Object()); // 永远不释放
}
}
// ✅ 及时清理
public void clear() {
list.clear();
}3. 使用对象池
java
// 频繁创建的对象使用对象池
ObjectPool<Connection> pool = new ObjectPool<>();
Connection conn = pool.borrow();
// 使用 conn
pool.return(conn);📝 练习
完成 练习/Ex35_JVM_Memory.java:
- 演示堆内存溢出
- 演示栈溢出
- 字符串常量池测试
- 分析对象内存分配
- 监控内存使用
- 综合:内存泄漏排查
🎓 下一步
- 第36课:垃圾回收 - GC 算法、垃圾收集器、GC 调优