JAVA JVM内存结构与运行时数据区

11 · JVM 内存结构与运行时数据区

对应学习计划:第一月第3周「JVM 虚拟机(面试高频压轴)」

面试定位:JVM 内存结构是 JVM 面试的第一问,也是 OOM 排查、GC 调优的「地图」。答不清内存分区,后面的题全掉分。


一、本文能帮你回答的面试题

  1. JVM 运行时数据区有哪几块?线程私有/共享怎么分?
  2. 堆(Heap)的分代结构?(新生代/老年代/元空间)
  3. 栈帧里都有什么?(局部变量表/操作数栈/动态连接/返回地址)
  4. -Xms/-Xmx/-Xmn/-XX:MetaspaceSize 都是控制谁的?
  5. 什么对象进老年代?(大对象、长期存活对象)
  6. 常见 OOM:堆溢出、栈溢出、元空间溢出的区别?
  7. 对象创建到被回收的完整生命周期?

二、概念介绍

JVM 按「是否线程私有」把内存分成两块:

复制代码
JVM 内存(运行时数据区)
├── 线程私有(随线程生死)
│   ├── 程序计数器(PC)
│   ├── 虚拟机栈(Stack)
│   └── 本地方法栈(Native Method Stack)
└── 线程共享(随 JVM 生死)
    ├── 堆(Heap)
    ├── 方法区(Metaspace/元空间)
    └── 直接内存(Direct Memory,堆外)

记忆口诀:两栈一计线程有,一堆一区线程共。


三、原理深入

3.1 线程私有区(三兄弟)

① 程序计数器(PC Register)

  • 记录当前线程执行的字节码行号。
  • 唯一不会 OOM 的区域;唯一规范中没有规定任何 OutOfMemoryError 的区域。
  • 多线程切换时靠它恢复执行位置。

② 虚拟机栈(Stack)

  • 每个方法调用 = 压入一个栈帧 ;栈内存默认 -Xss(如 512k~1m)。
  • 栈帧组成(面试常问):
    • 局部变量表:8 大基本类型 + 引用 + returnAddress 的槽位。

    • 操作数栈:字节码指令的运算工作区(如 iadd 从栈取两数相加压回)。

    • 动态连接:指向运行时常量池的方法引用(实现多态)。

    • 方法返回地址:异常表 + 正常返回的跳转位置。

      线程A栈: [栈帧3: methodC()] → 栈帧2: methodB() → 栈帧1: methodA() → main
      栈越深 → 递归太深 → StackOverflowError(不是 OOM 栈)

③ 本地方法栈:给 native 方法用(如 Unsafe 底层、JNI),与虚拟机栈对称。

3.2 线程共享区

① 堆(Heap)------JVM 内存大头,GC 主战场

复制代码
堆(默认 -Xms = 起始,-Xmx = 最大,通常两者设相等避免动态伸缩)
├── 新生代(Young)
│   ├── Eden(伊甸园,新对象都先去这)
│   ├── S0(Survivor From)
│   └── S1(Survivor To,S0/S1 轮换)
└── 老年代(Old)

对象晋升路线:

复制代码
new 对象 → Eden → (MinorGC 存活 + 年龄+1)→ S0/S1 反复换 → 年龄≥15(默认 MaxTenuringThreshold)→ Old
大对象(-XX:PretenureSizeThreshold)→ 直接 Old

② 方法区 / 元空间(Metaspace,JDK8 起)

  • 存放:类元信息(Class 对象、方法字节码)、运行时常量池、静态变量(JDK7 后移到堆)。
  • JDK7 叫永久代 Permanent Gen,JDK8 改成元空间(直接内存) ------为什么要改?永久代有固定上限且容易 OOM(类加载器泄漏、动态代理过多);元空间使用本地内存,默认只看 OS 内存,并支持动态扩容。
  • 控制:-XX:MetaspaceSize(初始/触发阈值)、-XX:MaxMetaspaceSize(上限)。

③ 直接内存(Direct Memory)

  • NIO 的 DirectByteBuffer、Netty 的堆外内存都在这。
  • 不受 -Xmx 约束,受 OS 物理内存限制;-XX:MaxDirectMemorySize 控制。
  • 溢出特征:OutOfMemoryError: Direct buffer memory(比较坑:GC 不管它,只能靠 Cleaner 回收)。

3.3 常见参数速查

参数 控制 默认(典型)
-Xms 堆初始大小 物理内存 1/64
-Xmx 堆最大大小 物理内存 1/4
-Xmn 新生代大小 堆 1/3
-Xss 栈大小/线程栈 1MB(JDK8)
-XX:MetaspaceSize 元空间初始 21MB
-XX:MaxMetaspaceSize 元空间上限 无上限(OS 决定)
-XX:MaxDirectMemorySize 直接内存上限 略小于堆

生产铁律:-Xms 必须等于 -Xmx,避免堆动态伸缩导致的频繁 FullGC 与性能抖动。

3.4 对象的完整一生(答题剧本)

复制代码
① 类加载 → ② 分配内存(指针碰撞/空闲列表)→ ③ 内存零值初始化
→ ④ 设置对象头(Mark Word + 类型指针)→ ⑤ 执行 <init> 构造
→ 使用中:可达性分析(GC Roots 引用链)
→ 死亡时:标记 + 清除 → 新生代复制/老年代标记整理 → 内存归还

GC Roots(可达性分析的根):

  • 栈(虚拟机栈/本地方法栈)中引用的对象。
  • 静态变量、常量引用。
  • 活跃线程(Thread)、JNI 引用(GC Roots 背下来,面试必问)。

3.5 四种 OOM 一表对比

溢出类型 报错 原因 排查
堆溢出 Java heap space 对象太多/内存泄漏 堆 dump + MAT 分析
栈溢出 StackOverflowError 递归无出口/栈太深 看异常栈
元空间溢出 Metaspace 动态代理/CGLIB 类无限生成 检查类加载器泄漏
直接内存溢出 Direct buffer memory 堆外缓存未释放 检查 NIO/Netty 是否泄漏

四、实战例子

实战 1:看运行时参数(打印 JVM 配置)

java 复制代码
public class JvmInfo {
    public static void main(String[] args) {
        long max = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        long total = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long free = Runtime.getRuntime().freeMemory() / 1024 / 1024;
        System.out.println("堆最大: " + max + "MB");
        System.out.println("当前堆: " + total + "MB");
        System.out.println("空闲: " + free + "MB");
        System.out.println("核数: " + Runtime.getRuntime().availableProcessors());
    }
}
// 配合 jcmd <pid> VM.flags 或 jmap -heap <pid> 查看真实参数

实战 2:栈溢出复现(StackOverflowError)

java 复制代码
public class StackOverflowDemo {
    static int depth = 0;
    static void recurse() {
        depth++;
        recurse();            // 无限递归
    }
    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("栈溢出,递归深度: " + depth);
        }
    }
}
// 换 -Xss512k 减少栈大小 → 深度变小 → 证明栈空间由 -Xss 控制

实战 3:对象晋升到老年代(观察 GC 日志)

bash 复制代码
# 启动参数:xms/xmx 相等避免伸缩,打印 GC 详情
java -Xms256m -Xmx256m -Xmn128m -XX:+UseParallelGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps Demo

# 反复创建大对象(2MB × 100 次,超过 Eden 一半会直接进老年代)
# 只见 MajorGC(Old)出现:证明大对象越过了 Survivor
java 复制代码
public class BigObjectDemo {
    private static final int CHUNK = 2 * 1024 * 1024;   // 2MB
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            byte[] big = new byte[CHUNK];               // 大对象
        }
    }
}

五、面试话术(背下来)

Q:JVM 内存结构?

「分线程私有和共享。私有:程序计数器、虚拟机栈、本地方法栈。共享:堆(新生代/老年代)、方法区(JDK8 是元空间)、还有堆外直接内存。栈管线程方法调用,帧里是局部变量表和操作数栈;堆管对象,GC 主战场;元空间放类元信息,走本地内存。」

Q:为什么要元空间替代永久代?

「永久代是堆内固定大小,容易 OOM(类加载器泄漏时);JDK8 的元空间直接使用本地内存,不受堆大小限制,默认上限是 OS 内存,而且字符串常量池、静态变量等已挪到堆,从数据归属上也更合理。」

Q:对象怎么进老年代?

「三种:一是 MinorGC 后存活且年龄达到阈值(默认15)被晋升;二是大对象超过 -XX:PretenureSizeThreshold 直接进老年代(Eden 放不下);三是 Survivor 区放不下的对象直接进老年代(动态年龄判定规则)。」

Q:怎么排查堆 OOM?

「加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath 生成 dump,用 MAT/VisualVM 查大对象和引用链,看是内存泄漏(该回收没回收)还是内存不足(真的大数据量)。线上有「gc.log + dump 快照 + 压测模拟」标准流程。」


六、踩坑总结(易错点)

# 坑 说明
1 -Xms 不等于 -Xmx 堆动态伸缩,缩时卡顿 + FullGC 频繁
2 盲目增大堆 堆越大 GC 单次 STW 越长;配合吞吐目标调,不是越大越好
3 以为方法区没了 只是换名成元空间,JDK8 后从堆内挪到本地内存
4 栈溢出当 OOM 查 是 Error 不是 OOM,无限递归/深链首查代码
5 忽略直接内存 Netty/NIO 泄漏报 Direct buffer memory,GC日志里看似正常但进程 OOM
6 程序计数器有 OOM 没有!它是唯一不会溢出的区域
7 混淆 StackOverflowError 和 OutOfMemoryError: unable to create new native thread 后者是线程太多耗尽操作系统线程/内存,可能是你开了无限线程池

七、课后作业

  1. 手画一张「JVM 内存结构图」,标出各区域放什么 + 控制参数。
  2. 写一个堆 OOM 的 Demo,用 -Xmx64m -XX:+HeapDumpOnOutOfMemoryError 生成 dump,再写一个 MAT 的分析结论。
  3. 用 jmap -heap 观察一个运行中 SpringBoot 的堆分区与新生代/老年代占比。
  4. 验证:什么时候新生代对象会直接去老年代(提示:Survivor 空间不足以容纳时)。
相关推荐
slongzhang_1 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
光锥智能1 小时前
海康机器人举行2026移动机器人新品发布会:箱式存拣升级,「明智」系列发布
java·前端·机器人
行思理1 小时前
Nginx 跨站请求(CORS)配置完全指南:从原理到生产实践
java·前端·nginx
小溪学编程1 小时前
Spring @Bean 与 @Component 注解的区别详解
java·后端·spring
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 圈子互动章总结与自检清单
java·bootstrap·交友
云和数据.ChenGuang2 小时前
Python 常见特殊方法详解:__new__、__init__、__del__、__str__、__repr__ 与 __getattribute__
开发语言·人工智能·python·1024程序员节
xyouyuweizhu2 小时前
字节流与字符流:Java IO 的两条主线该怎么选
java·开发语言·python
代码方舟3 小时前
零信任架构实战:基于天远人企关联构建自动化供应链核心企业关联穿透与利益冲突审查网关
java·人工智能·架构·自动化
多弗朗皮卡丘3 小时前
C++的IO流
开发语言·c++·io流·文件io流