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 世界 😎