跳转至

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)

思想:

  1. 从 GC Roots 做可达性分析,把所有“还活着”的对象标记出来。
  2. 扫描堆,把没有标记的对象内存回收掉 。

优点

  • 实现简单,历史最久,很多 GC 都以它为基础 。
  • 不需要额外预留一大块“备用空间”,内存利用率较高。

缺点

  • 会产生大量内存碎片:活对象分散在堆各处,后续可能因为找不到连续大块空间导致频繁 Full GC 。
  • 清除阶段是通过链表或空闲表来分配内存,速度比顺序分配慢。

适用位置:

  • 早期老年代回收算法的基础。
  • 在对象存活率高、又不想浪费太多额外空间时,经常会先用标记–清除,然后再配合压缩或整理 。

2. 复制算法(Copying)

思想:

把内存按比例分成两块(From、To),每次只在一块上分配对象:

  1. GC 时,只扫描当前使用的那块,把存活对象复制到另一块连续空间。
  2. 复制完后,直接“整块”清空原空间(全部视为可用)。

HotSpot 新生代实际是 Eden + 两个 Survivor(From/To)区,使用的是改良的复制策略 。

优点

  • 没有内存碎片,因为总是把活对象搬到一块连续区域。
  • 分配速度极快:只需要一个指针顺序往后移动,分配新对象非常快。
  • 实现相对简单,适合“朝生夕死”的区域(新生代)。

缺点

  • 天然会“浪费”一部分空间(两块只用一块),即使 Eden+Survivor 优化后,仍有一定空间利用率损失 。
  • 当存活对象比例升高时,复制成本会变大,效率会下降。

适用位置:

  • 新生代:因为绝大多数新对象很快就死掉,每次 Minor GC 只需复制少量幸存者,整体非常高效 。

3. 标记–整理 / 标记–压缩(Mark–Compact)

可以理解为:标记–清除 + 整理。

思想:

  1. 和标记–清除一样,先标记所有存活对象。
  2. 将所有存活对象向一端移动(压缩 / 整理),保持它们连续。
  3. 清理边界之外的所有内存 。

优点

  • 回收后堆空间是连续的,不会产生碎片,后续大对象分配更容易 。
  • 不需要像复制算法那样保留完整“等大”的另一块空间,内存利用率高。

缺点

  • 比“标记–清除”多了一步整理/移动步骤,开销更大。
  • 需要移动对象,意味着要更新所有引用这些对象的指针,实现和成本都更复杂 。

适用位置:

  • 老年代:对象存活率高,不适合复制算法浪费空间,也不想长期忍受严重碎片,所以多用标记–清除和标记–整理的组合 。
  • 现代收集器(如 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类型

按作用区域分类