JVM 
🧠 一、JVM 内存模型概览 
JVM 的内存区域可以分为以下几个主要部分:
| 区域名称 | 是否线程私有 | 用途说明 | 
|---|---|---|
| 程序计数器 | ✅ 是 | 记录当前线程执行的字节码行号 | 
| 虚拟机栈 | ✅ 是 | 存放方法调用时的局部变量、操作数栈等 | 
| 本地方法栈 | ✅ 是 | 为 Native 方法服务(如 C/C++ 调用) | 
| 堆(Heap) | ❌ 否 | 所有线程共享,存放对象实例 | 
| 方法区(MetaSpace) | ❌ 否 | 存储类信息、常量池、静态变量、编译器编译后的代码等 | 
| 直接内存(Direct Memory) | ❌ 否 | NIO 使用,不属于 JVM 堆内 | 
🔥 二、重点详解:堆、栈、方法区 
1. 堆(Heap) 
✅ 特点: 
- 所有线程共享的一块内存区域。
- 用于存储所有的 对象实例(Object) 和 数组。
- 是垃圾回收器(GC)管理的主要区域。
- 可以动态扩展大小(通过 JVM 参数设置)。
示例: 
java
Person p = new Person(); // new 出来的对象就存在堆中GC 分代模型(现代 JVM 的堆结构): 
- 新生代(Young Generation)- Eden 区
- Survivor 0、Survivor 1
 
- 老年代(Old Generation)
- 元空间(Metaspace):替代了永久代(PermGen)
💡 注意:从 JDK 8 开始,永久代(PermGen)被元空间(Metaspace)取代,元空间使用的是本地内存(Native Memory),不在 JVM 堆中。
2. 虚拟机栈(Java Stack) 
✅ 特点: 
- 每个线程都有一个自己的虚拟机栈。
- 栈中保存的是一个个的 栈帧(Stack Frame),每个栈帧对应一个方法的调用。
- 每次方法调用都会在栈中压入一个新的栈帧,方法执行完毕弹出。
栈帧中包含: 
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 其他附加信息
示例: 
java
public void sayHello() {
    int a = 10;
    String name = "Tom";
}- sayHello()方法被调用时,会创建一个栈帧。
- a和- name是局部变量,会被放入栈帧的局部变量表中。
- 实际的字符串 "Tom"是对象,放在堆中。
常见异常: 
- StackOverflowError:递归太深或方法调用层次太多导致栈溢出。
- OutOfMemoryError:如果线程请求的栈深度超过最大允许值。
3. 本地方法栈(Native Method Stack) 
- 类似于虚拟机栈,但它是为 JVM 使用的 Native 方法 提供服务的。
- 例如:Java 中调用 Thread.start()底层可能调用了 C/C++ 的函数,这些函数就在本地方法栈中运行。
4. 方法区(Method Area / Metaspace) 
✅ 特点: 
- 所有线程共享。
- 存储已加载的类信息(Class)、常量池、静态变量、JIT 编译后的代码等。
与堆的区别: 
- 堆:存放对象实例。
- 方法区:存放类的元数据。
JDK 8 之前 vs JDK 8 之后: 
| 项目 | JDK 8 之前 | JDK 8 及以后 | 
|---|---|---|
| 方法区实现 | 永久代(PermGen) | 元空间(Metaspace) | 
| 内存来源 | JVM 堆内存 | 本地内存(Native) | 
| 默认大小限制 | 小,默认几十 MB | 无上限(受限于物理内存) | 
5. 程序计数器(Program Counter Register) 
- 每个线程都有一个程序计数器。
- 它是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的区域。
- 作用:记录当前线程执行的字节码指令地址(如果是 Native 方法,则为空)。
6. 直接内存(Direct Memory) 
- 不属于 JVM 堆内存的一部分。
- 使用 ByteBuffer.allocateDirect()创建。
- 由操作系统直接管理,速度更快,适合 IO 操作(如 NIO)。
- 但不受 JVM 垃圾回收机制管理,需要手动释放。
🧪 三、举个例子理解整个流程 
java
public class Demo {
    public static void main(String[] args) {
        int age = 20;                // 局部变量,存在栈中
        String name = "John";        // 引用变量在栈中,实际对象在堆中
        Person p = new Person();     // 对象在堆中,p 是引用变量,在栈中
        p.setName("Alice");          // 调用方法,方法调用信息也在栈中
    }
}内存分布如下: 
| 内容 | 存储位置 | 
|---|---|
| age | 栈 | 
| name,p | 栈 | 
| "John"字符串对象 | 堆 | 
| new Person() | 堆 | 
| setName()方法信息 | 方法区 | 
| 当前执行位置 | 程序计数器 | 
🛠 四、常见问题 & 面试高频点 
Q1: Java 中“栈”和“堆”的区别? 
| 项目 | 栈(Stack) | 堆(Heap) | 
|---|---|---|
| 存储内容 | 局部变量、基本类型、对象引用 | 对象实例 | 
| 线程可见性 | 线程私有 | 所有线程共享 | 
| 生命周期 | 方法调用开始 → 方法结束 | 对象创建 → GC 回收 | 
| 内存管理 | 自动分配、自动释放 | 手动分配(new),GC 自动回收 | 
| 速度 | 快 | 相对慢 | 
Q2: String 存在哪?"abc" 和 new String("abc") 有什么不同? 
- "abc":字符串常量池(在堆中,JDK7+ 之后统一在堆中)
- new String("abc"):会在堆中新建一个字符串对象,同时常量池中也可能存在- "abc"
Q3: 为什么会有元空间(Metaspace)代替永久代(PermGen)? 
- PermGen 容易出现 OutOfMemoryError: PermGen space
- 元空间使用本地内存,避免内存不足的问题
- 更容易进行垃圾回收和性能优化
📚 五、总结图解(文字版) 
┌───────────────────────────────┐
│         方法区 (Metaspace)   │ ← 类信息、静态变量、常量池
├───────────────────────────────┤
│             堆               │ ← 所有对象实例
├───────────────────────────────┤
│           线程 1             │
│   ┌───────────────────────┐  │
│   │  程序计数器(PC Reg) │  │
│   ├───────────────────────┤  │
│   │    虚拟机栈(Stack)  │  │ ← 局部变量、方法调用
│   └───────────────────────┘  │
├───────────────────────────────┤
│           线程 2             │
│   ┌───────────────────────┐  │
│   │  程序计数器(PC Reg) │  │
│   ├───────────────────────┤  │
│   │    虚拟机栈(Stack)  │  │
│   └───────────────────────┘  │
└───────────────────────────────┘如果你还想了解:
- 如何查看 JVM 内存使用情况?
- 如何设置 JVM 内存参数(如 -Xms,-Xmx,-XX:MaxMetaspaceSize)?
- JVM 内存调优技巧?
- G1、CMS、ZGC 等垃圾回收器的区别?
欢迎继续提问,我可以一步步带你深入 JVM 世界 😎