Skip to content

第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/3

2. 对象分配流程

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:1

4. 堆内存溢出

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);  // true

3. 元空间设置

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 exceededGC 占用时间过多增大堆、优化代码
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 对比

特性CJava
对象分配malloc/freenew + 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

  1. 演示堆内存溢出
  2. 演示栈溢出
  3. 字符串常量池测试
  4. 分析对象内存分配
  5. 监控内存使用
  6. 综合:内存泄漏排查

🎓 下一步

  • 第36课:垃圾回收 - GC 算法、垃圾收集器、GC 调优