第61篇:JVM内存区域(2026版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第60篇:并发编程面试压轴题 |
➡️ 下一篇:第62篇:垃圾回收器详解(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第51~60篇拿下了并发编程,本篇开启JVM调优与故障排查专题。JVM内存区域是理解GC、OOM、性能调优的基石,搞懂这五大区域,你就掌握了JVM的“地图”:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| 五大区域 | JVM内存分哪几块? | 程序计数器、虚拟机栈、本地方法栈、堆、元空间(JDK 8+) |
| 线程私有 | 哪些区域是线程独享的? | 程序计数器、虚拟机栈、本地方法栈(每个线程各有一份) |
| 线程共享 | 哪些区域是所有线程共用的? | 堆、元空间(所有线程共用,需考虑并发安全) |
| 堆 | 对象存在哪里? | 堆——所有new出来的对象和数组都在这里 |
| 栈 | 方法调用存在哪里? | 虚拟机栈——每个方法调用对应一个栈帧 |
| 元空间 | 类的信息存在哪里? | 元空间——存储类元数据、常量池、静态变量 |
| OOM场景 | 哪些区域会内存溢出? | 堆(OOM)、栈(StackOverflowError/OOM)、元空间(OOM) |
| 面试最爱问 | 高频考点有哪些? | 见文末 🎤 小节 |
一、核心知识点
JVM运行时数据区(JDK 8+):
JVM在执行Java程序时,会将其管理的内存划分为5个核心运行时数据区,根据线程私有/共享特性可分为两大类:
| 分类 | 包含区域 | 生命周期 | 垃圾回收 |
|---|---|---|---|
| 线程私有 | 程序计数器、虚拟机栈、本地方法栈 | 与线程同生共死 | 基本不回收 |
| 线程共享 | 堆、元空间 | 与JVM进程同生共死 | 主要回收区域 |
💡JDK 8 核心变更:永久代(PermGen)被彻底移除,方法区的实现改为元空间(Metaspace),直接使用本地内存(Native Memory)而非JVM堆内存。
五大区域速览:
二、通俗讲解(1分钟开心学)
1. 把JVM内存想象成一个图书馆
程序计数器:你正在读的书签——记录你读到第几页第几行(当前执行到哪条字节码)。线程切换时,书签帮你找回刚才读到哪儿了。这本书签极小,而且永远不会弄丢(不会OOM)。
虚拟机栈:你的“工作台”——每调用一个方法(读一本书),就在工作台上放一个文件夹(栈帧),方法执行完就把文件夹拿走。工作台太小时,文件夹叠太多会塌(StackOverflowError)。
本地方法栈:专门处理“外文书籍”(Native方法,如C/C++写的代码)的工作台。
堆:图书馆的大仓库——所有
new出来的对象和数组都存放在这里。仓库太大或太小都会出问题(OOM),所以需要管理员(GC)定期清理没人要的东西。元空间:图书馆的目录卡柜——存储类的元数据(类名、方法、字段等)。JDK 8之前这个柜子放在仓库里(永久代),容易占满;JDK 8后搬到隔壁的独立房间(本地内存),不再占用仓库空间。
2. 一句话记住五大区域
程序计数器记位置,虚拟机栈管方法,本地方法栈管Native,堆存对象,元空间存类信息。
三、实操代码案例 + 场景说明
3.1 栈溢出(StackOverflowError)演示
无限递归是触发栈溢出的最典型场景:
publicclassStackOverflowDemo{privateintdepth=0;publicvoidrecursion(){depth++;recursion();// 无限递归}publicstaticvoidmain(String[]args){StackOverflowDemodemo=newStackOverflowDemo();try{demo.recursion();}catch(StackOverflowErrore){System.out.println("栈溢出!递归深度:"+demo.depth);// 输出:栈溢出!递归深度:约 10000~20000(取决于栈大小)}}}💡原因:每个方法调用都会创建一个栈帧压入虚拟机栈,无限递归导致栈帧不断堆积,超过栈的最大深度时抛出
StackOverflowError。可通过-Xss参数调整栈大小(如-Xss1m)。
3.2 堆溢出(OutOfMemoryError)演示
不断创建对象并保持引用,是触发堆溢出的典型场景:
importjava.util.ArrayList;importjava.util.List;publicclassHeapOOMDemo{publicstaticvoidmain(String[]args){List<byte[]>list=newArrayList<>();try{while(true){// 每次分配 1MB,无限循环list.add(newbyte[1024*1024]);}}catch(OutOfMemoryErrore){System.out.println("堆溢出!已创建对象数:"+list.size());// 输出:堆溢出!已创建对象数:约 XXXX(取决于 -Xmx 设置)}}}💡原因:堆内存被不断创建的对象占满,且对象一直被
list引用,GC无法回收,最终抛出OutOfMemoryError: Java heap space。可通过-Xmx参数调整堆最大大小。
3.3 查看JVM内存参数
# 查看JVM默认参数java-XX:+PrintFlagsFinal-version|grep-E"HeapSize|Metaspace|StackSize"# 设置JVM参数运行程序java-Xms256m-Xmx1024m-Xss1m-XX:MetaspaceSize=128m-XX:MaxMetaspaceSize=256m HeapOOMDemo| 参数 | 含义 | 示例 |
|---|---|---|
-Xms | 初始堆大小 | -Xms256m |
-Xmx | 最大堆大小 | -Xmx1024m |
-Xss | 每个线程的栈大小 | -Xss1m |
-XX:MetaspaceSize | 元空间初始大小 | -XX:MetaspaceSize=128m |
-XX:MaxMetaspaceSize | 元空间最大大小 | -XX:MaxMetaspaceSize=256m |
四、避坑要点
| 错误/误区 | 后果 | 正确做法 |
|---|---|---|
| 认为程序计数器会OOM | 概念错误 | 程序计数器是唯一不会OOM的区域 |
| 认为元空间在堆内 | 调参错误 | JDK 8+元空间使用本地内存,不受堆大小限制 |
| 无限递归不设终止条件 | StackOverflowError | 控制递归深度或改为迭代 |
| 大量创建对象不释放引用 | OutOfMemoryError | 及时置null或使用局部变量 |
| 混淆堆和栈的存储内容 | 理解偏差 | 堆存对象,栈存局部变量和方法调用 |
五、面试高频考点
Q1:JVM内存模型包含哪几个区域?哪些是线程私有的?
五大区域:程序计数器、虚拟机栈、本地方法栈、堆、元空间。线程私有:程序计数器、虚拟机栈、本地方法栈(每个线程独有)。线程共享:堆、元空间(所有线程共用)。
Q2:堆和栈的区别?
①用途:栈存方法执行信息(栈帧、局部变量),堆存对象实例和数组。②生命周期:栈随方法调用创建/销毁,堆随对象创建/GC回收。③线程归属:栈线程私有,堆线程共享。④异常:栈溢出
StackOverflowError,堆溢出OutOfMemoryError。
Q3:元空间和永久代的区别?
①内存位置:永久代在JVM堆内存中,元空间在本地内存中。②大小限制:永久代受
-XX:MaxPermSize限制,元空间默认无上限(受物理内存限制)。③OOM风险:永久代更容易OOM,元空间更灵活。
Q4:程序计数器为什么不会OOM?
程序计数器只记录当前线程执行的字节码指令地址,内存空间极小,JVM规范未对该区域规定任何内存限制,也不会抛出
OutOfMemoryError。
Q5:栈帧中包含哪些内容?
栈帧包含四个部分:局部变量表(存储方法参数和局部变量)、操作数栈(执行运算时的临时数据)、动态链接(将符号引用转为直接引用)、方法返回地址(方法执行完后的返回位置)。
Q6:什么情况下会抛出OutOfMemoryError: unable to create native thread?
创建了过多的线程,JVM无法为新线程分配栈内存时抛出。注意:这是本地内存不足导致的,而非虚拟机栈本身的内存耗尽。
🎤 面试官追问陷阱(加分题)
追问1:“String s = new String("abc")创建了几个对象?分别存在哪里?”
👉 两个对象。
"abc"字面量在字符串常量池(元空间中)创建一个对象;new String()在堆中创建一个对象,引用指向池中的字面量。s引用变量存在虚拟机栈的局部变量表中。
追问2:“static变量存在哪个区域?”
👉 JDK 8之后,
static变量(基本类型值直接存储,对象引用存储指针)存储在元空间中。static修饰的对象引用指向的对象本身仍在堆中。
六、练习题
分析题:写出以下代码中变量
a、b、obj、str在JVM内存中的位置(栈/堆/元空间)。classTest{staticinta=10;intb=20;voidmethod(){intc=30;Objectobj=newObject();Stringstr="hello";}}💡思路:
a(静态变量)在元空间;b(实例变量)在堆(随对象);c(局部变量)在栈;obj引用在栈、对象在堆;str引用在栈、字面量"hello"在字符串常量池(元空间)。代码题:编写一个程序,分别触发
StackOverflowError和OutOfMemoryError,并观察错误信息。场景题:某线上服务频繁报
OutOfMemoryError: Metaspace,分析可能原因和解决方案。💡思路:元空间存储类元数据,如果应用动态加载了大量类(如热部署、动态代理、Groovy脚本等),元空间可能被占满。解决方案:增大
-XX:MaxMetaspaceSize,或排查是否存在类加载器泄漏。
📊 你的学习进度
- 当前:第61篇 / 共108篇 ·进阶篇:JVM调优与故障排查(第61~70篇)
- ✅ 已完成:基础篇44篇 + 第45~61篇
- 📖 正在学:第61篇
- ⏳ 待学习:第62~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第62篇:垃圾回收器详解》
内容简介:Serial、Parallel、CMS、G1、ZGC 等垃圾回收器原理与选型,GC日志分析,JVM调参实战。
👉JVM调优专题持续深入,拿下垃圾回收器!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!