# 1. JVM 运行时数据区有哪些?每个区域的作用是什么?

# 标准答案

JVM 运行时数据区主要包括 方法区(Metaspace)、堆(Heap)、虚拟机栈(Java Stack)、本地方法栈(Native Stack)、程序计数器(PC Register) 五个部分。它们各自承担不同的存储职责:

  1. 方法区(Metaspace):存储类元信息(Class Metadata)、常量池(Runtime Constant Pool)、静态变量等,JDK 8 之后移至本地内存。
  2. 堆(Heap):存放对象实例,是 GC 主要管理的区域,划分为 年轻代(Young Generation)和老年代(Old Generation)
  3. 虚拟机栈(Java Stack):存放方法调用的栈帧,包括局部变量表、操作数栈、动态链接等,线程私有。
  4. 本地方法栈(Native Stack):用于执行 JNI(Java Native Interface)调用的 C/C++ 代码,与虚拟机栈类似但执行 native 方法。
  5. 程序计数器(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++ 方法的调用栈,线程私有。
  • 可能抛出 StackOverflowErrorOutOfMemoryError

# 5. 程序计数器(PC Register)

作用

  • 记录当前线程执行的字节码指令地址。
  • 线程私有,保证多线程切换时,每个线程能恢复到正确的执行位置。
  • 如果执行的是 native 方法,则 PC 计数器为空。

# 常见错误

  1. OutOfMemoryError: Java heap space

    • 发生在堆(Heap),通常是对象过多、没有及时 GC。
    • 优化方法:使用 -Xmx 限制最大堆大小,结合 GC 调优。
  2. OutOfMemoryError: Metaspace(JDK 8 及以后)

    • 由于 Metaspace 过大,导致本地内存耗尽。
    • 优化方法:调整 -XX:MaxMetaspaceSize,减少动态生成类(如 CGLIB 代理)。
  3. StackOverflowError

    • 递归调用过深,导致 Java 栈空间耗尽。
    • 优化方法:优化递归算法、增加 -Xss 线程栈大小。

# 最佳实践

  1. 堆优化(Heap)

    • 调整 -Xms-Xmx,保证合理的内存分配,避免频繁 GC。
    • 对象池化,减少不必要的对象创建(如 ThreadLocal)。
  2. 方法区优化(Metaspace)

    • 限制动态生成类(如 CGLIB、Javassist 代理)。
    • 调整 -XX:MetaspaceSize,避免 Metaspace 过大占用本地内存。
  3. 栈优化(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 ?