目录
[1. 程序计数器(PC Register)](#1. 程序计数器(PC Register))
[2. 虚拟机栈(VM Stack)](#2. 虚拟机栈(VM Stack))
[3. 本地方法栈(Native Method Stack)](#3. 本地方法栈(Native Method Stack))
[4. 堆(Heap)](#4. 堆(Heap))
[GC 主战场](#GC 主战场)
[5. 方法区(Method Area)](#5. 方法区(Method Area))
JVM 把内存分成 5 块,各管各的。
JVM 一启动,会向操作系统要一块内存。然后它把这块内存切成五份,每份干不同的事。这就是所谓运行时数据区------其实就是"运行时 JVM 拿到的内存,分成五个抽屉"。
五块总览:先看大图
切之前先回答一个关键问题:这五块是"一人一份",还是"大家共用"?
答案:
-
线程私有(一人一份):程序计数器、虚拟机栈、本地方法栈
-
线程共享(大家共用):堆、方法区
记住这个划分,能解释很多事------比如为什么并发问题主要发生在堆和方法区,不在栈上。
┌─────────────────────────────────────────────────────────────┐
│ JVM 进程内存 │
├─────────────────────────────────┬───────────────────────────┤
│ 线程私有(每个线程一份) │ 线程共享(所有线程共用) │
├─────────────────────────────────┼───────────────────────────┤
│ 1. 程序计数器 PC │ │
│ 当前执行到字节码的第几行 │ 4. 堆 Heap │
│ │ 所有 new 出来的对象 │
│ 2. 虚拟机栈 VM Stack │ 数组、字符串常量池 │
│ 每个方法调用压一个栈帧 │ GC 主战场 │
│ │ (新生代 + 老年代) │
│ 3. 本地方法栈 │ │
│ 服务 native 方法 │ 5. 方法区 Method Area │
│ (同 VM 栈) │ 类的元信息、常量池 │
│ │ JDK 8+ 由元空间实现 │
│ 不会并发冲突 · 但会 SOF │ 并发问题主战场 · 主要 OOM │
└─────────────────────────────────┴───────────────────────────┘
1. 程序计数器(PC Register)
是什么
一句话:记录"当前线程执行到字节码的第几行了"。
比喻:你读一本书,看到一半去上厕所,回来怎么知道读到哪?你夹了个书签。PC 就是这个书签------告诉 JVM:"下一条要执行的字节码在这儿。"
存什么
-
一个数字,指向当前正在执行的字节码地址
-
如果当前线程执行的是 native 方法(C/C++ 写的),PC 值是
undefined
谁来用
每个线程一个,线程私有。CPU 时间片轮转,线程被切走又切回来,能从断点继续执行,靠的就是 PC 记住了位置。如果 PC 是共享的,线程 A 切走时被线程 B 改了,回来就懵了------所以必须一人一份。
会出什么问题
✅ 不会出错。 这是 JVM 内存里唯一不会 OOM 的区域。JVM 规范白纸黑字写明了。面试常考。
一个小例子
0: iconst_1 // 把常量 1 压入操作数栈
1: istore_1 // 把栈顶存到局部变量表 slot 1
2: iload_1 // 把 slot 1 加载到操作数栈
3: ireturn // 返回栈顶
PC 从 0 开始,每执行一条字节码就更新到下一条地址。线程被切走又切回来时,CPU 看 PC 就知道从哪继续。
2. 虚拟机栈(VM Stack)
是什么
一句话:每个方法调用会创建一个"栈帧"压入这个栈,方法返回时弹出。
比喻 :抽屉堆叠。每开一个抽屉放方法的东西,方法结束就关掉抽屉。最底下那个抽屉是 main 方法。
栈帧里装什么
-
局部变量表 :方法的局部变量,包括
this(实例方法)、参数、方法内定义的变量 -
操作数栈 :字节码指令的"工作台",比如
a + b会先把 a、b 压栈再 iadd -
动态链接:指向运行时常量池里该方法的引用
-
方法返回地址:方法结束后回到调用方的哪个位置
谁来用
每个线程一个 VM 栈,线程私有。所以局部变量天生是线程安全的------别的线程根本看不到你的栈。
会出什么问题
⚠️ 两种错:
StackOverflowError:栈帧压太深。最常见原因------递归没出口、方法相互循环调用
OutOfMemoryError:栈要扩容但内存不够了
小例子
void a() { b(); }
void b() { c(); }
void c() { a(); } // 死循环递归
// 调用 a() 后:a → b → c → a → b → c → ...
// 栈帧一直压不弹,最后 StackOverflowError
两个常被问的参数
-
-Xss:每个线程的栈大小。默认通常 512KB ~ 1MB,跟平台有关 -
栈越大,能递归越深,但能开的线程数越少(总内存是固定的)
3. 本地方法栈(Native Method Stack)
是什么
一句话 :和虚拟机栈一模一样,只是服务对象不同------VM 栈服务 Java 方法,本地方法栈服务 native 方法(用 native 关键字修饰、由 C/C++ 实现、通过 JNI 调用的方法)。
举例 :Object.getClass()、Thread.start0()、System.arraycopy() 这些底层都是 native 方法。
谁来用
线程私有。
会出什么问题
同 VM 栈:SOF 和 OOM。
ℹ️ 注意: HotSpot 把 VM 栈和本地方法栈合二为一了,所以日常你基本感觉不到这俩的区别。但 JVM 规范是分开定义的------面试时要分开答。
4. 堆(Heap)
是什么
一句话:所有对象和数组的家。
比喻 :大仓库。所有 new 出来的东西都堆这里。
存什么
-
对象实例(
new出来的) -
数组
-
JDK 7 之后:字符串常量池也从方法区搬到这里了
谁来用
所有线程共享。并发问题的主战场------两个线程同时改同一个对象的字段,没同步就会出问题。
会出什么问题
⚠️ OutOfMemoryError: Java heap space ------ 堆装不下了。最常见的 OOM,没有之一。
GC 主战场
堆是 GC 工作最密集的地方。为了回收高效,堆被分成两代:
-
新生代(Young Gen):又分 Eden + Survivor 0 + Survivor 1。新对象大多活不过一轮 GC,朝生夕死
-
老年代(Old Gen):熬过多次 GC 还活着的对象,晋升到这里
分代的核心假设:绝大多数对象朝生夕死,少数活很久。把"短命的"放一块集中清理,比全堆扫描快得多。
小例子
List<Object> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不断塞 1MB
}
// 最终:java.lang.OutOfMemoryError: Java heap space
常用参数
-
-Xms:堆初始大小 -
-Xmx:堆最大大小(生产环境通常和-Xms设一样,避免动态扩容的开销) -
-Xmn:新生代大小
5. 方法区(Method Area)
是什么
一句话:类的"说明书仓库"。每个类长什么样、有什么方法、常量是什么,全存这里。
比喻:图书馆的"图书目录"。书(对象)本身在堆里,但"这本书是什么、作者是谁、有哪些章节"这类描述信息在方法区。
存什么
-
类的元信息:类名、字段、方法、父类、接口、修饰符
-
运行时常量池:类文件里常量池加载后的运行时版本
-
静态变量(JDK 7 之前在这里;JDK 7 之后搬到堆里的 Class 对象上)
-
JIT 编译后的本地代码(部分实现)
谁来用
所有线程共享。
会出什么问题
⚠️
JDK 8+:OutOfMemoryError: Metaspace
JDK 7 及之前:OutOfMemoryError: PermGen space
绕不开的变迁史
方法区这块最容易把人绕晕,因为它的"实现"换过两次:
| JDK 版本 | 方法区的实现 | 位置 | 问题 |
|---|---|---|---|
| JDK 7 之前 | 永久代 PermGen | JVM 堆内 | 大小固定,易 OOM |
| JDK 7 | 永久代(开始过渡) | 字符串常量池、静态变量搬到堆 | 过渡版本 |
| JDK 8+ | 元空间 Metaspace | 本地内存(堆外) | 理论上"想要多少有多少" |
⚠️ 为什么要改? 永久代大小固定、难调,加载太多类(比如用大量动态代理、Spring 项目多)就容易
PermGen space。元空间用本地内存,JVM 不再被自己的堆大小限制,能装下更多类元信息------当然,OS 内存也是有限的,爆了照样 OOM。
常用参数
-
JDK 8+:
-XX:MetaspaceSize(初始)、-XX:MaxMetaspaceSize(上限) -
JDK 7 及之前:
-XX:PermSize、-XX:MaxPermSize(已废弃)
对比表:一张表记住五块
| 区域 | 线程私有 | 存什么 | 会出的错 | 调参 |
|---|---|---|---|---|
| 程序计数器 | ✓ | 字节码地址 | 不会出错 | 无 |
| 虚拟机栈 | ✓ | 栈帧(局部变量、操作数栈等) | SOF / OOM | -Xss |
| 本地方法栈 | ✓ | native 方法的栈帧 | SOF / OOM | -Xss |
| 堆 | ✗ 共享 | 对象、数组、字符串常量池 | OOM: Java heap space | -Xms / -Xmx |
| 方法区 | ✗ 共享 | 类元信息、运行时常量池 | OOM: Metaspace / PermGen | -XX:MaxMetaspaceSize |
常见疑问(自问自答)
Q:字符串常量池到底在哪?
A:JDK 7 之前在方法区(永久代)。JDK 7 之后搬到堆里 。所以现在 String.intern() 的对象和普通对象一样在堆里,能被 GC。这是为什么 JDK 7 之后 intern 大量字符串不会撑爆永久代了。
Q:静态变量在哪?
A:JDK 7 之前在方法区。JDK 7 之后,静态变量本身存在堆里的 Class 对象上(更准确说:static 引用指向的对象一直都在堆里,只是引用的位置从方法区挪到了堆里的 Class 对象)。
Q:方法区 == 永久代吗?
A:不等。方法区是 JVM 规范定义的"概念",永久代只是 HotSpot 在 JDK 7 及之前的"实现"。JDK 8 起永久代被废弃,方法区改由元空间实现。规范和实现的关系。
Q:方法区 == 元空间吗?
A:也不等。元空间是方法区在 JDK 8+ 的实现。其他 JVM(比如 J9、Zing)的实现可能不一样。
Q:为什么栈是线程私有的,但堆是共享的?
A:栈装的是方法执行状态(局部变量、操作数栈)------每个方法调用都是某个线程在执行,当然私有。堆装的是对象------对象要在线程间共享传递,当然共享。这是设计上的必然,不是随便分的。
Q:直接内存(Direct Memory)算运行时数据区吗?
A:不算。直接内存是 JVM 之外的本地内存,通过 NIO 的 ByteBuffer.allocateDirect() 申请,不走堆。但元空间用的也是这种本地内存------所以严格说,元空间在"方法区"这个概念里,但用的内存在"直接内存"的区域。绕,但是事实。
一句话总结
JVM 把内存切成五份:三份给线程私有(PC、虚拟机栈、本地方法栈),两份给所有线程共享(堆、方法区)。私有的死在 StackOverflow,共享的死在 OutOfMemory。这就是全部。