JVM 运行时数据区——把内存五块讲清楚

目录

五块总览:先看大图

[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。这就是全部。


相关推荐
Mark_ZP1 天前
CMS 垃圾回收器配置详解
jvm
北风toto1 天前
LangGraph 深度使用教程与总结
jvm
井川廊咏1 天前
初探性能优化——2个月到4小时的性能提升
jvm·数据库·性能优化
程序员天天困1 天前
Arthas Profiler 火焰图实战:CPU 热点在哪一目了然
java·jvm·后端
Co_Hui1 天前
JVM vs. DVM vs. ART
jvm
笨蛋不要掉眼泪2 天前
Java虚拟机:对象复活、引用强度与Stop-The-World
java·开发语言·jvm
吃饱了得干活2 天前
JVM垃圾回收:从新生代到ZGC,从理论到调优
java·jvm·后端
长不胖的路人甲2 天前
可达性分析法(根搜索算法)完整详解
java·jvm·算法
程序员天天困2 天前
Arthas mc + retransform 实战:线上改完代码不用重新发版
jvm·后端