写在 2017 年的笔记。当年 Java 8 已经普及,方法区从「PermGen」换成了「Metaspace」,G1 也终于能稳定扛生产了。这一篇先把 JVM 内存区域讲清楚。
一、JVM 是什么
JVM(Java Virtual Machine),一种用于计算设备的规范。它是虚构出来的计算机——通过在真实计算机上仿真模拟各种计算机功能来实现。引入 JVM 后,Java 语言在不同平台上运行时不需要重新编译,Java 语言编译程序只需要生成在 JVM 上运行的目标代码(字节码)就可以在多种平台运行。
换句话说:JVM 是 Java 跨平台能力的核心,也是 Java 程序员必须搞懂的基础。
二、JVM 模块构成
JVM 大致可以拆成五块:
- 类加载子系统:把
.class文件加载进内存 - 运行时数据区(内存空间):线程共享 + 线程私有两块
- 执行引擎:对字节码指令进行解释执行 / JIT 编译执行
- 本地方法接口(JNI):调用 native 方法(C/C++ 实现)
- 垃圾收集模块(GC):自动管理堆内存
这一篇重点讲「运行时数据区」——它是我们排查内存问题(OOM / 栈溢出 / 性能瓶颈)的核心。
三、运行时数据区全景

线程共享:堆(Heap)、方法区(Method Area / Metaspace) 线程私有:PC 寄存器、虚拟机栈、本地方法栈
这是排查 JVM 问题时第一张要画的图——问题出在哪个区,决定了你用什么工具。
四、堆(Heap)
堆区是所有线程共享的区域。分为 Young 区 和 Old 区:
- Young 区:新创建的对象先分配在这里。Young 区又分为:
- Eden 区:绝大多数对象「朝生夕死」的地方
- Survivor 区:分 S0 和 S1 两个大小相等的区域,同一时间只有一个区域有数据
- Old 区:经过多次 GC 仍然存活的对象晋升到这里
「S0/S1 同时只有一个有数据」是为了解决 Survivor 区碎片化问题。GC 时把存活对象从 S0 复制到 S1,然后清空 S0——下个周期反过来。这种算法叫「复制算法」,对应「年轻代」的场景。
五、虚拟机栈(JVM Stack)
每当启动一个新线程时,Java 虚拟机都会为它分配一个 Java 栈。Java 栈以帧(Stack Frame)为单位保存线程的运行状态。
虚拟机只会直接对 Java 栈执行两种操作:以帧为单位的压栈和出栈。
5.1 栈帧的组成
每个方法从调用到执行完成,对应一个栈帧的入栈到出栈过程。一个栈帧包含四部分:
| 部分 | 作用 |
|---|---|
| 局部变量表(Local Variable Table) | 存放方法参数、方法内定义的局部变量 |
| 操作数栈(Operand Stack) | 字节码指令的工作区,方法的参数传递、运算都通过它 |
| 动态链接(Dynamic Linking) | 指向运行时常量池的方法引用,支持多态 |
| 方法返回地址(Return Address) | 方法正常返回或异常退出时,回到调用方的位置 |
5.2 局部变量表
存放基本数据类型、对象引用、returnAddress 类型(指向一条字节码指令的地址)。

其中:
- 64 位长度的
long和double类型的数据会占用 2 个局部变量空间(slot),其余数据类型只占用 1 个 - 静态方法和实例方法对应的局部变量表基本类似
- 区别:实例方法的表中,第一个位置存放的是当前对象的引用(即
this)
5.3 操作数栈

也被称为操作栈。Java 没有寄存器,所有的参数传递都是用操作数栈。
同局部变量表一样,也被组织成一个以字长为单位的数组。但与局部变量表不同,它不是通过索引来访问,而是通过标准的栈操作:压栈和出栈。
比如某一指令将值压到操作数栈,另一指令就能弹栈获取这个值。虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如 int、long、float、double、reference 和 returnType 等。
一个小贴士:i++ 和 ++i 的字节码差异仅在
iload(压栈)和iinc(自增)的顺序上——前者先iload后iinc,后者先iinc后iload。两者都不是原子操作,在多线程下都需要同步(synchronized或AtomicInteger)。它们之间不存在”可见性差异”——这是网上流传的一个误解。
5.4 动态链接
栈帧内部的一个引用,指向运行时常量池中该栈帧所属方法的引用。支持多态调用——子类重写父类方法时,invokevirtual 指令通过动态链接决定调用哪个版本。
六、程序计数器(PC Register)

程序计数器是一块较小的内存,可以看做是当前线程所执行的字节码行号指示器。
字节码解释器工作的时候,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
- 如果正在执行的是 Native 方法,这个计数器则为空(Undefined)
- 此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域——它随线程启动而启动,线程结束即销毁
七、本地方法栈(Native Method Stack)
与 Java 虚拟机栈相似,为虚拟机使用的 Native 方法服务。
- 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务
- 本地方法栈为虚拟机使用到的 Native 方法服务
在 HotSpot 虚拟机中,本地方法栈和虚拟机栈合二为一。其他虚拟机(如 Sun HotSpot 之外的部分 JVM)可能仍然分开。
八、堆和栈的区别
| 维度 | 堆 | 栈 |
|---|---|---|
| 管理方式 | 需要 GC 回收 | 自动释放(方法出栈即销毁) |
| 空间大小 | 大(GB 级别) | 小(MB 级别) |
| 碎片相关 | 碎片较多 | 碎片远小于堆 |
| 分配方式 | 只支持动态分配 | 支持静态 + 动态分配 |
| 效率差别 | 低 | 高(仅次于寄存器) |
| 线程归属 | 线程共享 | 线程私有 |
「栈上分配」是 JVM 优化手段——通过逃逸分析判定对象不会逃逸出方法时,把对象直接拆成标量在栈帧上分配,从而不进入堆。这个话题下一篇细讲。
九、小结:JVM 内存区域速记
| 区域 | 线程归属 | 抛错 | 调优关键参数 |
|---|---|---|---|
| 程序计数器 | 私有 | 不会 | — |
| 虚拟机栈 | 私有 | StackOverflowError / OOM | -Xss |
| 本地方法栈 | 私有 | StackOverflowError / OOM | — |
| 堆 | 共享 | OOM | -Xms / -Xmx / -Xmn |
| 方法区 | 共享 | OOM | -XX:MetaspaceSize / -XX:MaxMetaspaceSize |
下一篇「一文拿下 JVM(中):参数配置与常用命令」将围绕如何给这块内存配参数展开——
-Xms该设多大、-Xmn该留多少、G1 该不该开。