# 1. JVM 运行时数据区有哪些?每个区域的作用是什么?
# 标准答案
JVM 运行时数据区主要包括 方法区(Metaspace)、堆(Heap)、虚拟机栈(Java Stack)、本地方法栈(Native Stack)、程序计数器(PC Register) 五个部分。它们各自承担不同的存储职责:
- 方法区(Metaspace):存储类元信息(Class Metadata)、常量池(Runtime Constant Pool)、静态变量等,JDK 8 之后移至本地内存。
- 堆(Heap):存放对象实例,是 GC 主要管理的区域,划分为 年轻代(Young Generation)和老年代(Old Generation)。
- 虚拟机栈(Java Stack):存放方法调用的栈帧,包括局部变量表、操作数栈、动态链接等,线程私有。
- 本地方法栈(Native Stack):用于执行 JNI(Java Native Interface)调用的 C/C++ 代码,与虚拟机栈类似但执行 native 方法。
- 程序计数器(PC Register):记录当前线程执行的字节码行号,多线程环境下,保证每个线程有独立的 PC 寄存器。
# 答案解析
JVM 运行时数据区划分的目的是 满足 Java 代码执行过程中不同类型数据的存储需求,同时优化 GC 处理,提升性能。
# 1. 方法区(Metaspace)
作用:
- 存储 类的元数据(类名、字段、方法、访问修饰符)、运行时常量池、静态变量等。
- 运行时动态生成的类(如
Proxy
代理类)也存储在方法区。 - 线程共享,JDK 8 以前叫 永久代(PermGen),JDK 8 之后改为 元空间(Metaspace),使用 本地内存 代替堆内存。
JDK 7 vs JDK 8 变更:
- JDK 7 及之前:
PermGen
(受 JVM 选项-XX:MaxPermSize
限制)。 - JDK 8 及之后:
Metaspace
(受-XX:MaxMetaspaceSize
控制,可动态扩展)。
为什么移除永久代?
- 受
Heap
限制,难以调整,容易OutOfMemoryError
。 Metaspace
使用本地内存,减少 Full GC 的压力。
# 2. 堆(Heap)
作用:
- 存储所有 Java 对象实例,几乎所有
new
创建的对象都会分配在堆中。 - 线程共享,由 GC 负责回收,按生命周期划分为 新生代(Young Gen)和老年代(Old Gen)。
内部分代(分区策略):
- Eden 区:新对象大多分配在 Eden 区。
- Survivor 区(S0、S1):存活对象从 Eden 复制到 Survivor,经过多次复制晋升到老年代。
- 老年代:长期存活的大对象、经历多次 GC 仍存活的对象会被放入老年代。
GC 策略:
- Minor GC:发生在年轻代,回收短生命周期对象。
- Major GC / Full GC:回收老年代,清理长期存活对象,速度慢。
# 3. 虚拟机栈(Java Stack)
作用:
- 存储 方法调用的栈帧,每个方法调用都会创建一个新的栈帧。
- 栈帧包括 局部变量表、操作数栈、动态链接、返回地址 等信息。
- 线程私有,生命周期随线程销毁。
局部变量表:
- 存储 基本数据类型(int、long、float、double)和对象引用(reference)。
- 受
-Xss
影响,超过限制会导致StackOverflowError
。
常见问题:
- 递归过深导致
StackOverflowError
:栈帧超出栈大小限制。 - 过小
-Xss
会限制并发线程数:栈空间越大,可创建的线程数越少。
# 4. 本地方法栈(Native Stack)
作用:
- 专门为执行 Native 方法(JNI 调用)而设计。
- 存储 C/C++ 方法的调用栈,线程私有。
- 可能抛出
StackOverflowError
或OutOfMemoryError
。
# 5. 程序计数器(PC Register)
作用:
- 记录当前线程执行的字节码指令地址。
- 线程私有,保证多线程切换时,每个线程能恢复到正确的执行位置。
- 如果执行的是 native 方法,则 PC 计数器为空。
# 常见错误
OutOfMemoryError: Java heap space
- 发生在堆(Heap),通常是对象过多、没有及时 GC。
- 优化方法:使用
-Xmx
限制最大堆大小,结合 GC 调优。
OutOfMemoryError: Metaspace
(JDK 8 及以后)- 由于
Metaspace
过大,导致本地内存耗尽。 - 优化方法:调整
-XX:MaxMetaspaceSize
,减少动态生成类(如 CGLIB 代理)。
- 由于
StackOverflowError
- 递归调用过深,导致 Java 栈空间耗尽。
- 优化方法:优化递归算法、增加
-Xss
线程栈大小。
# 最佳实践
堆优化(Heap)
- 调整
-Xms
和-Xmx
,保证合理的内存分配,避免频繁 GC。 - 对象池化,减少不必要的对象创建(如
ThreadLocal
)。
- 调整
方法区优化(Metaspace)
- 限制动态生成类(如 CGLIB、Javassist 代理)。
- 调整
-XX:MetaspaceSize
,避免Metaspace
过大占用本地内存。
栈优化(Stack)
- 递归改为循环,避免
StackOverflowError
。 - 调整
-Xss
以适应深度递归。
- 递归改为循环,避免
# 深入追问
- 为什么 JDK 8 移除了永久代(PermGen),而采用
Metaspace
? - JVM 的
TLAB
(Thread Local Allocation Buffer)如何优化对象分配? - GC 是如何管理堆内存的?CMS 和 G1 的区别是什么?
# 相关面试题
- JVM 堆(Heap)和栈(Stack)的区别是什么?
- GC 在方法区(Metaspace)如何回收无用类?
- JVM 如何通过
-XX:+UseG1GC
来优化 GC ?