JVM 运行时数据区

候选人小张在面试阿里 P6 时,面试官翻到简历上"JVM 调优经验"这一行,开口问道:

"JVM 运行时数据区有哪些?它们的作用是什么?"

小张说:"有堆、栈、方法区..."面试官追问:"堆和栈的区别是什么?"

小张说:"堆存放对象,栈存放局部变量..."面试官继续追问:"JDK 8 对方法区做了什么变更?Metaspace 和 PermGen 的区别是什么?"

小张答不上来了...

一、核心问题:运行时数据区有哪些 🔴

1.1 问题拆解

第一层:五大区域(有哪些?)
  "JVM 运行时数据区由哪几部分组成?"
  考察点:堆、栈、方法区、PC寄存器、本地方法栈

第二层:线程私有 vs 共享(怎么分?)
  "哪些区域是线程私有的?哪些是共享的?"
  考察点:线程隔离、安全性

第三层:JDK 8 变更(有什么变化?)
  "JDK 8 为什么移除 PermGen?Metaspace 是什么?"
  考察点:字符串常量池、静态变量、方法区位置

1.2 ❌ 错误示范

候选人原话 A:"栈存放对象,堆存放局部变量。"

问题诊断:说反了。栈存放局部变量(基本类型引用、对象引用)、方法调用栈帧;堆存放所有 new 出来的对象。

候选人原话 B:"方法区在堆里面。"

问题诊断:方法区是 JVM 规范中定义的逻辑区域,在 JDK 7 及之前实现为 PermGen(堆的一部分),JDK 8 移除了 PermGen,方法区使用 Metaspace 实现(位于本地内存,不在堆中)。

1.3 标准回答

P5 级别:五大区域

JVM 运行时数据区

graph TD
    subgraph JVM进程内存
        subgraph 线程共享
            A1[堆 Heap<br/>对象实例、数组]
            A2[方法区 Method Area<br/>类信息、常量、静态变量<br/>JDK8 = Metaspace]
        end

        subgraph 线程私有
            B1[Java虚拟机栈<br/>局部变量、操作数栈]
            B2[本地方法栈<br/>Native方法栈帧]
            B3[程序计数器 PC<br/>当前指令地址]
        end

        A3[直接内存<br/>NIO direct buffer]
    end
区域线程关系存储内容异常
堆 Heap线程共享所有对象实例、数组OutOfMemoryError
方法区 Method Area线程共享类信息、常量、静态变量、JIT 编译代码OutOfMemoryError(JDK 7 PermGen)
Java 虚拟机栈线程私有方法调用的栈帧(局部变量表、操作数栈等)StackOverflowError / OutOfMemoryError
本地方法栈线程私有Native 方法的栈帧StackOverflowError / OutOfMemoryError
程序计数器线程私有当前指令地址无(无 OOM)

P6 级别:堆与栈的详细对比

堆(Heap)

JVM 管理的最大内存区域,所有对象实例和数组都在堆上分配。

// 堆上分配
Object obj = new Object();  // 对象实例在堆中,obj 引用在栈(局部变量表)中
int[] arr = new int[1024];  // 数组在堆中

堆是 GC 的主要工作区域,分代结构:

graph TD
    E[堆 Heap] --> S[Eden 区<br/>新对象分配]
    E --> ST[Survivor 区<br/>S0 / S1]
    E --> O[Old Generation<br/>长期存活对象]
    S -->|Minor GC 后| ST
    ST -->|年龄阈值| O

Java 虚拟机栈(Java VM Stack)

每个线程有自己的虚拟机栈,栈由栈帧(Stack Frame)组成。每调用一个方法,创建一个栈帧入栈;方法返回时,栈帧出栈。

public class StackDemo {
    public int methodA() {
        int a = 1;
        return methodB(a);  // methodA 的栈帧在 methodB 调用前入栈
    }

    public int methodB(int x) {
        int y = x + 2;
        return y;  // methodB 的栈帧出栈,methodA 的栈帧恢复
    }
}

程序计数器(PC Register)

每个线程私有,记录当前线程执行的字节码指令地址。如果当前方法是 native 方法,计数器值为 undefined。

P7 级别:JDK 8 的方法区变更

JDK 7 之前:PermGen(永久代)

方法区在 JDK 7 及之前的 HotSpot 实现是 PermGen(永久代),位于堆内存中。

PermGen 的三大问题

  1. 固定大小:PermGen 大小在 JVM 启动时固定(-XX:MaxPermSize),难以调优
  2. 字符串常量池在 PermGen:字符串常量池和类元数据争夺同一块内存
  3. Full GC 时收集 PermGen:增加了 Full GC 的负担

JDK 8 之后:Metaspace(元空间)

graph TD
    JDK7["JDK 7"] --> P[堆 Heap]
    P --> PermGen[PermGen<br/>方法区实现]
    P --> YoungEden[Eden]
    P --> OldGen[Old Gen]

    JDK8["JDK 8"] --> Heap8[堆 Heap<br/>对象实例、数组]
    Heap8 --> Young8[Eden + Survivor]

    JDK8 --> Meta[Metaspace<br/>本地内存<br/>使用操作系统的内存]

    Meta --> ClassMeta[类元数据]
    Meta --> ConstantPool[字符串常量池<br/>JDK 8 移到这里]
    Meta --> CodeCache[JIT 编译代码]

Metaspace 的优势

  1. 使用本地内存:不再占用堆空间,默认无上限(受物理内存限制)
  2. 自动调整:MetaspaceSize 可以自动调整
  3. 独立 GC:类元数据的回收不触发 Full GC

但 Metaspace 仍可能 OOM

# Metaspace OOM 的典型原因
# 大量动态类生成(CGlib、JSP、模板引擎)
-XX:MaxMetaspaceSize=256m  # 限制 Metaspace 大小
-XX:MetaspaceSize=128m     # 初始阈值

静态变量的归宿

JDK 7 将字符串常量池从 PermGen 移到了堆中,但静态变量呢?

  • JDK 7:静态变量仍然在 PermGen(方法区)
  • JDK 8:静态变量随类一起移到了 Metaspace,但对象引用(即引用类型静态变量指向的堆对象)仍然在堆中
public class StaticDemo {
    static Object obj = new Object();  // 引用在 Metaspace,对象在堆
    static String str = "hello";       // 字符串常量在 Metaspace
    static int num = 42;               // 基本类型在 Metaspace
}

【面试官心理】 这道题我能问到 P7 级别,是因为 JDK 8 的方法区变更是近年最重要的 JVM 变更之一。能说清 Metaspace vs PermGen 差异的候选人说明他跟进了 JDK 演进。能解释字符串常量池迁移的候选人说明他理解了方法区的内部结构。

1.4 追问升级

追问 1:直接内存(Direct Memory)是什么?

NIO 的 DirectByteBuffer 使用直接内存,分配在堆外,不受 GC 管理,但通过 Cleaner 间接引用,GC 时会释放。直接内存大小由 -XX:MaxDirectMemorySize 控制,默认与堆最大大小相同。

追问 2:为什么需要本地方法栈?

JVM 需要调用本地(C/C++)代码来完成一些 Java 无法直接完成的任务(如系统调用、底层库)。本地方法栈用于支持 native 方法的调用,与 Java 虚拟机栈类似但服务于不同的方法。

二、生产避坑 🟡

2.1 字符串常量池导致的 OOM(JDK 7)

JDK 7 中字符串常量池在 PermGen 中,如果大量字符串驻留(如 String.intern() 滥用),会导致 PermGen OOM。

解决:升级到 JDK 8+,或增大 PermGen。

2.2 Metaspace OOM(JDK 8+)

// 危险操作:大量动态类生成
public static void main(String[] args) {
    while (true) {
        CglibBean proxy = new CglibBean();  // 每个代理类都是一个新类
        // 大量生成代理类 → Metaspace OOM
    }
}

三、运行时数据区参数配置 🟢

3.1 常用参数

# 堆大小
-Xms4g -Xmx4g           # 初始堆和最大堆

# Metaspace(JDK 8+)
-XX:MetaspaceSize=256m   # 初始阈值
-XX:MaxMetaspaceSize=512m # 最大限制

# 直接内存
-XX:MaxDirectMemorySize=1g

3.2 监控工具

jstat -gc <pid>          # 查看 GC 和 Metaspace 统计
jmap -heap <pid>          # 查看堆和 Metaspace 配置
jcmd <pid> VM.native_memory # 查看详细内存使用
💡

面试加分点:能说出"JDK 11 引入了 Epsilon GC(无操作 GC),它不回收任何内存,用于短生命周期程序和性能测试",说明他关注了 JDK 新特性。

⚠️

面试陷阱:被问到"方法区存不存在 OOM",很多人会说"不存在"。准确答案是:方法区在 JDK 7 PermGen 时会 OOM(OutOfMemoryError: PermGen space),JDK 8 Metaspace 也会 OOM(OutOfMemoryError: Metaspace)