JVM¶
基础¶
1. 核心概念与三层体系¶
首先要理清 Java 环境的“套娃”关系,这是面试和实际配置环境时的基础。
- JVM (Java Virtual Machine):Java虚拟机。它是一个“虚构”的计算机,负责将 Java 字节码(.class 文件)翻译成机器码执行 。
- 核心作用:实现“一次编写,到处运行” (Write Once, Run Anywhere)。
- JRE (Java Runtime Environment):Java运行环境。
- 公式:JRE = JVM + Java核心类库(如 String, System 等)。
- 场景:如果你只需要运行 Java 程序,安装 JRE 即可。
- JDK (Java Development Kit):Java开发工具包。
- 公式:JDK = JRE + 开发工具(javac 编译器, jconsole, jmap 等)。
- 场景:开发人员必须安装 JDK 。
2. JVM 内存区域(运行时数据区)¶
这是JVM中最核心的部分,理解它才能明白对象存在哪里,以及为什么会由内存溢出。
| 区域名称 | 简写/术语 | 说明与作用 | 是否线程私有 | 常见异常 |
|---|---|---|---|---|
| 堆 | Heap | 存储对象实例(如 new User())。这是GC(垃圾回收)的主要区域,也是最大的内存块。 | 否 (共享) | OOM (OutOfMemoryError) |
| 虚拟机栈 | VM Stack | 存储局部变量、方法调用链。每个方法执行时会创建一个“栈帧” (Stack Frame)。 | 是 (私有) | SOF (StackOverflowError) |
| 方法区 | Method Area | 存储类信息、常量、静态变量。Java 8+ 称为 Metaspace (元空间) | 否 (共享) | OOM |
| 程序计数器 | PC Register | 记录当前线程执行到哪一行字节码了,用于线程切换后恢复执行 interview。 | 是 (私有) | 无 |
| 本地方法栈 | Native Stack | 服务于 native 方法(如 C++ 写的底层库)。 | 是 (私有) | SOF |
3. 代码示例:通过代码理解内存分布¶
public class JvmDemo {
// 静态变量 -> 存放在方法区 (Metaspace)
private static int staticVar = 100;
public void method() {
// 局部变量 (基本类型) -> 存放在 虚拟机栈 (Stack)
int x = 10;
// user 是一个引用 -> 存放在 虚拟机栈 (Stack)
// new User() 是实际对象 -> 存放在 堆 (Heap)
User user = new User();
}
}
为什么区分栈和堆? 栈是用来处理程序逻辑的(方法调用),用完即丢,速度快;堆是用来存储数据的(对象),生命周期长,需要垃圾回收器(GC)来管理。
4. 必须掌握的专业术语与缩写¶
在技术文档、日志分析和架构讨论中,以下缩写非常高频:
运行与编译类
- JIT (Just-In-Time Compiler):即时编译器。JVM不是傻傻地解释每一行代码,它会把“热点代码”(经常执行的代码)编译成高效的本地机器码,这是Java性能接近C++的关键。
- Bytecode:字节码。即 .class 文件,它是JVM的“机器语言” 。
垃圾回收 (Garbage Collection) 类
- GC (Garbage Collection):垃圾回收。自动回收不再使用的堆内存。
- STW (Stop The World):全线暂停。指在进行垃圾回收时,必须暂停所有应用线程。这是性能调优要极力避免或减少的时间 。
- Minor GC / Young GC:发生在新生代(Young Gen)的回收,非常频繁,速度快。
- Full GC / Major GC:发生在老年代(Old Gen)和整个堆的回收,速度慢,尽量少触发 。
故障排查类
- OOM (OutOfMemoryError):内存溢出。内存不够用了,通常是因为堆满了且无法回收。
- SOF (StackOverflowError):栈溢出。通常是因为死递归(方法无限调用自己)导致栈空间耗尽。
- Dump:快照。
- Heap Dump:堆内存快照,用于分析 OOM 原因 。
- Thread Dump:线程快照,用于分析死锁或 CPU 飙高。
5. 常用命令工具(CLI)¶
在 Linux 服务器上排查问题时,你没有图形界面,必须掌握这些命令 :
- jps:查看当前运行的 Java 进程 ID。
- jstat:查看 GC 统计信息(最常用)。
- jmap:生成堆内存快照 (Dump)。
- jstack:查看线程堆栈(查死锁神器)。
JVM 运行时内存结构¶
JVM 在运行一个 Java 程序时,会把管理的内存划分成几个区域,每个区域负责不同的职责 :
- 程序计数器(Program Counter,PC)
- Java 虚拟机栈(VM Stack)
- 本地方法栈(Native Stack)
- 堆(Heap)
- 方法区(Method Area,含元空间 Metaspace 等)
1. 线程私有区域¶
这些区域和线程“一一对应”,线程结束就销毁 。
1)程序计数器 PC
- 作用:记录当前线程下一条要执行的字节码指令地址。
- 为什么需要:
- 多线程是“轮流”占用 CPU 的,切换回来时要知道自己上次执行到哪。
2)Java 虚拟机栈(栈)
- 每个线程有一个栈,里面由**栈帧(Stack Frame)**构成,一个方法就是一个栈帧 。
- 栈帧里主要放:
- 局部变量表(int x、User user 这种)
- 操作数栈
- 方法返回地址等
- 典型错误:递归太深导致 StackOverflowError(SOF)。
3)本地方法栈
- 给 native 本地方法使用(C/C++实现的那种)。
- 结构类似虚拟机栈,只不过是为本地方法服务。
2. 线程共享区域¶
这些区域是所有线程共享的 。
1)堆(Heap)
- 存放:所有的对象实例、数组(new 出来的东西基本都在这)。
- 特点:
- 最大的一块内存。
- 垃圾回收(GC)的“主战场”。
- 通常会再划分为:新生代(Eden + Survivor 区),老年代等,用于不同 GC 策略 。
- 典型错误:OutOfMemoryError: Java heap space 。
2)方法区(Method Area)
- 存放:
- 类元数据(类名、字段、方法信息等)
- 静态变量、常量
- 运行时常量池等 。
- JDK8 以后实现为元空间(Metaspace),使用本地内存 。
- 典型错误:类太多或元空间不够时 OutOfMemoryError: Metaspace 。
3)直接内存(Direct Memory)
- 不属于 JVM 运行时数据区的一部分,但经常一起讨论 。
- 通过 NIO 的 DirectByteBuffer 使用,绕过堆,减少一次复制,提高 I/O 性能。
- 配置不当、使用过多也会 OOM。
3. 用一段代码把这些区域串起来¶
public class MemoryModelDemo {
// 静态变量 -> 方法区(类元数据区域)
private static int staticCounter = 0;
// 实例变量 -> 堆
private int value;
public MemoryModelDemo(int value) {
this.value = value;
}
public int add(int x, int y) {
// x、y、sum -> 栈帧中的局部变量表
int sum = x + y;
staticCounter++; // 读写方法区中的静态变量
return sum;
}
public static void main(String[] args) {
// args -> main 方法栈帧中的局部变量
MemoryModelDemo demo = new MemoryModelDemo(10); // demo 对象在堆
int result = demo.add(1, 2); // add 创建新的栈帧
System.out.println(result);
}
}
重点理解:
- 方法调用 → 对应栈上的栈帧,局部变量、参数都在栈里。
- new 出来的对象 → 在堆里。
- static 成员 → 类的元数据和静态变量在方法区 / 元空间中。
JVM 运行时数据区(结构图)¶
- 目的:搞清楚“数据放在哪块物理区域”以及“典型异常”。
- 记忆路径:
- 线程私有:PC、栈、本地方法栈。
- 线程共享:堆、方法区(元空间)、直接内存。
JVM 问题¶
垃圾回收算法¶
如何确定对象已死?¶
通常,判断一个对象是否被销毁有两种方法:
- 引用计数算法: 为对象添加一个引用计数器,每当对象在一个地方被引用,则该计数器加1;每当对象引用失效时,计数器减1。当计数器为0的时候,就表示该对象没有被引用。
- 可达性分析算法: 通过一系列被称之为“GC Roots”的根节点开始,沿着引用链进行搜索,凡是在引用链上的对象都不会被回收。
这个GC Roots又是什么?下面列举可以作为GC Roots的对象:
- Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象,比如引用类型的静态变量。
- 方法区中常量引用的对象。
- 本地方法栈中所引用的对象。
- Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
- 被同步锁(synchronized)持有的对象。
垃圾回收算法¶
标记--清除算法¶
- 标记–清除(Mark-Sweep)
- 复制(Copying)
- 标记–整理 / 标记–压缩(Mark-Compact)
- 分代收集(Generational Collection,实际上是“策略”)
1. 标记–清除(Mark–Sweep)
思想:
- 从 GC Roots 做可达性分析,把所有“还活着”的对象标记出来。
- 扫描堆,把没有标记的对象内存回收掉 。
优点
- 实现简单,历史最久,很多 GC 都以它为基础 。
- 不需要额外预留一大块“备用空间”,内存利用率较高。
缺点
- 会产生大量内存碎片:活对象分散在堆各处,后续可能因为找不到连续大块空间导致频繁 Full GC 。
- 清除阶段是通过链表或空闲表来分配内存,速度比顺序分配慢。
适用位置:
- 早期老年代回收算法的基础。
- 在对象存活率高、又不想浪费太多额外空间时,经常会先用标记–清除,然后再配合压缩或整理 。
2. 复制算法(Copying)
思想:
把内存按比例分成两块(From、To),每次只在一块上分配对象:
- GC 时,只扫描当前使用的那块,把存活对象复制到另一块连续空间。
- 复制完后,直接“整块”清空原空间(全部视为可用)。
HotSpot 新生代实际是 Eden + 两个 Survivor(From/To)区,使用的是改良的复制策略 。
优点
- 没有内存碎片,因为总是把活对象搬到一块连续区域。
- 分配速度极快:只需要一个指针顺序往后移动,分配新对象非常快。
- 实现相对简单,适合“朝生夕死”的区域(新生代)。
缺点
- 天然会“浪费”一部分空间(两块只用一块),即使 Eden+Survivor 优化后,仍有一定空间利用率损失 。
- 当存活对象比例升高时,复制成本会变大,效率会下降。
适用位置:
- 新生代:因为绝大多数新对象很快就死掉,每次 Minor GC 只需复制少量幸存者,整体非常高效 。
3. 标记–整理 / 标记–压缩(Mark–Compact)
可以理解为:标记–清除 + 整理。
思想:
- 和标记–清除一样,先标记所有存活对象。
- 将所有存活对象向一端移动(压缩 / 整理),保持它们连续。
- 清理边界之外的所有内存 。
优点
- 回收后堆空间是连续的,不会产生碎片,后续大对象分配更容易 。
- 不需要像复制算法那样保留完整“等大”的另一块空间,内存利用率高。
缺点
- 比“标记–清除”多了一步整理/移动步骤,开销更大。
- 需要移动对象,意味着要更新所有引用这些对象的指针,实现和成本都更复杂 。
适用位置:
- 老年代:对象存活率高,不适合复制算法浪费空间,也不想长期忍受严重碎片,所以多用标记–清除和标记–整理的组合 。
- 现代收集器(如 G1)整体上也是基于“标记–整理”,局部又结合复制的思想 。
4. 分代收集(Generational Collection)
严格来说,它不是一个独立的算法,而是一个策略/思想:
根据对象的“年龄”(存活时间长短)把堆分成新生代、老年代等区域,对不同区域使用 不同的具体算法 。
核心观察
- 大部分对象“朝生夕死”,少数对象“长寿”。
- 新生代:回收频率高,但每次回收后存活对象少 → 适合 复制算法。
- 老年代:回收频率低,但活对象多 → 适合 标记–清除或标记–整理。
优点
- 充分利用各算法优势:
- 新生代用复制算法 → 快且简单。
- 老年代用标记–整理 / 标记–清除 → 节省空间、避免碎片。
- 极大提高整体 吞吐量 和 内存利用率,这是 HotSpot 各 GC 的基础设计 。
缺点
- 设计和实现更复杂:需要维护对象晋升(新生代 → 老年代)、跨代引用(需要记忆集 Remembered Set)等机制。
- 调参更多:新生代大小、晋升阈值、Survivor 比例等,调优成本较高。
垃圾收集器与算法的关系¶
- Serial / ParNew(新生代):复制算法(标记–复制),单线程 / 多线程。
- Parallel Scavenge / Parallel Old:新生代复制、老年代标记–整理,侧重吞吐量 。
- CMS:老年代基于标记–清除,并发回收,停顿短但会有碎片 。
- G1:整体是标记–整理 + 分区 + 复制,支持预测停顿时间,JDK9+ 默认收集器 。
- ZGC / Shenandoah:本质也是标记–复制 / 标记–整理的改进版本,通过读屏障等手段把大部分 GC 工作并发化,把停顿时间压到毫秒级 。
内存模型¶
堆内存模型¶
- JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为年轻代和老年代,而非堆内存包括直接内存、本地方法栈、Code Cache等。
- 新生代Young和老年代Old默认占比是1:2。
-XX:NewRatio=2
- 年轻代又会分为Eden和Survivor区,Survivor也会分为FromPlace和ToPlace,Eden、FromPlace和ToPlace的默认占比为 8:1:1。
-XX:SurvivorRatio=8
GC类型¶
按作用区域分类