--- title: "01-JVM与内存模型" created: 2025-11-26 tags: - Java --- # JVM 与内存模型 ## 核心概念 > JVM(Java Virtual Machine)是 Java 程序的**运行时环境**,负责: > > - 加载、校验、执行 `.class` 字节码; > - 管理内存(堆/栈/方法区/元空间 等); > - 管理线程与同步(配合 JMM 和锁)。 > > 目标:**一次编写,到处运行**(Write Once, Run Anywhere)。 > 手段:用 JVM 这一“虚拟机层”屏蔽不同操作系统/硬件的差异,并接管内存、线程等底层细节。 ## 一、为什么需要JVM整体结构? ### 1.1 问题背景 ```java //没有 JVM 会有什么问题? public class NoJvmExample { public static void main(String[] args) { // 假设需要自己直接管理“底层内存” Object obj1 = new Object(); // 谁负责释放?泄漏?碎片? Object obj2 = new Object(); // 多线程:如果没有统一线程模型 & 内存语义 Thread t = new Thread(() -> { // 访问共享数据,无任何约束,极易崩溃 }); // 问题: // - 不同平台内存布局不同,代码不可移植 // - 容易内存泄漏 / 野指针 / 段错误 // - 多线程数据竞争难以控制 } } ``` ### 1.2 设计动机 围绕 Java 的几个核心目标: - **平台无关**: JVM 屏蔽 OS / CPU 差异,通过统一的运行时数据区(堆、方法区、栈等)来执行字节码。 - **内存优化与安全**: - 把内存分成**线程共享**与**线程私有**,降低冲突; - 垃圾回收(GC)统一管理对象生命周期,减少内存泄漏。 - **并发支持**: - 每个线程有自己的栈/PC,互不干扰; - 堆/方法区共享,配合 JMM 提供可见性、有序性保证。 > 结论: > JVM 整体结构 = 通过“运行时数据区 + 线程模型”,把**手动内存管理 + 操作系统细节**封装掉,让 Java 更安全、可移植。 ## 二、JVM 运行时数据区(整体结构) ### 2.1 区域概览(按线程是否共享) **线程共享(所有线程可见):** - **堆(Heap)** - 存放:绝大多数对象实例(`new` 出来的对象)。 - 特点:GC 主要发生在这里;分为新生代 / 老年代。 - **方法区(Method Area / 元空间 Metaspace)** - 存放:类元数据、运行时常量池、静态变量、JIT 编译代码等。 - JDK 8 以前:永久代(PermGen);JDK 8 之后:元空间(使用本地内存)。 **线程私有(每个线程一份):** - **虚拟机栈(VM Stack)** - 存放:栈帧(局部变量表、操作数栈、动态链接、方法返回地址)。 - 方法调用入栈,方法返回出栈;递归过深 → `StackOverflowError`。 - **本地方法栈(Native Method Stack)** - 为 Native 方法服务(JNI 调用 C/C++)。 - **程序计数器(PC Register)** - 指向当前线程下一条将要执行的字节码指令地址;线程切换时用来恢复执行位置。 > 直观类比: > > - 堆 = 大家用的“公共池塘”; > - 方法区 = 存放类说明书、常量表的“元数据区”; > - 栈 = 每个线程自己的“工作台”。 --- ### 2.2 运行流程(简化) 1. **启动 JVM 进程** 2. **划分运行时数据区**(堆、方法区等) 3. **类加载**:把类元数据加载到方法区 4. **创建主线程**: - 分配自己的栈、本地方法栈、PC 寄存器 5. **执行方法**: - 每次方法调用 → 新建栈帧压入栈; - 创建对象 → 内存在堆中分配。 6. **多线程切换**: - 使用每个线程的 PC 寄存器恢复执行位置。 --- ### 2.3 内存分布示例代码 ```java public class JvmMemoryExample { private static int staticVar = 10; // 方法区(类静态变量) private int instanceVar = 20; // 堆(对象的一部分) public static void main(String[] args) { int localVar = 30; // 栈(main 栈帧的局部变量) JvmMemoryExample obj = new JvmMemoryExample(); // 对象在堆,引用在栈 System.out.println( "Static: " + staticVar + ", Instance: " + obj.instanceVar + ", Local: " + localVar ); } } ``` 对应关系: - `staticVar` → 方法区 - `instanceVar` → 堆中的对象数据 - `localVar`、`obj` 引用 → main 线程的栈帧中 --- ### 2.4 运行时数据区:总结一句话 > **共享区(堆、方法区)**:方便线程间共享数据,但易出并发问题,需要同步 + JMM。 > **私有区(栈、本地方法栈、PC)**:天然线程安全,执行效率高。 --- ## 三、线程模型:JVM 如何处理多线程? ### 3.1 线程模型概述 - Java 线程与 OS 线程 **1:1 映射**; - 每个 Java 线程都有: - 自己的虚拟机栈、本地方法栈、PC 寄存器; - 共享堆、方法区中的数据。 ```java public class ThreadModelExample { private static int sharedCounter = 0; // 共享(堆/方法区) public static void main(String[] args) { Thread t1 = new Thread(() -> { int local = 0; // t1 的私有栈 for (int i = 0; i < 1000; i++) { sharedCounter++; // 🔴 竞态条件 local++; } System.out.println("Thread1 local: " + local); }); Thread t2 = new Thread(() -> { int local = 0; // t2 的私有栈 for (int i = 0; i < 1000; i++) { sharedCounter++; local++; } System.out.println("Thread2 local: " + local); }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Shared counter: " + sharedCounter); // 理论应为 2000,实际可能 < 2000(存在并发问题) } } ``` 现象: - `local`:每个线程自己栈上的变量,始终是 1000,互不影响; - `sharedCounter`:共享区的变量,未同步时存在 race condition。 > 结论:**线程私有区负责隔离,线程共享区负责沟通,JMM + 锁/原子类负责“沟通时不打架”。** --- ## 四、堆 vs 栈 vs 方法区 ### 4.1 对比总览 | 区域 | 线程属性 | 存储内容 | 生命周期 | 主要错误 | 调优参数示例 | | --- | --- | --- | --- | --- | --- | | 堆 | 共享 | 对象实例 | JVM 进程存活期 | OOM | `-Xms` / `-Xmx` | | 虚拟机栈 | 私有 | 栈帧(局部变量、操作数栈) | 线程存活期 | StackOverflow | `-Xss` | | 方法区 | 共享 | 类信息、常量池、静态变量等 | JVM 进程存活期 | Metaspace OOM | `-XX:MetaspaceSize/Max...` | ### 4.2 小示例再巩固一下 ```java Object heapObj = new Object(); // 堆:对象数据 int stackVar = 1; // 栈:局部变量 static int methodAreaVar = 2; // 方法区:静态变量 ``` ### 4.3 最佳实践 - 临时数据、循环计数器优先用栈(局部变量)→ 快 & 自动回收; - 需要跨方法/线程共享的对象,放堆,但要注意生命周期和 GC; - 谨慎大量动态生成类,避免方法区 / 元空间 OOM。 --- ## 五、如何调整内存参数? ### 5.1 常见 JVM 内存参数 ``` # 堆大小 java -Xms512m -Xmx2g YourApp # 初始 512M,最大 2G # 每线程栈大小 java -Xss1m YourApp # 每个线程栈 1M # 元空间(方法区实现) java -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=512m YourApp ``` ### 5.2 典型使用场景 - 大量对象、缓存、集合 → 适当**加大堆**,避免 OOM; - 深递归、调用链复杂 → **增大栈** 或优化算法,避免 `StackOverflowError`; - 大量动态加载类(如热部署、字节码增强) → **增大元空间**。 ### 5.3 注意事项 - 参数过大:浪费内存,GC 停顿时间也可能变长; - 参数过小:容易各种 OOM / StackOverflow; - 实战中需配合 GC 日志、`jstat`、`jmap` 等工具观察。 --- ## 六、JVM 结构的应用与常见陷阱 ### 6.1 正确利用“共享堆”做数据交换 ```java class SharedData { // 存在堆中,被多个线程共享访问 private Object data = new Object(); } ``` ### 6.2 常见坑 1:忽略线程共享 ```java int shared = 0; // 如果被多个线程修改,应该是共享状态(static 或实例字段)+ 同步 ``` ### 6.3 常见坑 2:堆 OOM ```java List list = new ArrayList<>(); while (true) { list.add(new Object()); // 无限 new,最终堆 OOM } ``` ### 6.4 常见坑 3:StackOverflowError ```java public class StackOverflow { public static void recurse() { recurse(); } public static void main(String[] args) { recurse(); // 无限递归,栈空间耗尽 } } ``` ### 6.5 常见坑 4:方法区/元空间 OOM - 大量动态生成类(字节码增强、代理、大量 ClassLoader)、频繁热部署; - 可能导致元空间 “爆”。 --- ## 七、高频面试题(QA 版) - **JVM 运行时数据区有哪些?** - 共享:堆、方法区(元空间) - 私有:虚拟机栈、本地方法栈、程序计数器 - **堆和栈的区别?** - 堆:线程共享,存放对象实例,由 GC 管理,易 OOM; - 栈:线程私有,存放栈帧/局部变量,方法结束自动回收,会 `StackOverflowError`。 - **JVM 线程模型是什么样的?** - 基于 OS 线程的 1:1 模型; - 每个 Java 线程有独立的栈、PC 等,堆/方法区共享。 - **为什么需要方法区 / 元空间?** - 存放类元数据、常量池、静态变量、JIT 代码; - 支持类的加载、链接、初始化和反射等。 - **如何处理** `StackOverflowError`**?** - 优化/消除深递归(改为循环或尾递归优化); - 必要时适当调大 `-Xss`(但要谨慎)。 - **永久代(PermGen)和元空间(Metaspace)的区别?** - 永久代:JDK 8 之前,属于堆的一部分,大小固定,易 OOM; - 元空间:JDK 8 之后,使用本地内存,更灵活,减少 PermGen OOM。 --- ## 八、最佳实践 & 记忆口诀 ### 8.1 推荐做法 ✅ ``` # 监控 GC/堆使用 jstat -gc # 查看堆分布 jmap -heap ``` - 合理设置堆、栈、元空间大小; - 使用线程安全的方式访问堆中共享数据(`synchronized` / 原子类); - 避免无限制地 new 对象 / 动态生成类。 ### 8.2 避免做法 ❌ - 无限 `new`、无限缓存、不清除引用 → 堆 OOM; - 无出口的递归 → `StackOverflowError`; - 在多线程中随便改共享变量,不加任何同步; - 在类加载、字节码增强框架中无限创建 ClassLoader,导致元空间 OOM。 ### 8.3 记忆口诀 > **JVM 结构分共享私有堆栈区** > **堆存对象方法类静态常量池** > **线程私有栈与 PC 防冲突** > **调优参数 Xmx / Xss 要牢记** --- ### 🔗 关联 - [[02-类加载机制|02-类加载机制]]:解释“类是如何被装进方法区/元空间”的。 - [[03-垃圾回收与GC算法|03-垃圾回收与GC算法]]:解释“堆里垃圾谁来收、怎么收”。 - [[04-JMM与内存可见性|04-JMM与内存可见性]]:解释“多线程如何安全使用堆/方法区里的共享数据”。 --- ⬅️ [[02-instanceof|instanceof]] 🏠 [[00-Java|00-Java]] ➡️ [[02-类加载机制|类加载机制]]