Java面试题46:一文深入了解JVM 核心知识体系

第一章 JVM 运行时数据区(核心数据结构)

JVM 运行时数据区是 Java 程序运行时内存管理的核心,JVM 在启动时会将内存划分为不同的逻辑区域,各区域有明确的职责、创建销毁时机和内存特性。根据线程隔离性,可分为线程私有区域线程共享区域两大类,同时补充直接内存(JVM 可管理的堆外内存)。

一、线程私有区域(随线程创建而创建,线程结束而销毁)

1. 程序计数器(Program Counter Register)
  • 核心作用:记录当前线程正在执行的字节码指令的地址(行号),是字节码解释器的指示器,负责选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖它完成。
  • 核心特性
    1. 线程私有:每个线程都有独立的程序计数器,保证多线程切换后能恢复到正确的执行位置。
    2. 唯一不会抛出OutOfMemoryError(OOM)的区域:程序计数器只存储指令地址,内存空间固定且极小,不存在内存溢出问题。
    3. 执行 Native 方法时,程序计数器的值为空(Undefined),因为 Native 方法由底层 C/C++ 实现,JVM 无法追踪其执行地址。
2. Java 虚拟机栈(Java Virtual Machine Stack)
  • 核心作用 :为 Java 方法的执行提供内存支持,每个方法执行时都会同步创建一个栈帧(Stack Frame),方法从调用到执行完成的过程,对应栈帧在虚拟机栈中入栈到出栈的过程。
  • 核心结构(栈帧) :栈帧是方法执行的最小单元,包含 4 个核心部分:
    1. 局部变量表 :存储方法参数和方法内定义的局部变量,数据类型包括基本数据类型(boolean/byte/char/short/int/float/long/double)、对象引用(reference 类型,指向对象的内存地址)、returnAddress 类型(指向字节码指令的地址)。
      • 局部变量表的容量以 ** 变量槽(Slot)** 为最小单位,64 位的 long 和 double 占用 2 个 Slot,其余类型占用 1 个 Slot。
      • 局部变量表在编译期就确定了最大容量,运行期不会改变。
    2. 操作数栈 :字节码指令的执行栈,用于计算过程中临时存储操作数和计算结果。比如执行i+j时,会先将 i 和 j 压入操作数栈,再执行加法指令,弹出两个数计算后将结果压回栈中。
      • 操作数栈的最大深度也在编译期确定,32 位数据占用 1 个栈深度,64 位数据占用 2 个栈深度。
    3. 动态链接:将 Class 文件中方法的符号引用,在运行期转换为直接内存地址的引用。一部分符号引用在类加载阶段就解析为直接引用(静态解析),另一部分在每次运行期间才解析(动态链接,支持 Java 的多态特性)。
    4. 方法返回地址:方法执行结束后的返回位置,分为正常返回(执行到 return 指令)和异常返回(抛出未捕获的异常)。方法退出后,会根据返回地址恢复上层方法的执行状态,将返回值压入上层方法的操作数栈中。
  • 异常情况
    1. StackOverflowError:线程请求的栈深度超过 JVM 允许的最大深度(比如无限递归调用方法)。
    2. OutOfMemoryError:JVM 栈内存支持动态扩展时,扩展时无法申请到足够的内存(HotSpot 虚拟机的栈不支持动态扩展,只要申请栈空间成功就不会出现 OOM,申请失败直接 OOM)。
  • 核心参数-Xss,设置每个线程的虚拟机栈大小,默认值和平台相关(Linux/x64 默认 1MB)。
3. 本地方法栈(Native Method Stack)
  • 核心作用:和虚拟机栈功能完全一致,区别在于虚拟机栈为 Java 方法服务,本地方法栈为 Native 方法(C/C++ 实现的本地方法)服务。
  • 特性 :HotSpot 虚拟机直接将本地方法栈和虚拟机栈合二为一,没有做区分;同样会抛出StackOverflowErrorOutOfMemoryError异常。

二、线程共享区域(随 JVM 启动而创建,JVM 关闭而销毁)

1. Java 堆(Heap)
  • 核心作用:Java 内存中最大的一块,唯一目的是存放对象实例和数组,Java 中几乎所有的对象实例都在这里分配内存(JIT 编译的逃逸分析优化后,对象可栈上分配,不再进入堆)。
  • 核心结构(分代设计,经典分代模型) :堆内存基于分代回收思想,分为年轻代(Young Gen)老年代(Old Gen) ,JDK8 默认年轻代:老年代 = 1:2(可通过-XX:NewRatio调整)。
    1. 年轻代 :存放新创建的对象,绝大多数对象都是朝生夕灭,回收频率高、速度快。分为 1 个 Eden 区和 2 个 Survivor 区(From Survivor/S0、To Survivor/S1),默认比例 Eden:S0:S1=8:1:1(可通过-XX:SurvivorRatio调整)。
      • 对象优先在 Eden 区分配,Eden 区满时触发 Minor GC(年轻代 GC),存活的对象会复制到其中一个 Survivor 区;
      • 每次 Minor GC 后,存活对象的年龄 + 1,当年龄达到阈值(默认 15,可通过-XX:MaxTenuringThreshold调整),会晋升到老年代;
      • 动态年龄判定:Survivor 区中相同年龄的所有对象大小总和超过 Survivor 区的 50%,年龄大于等于该年龄的对象直接晋升老年代,无需等到阈值。
    2. 老年代:存放长期存活的对象、大对象,回收频率低、速度慢,老年代满时触发 Full GC(整堆 GC)。
  • 特殊设计:TLAB(Thread Local Allocation Buffer)
    • 线程本地分配缓冲区,是 Eden 区中为每个线程划分的私有分配区域,默认占用 Eden 区的 1%。
    • 解决对象分配时的线程安全问题:堆是线程共享的,多个线程同时分配对象时,需要加锁保证线程安全,TLAB 让每个线程在自己的缓冲区分配对象,无需加锁,提升对象分配效率。
  • 异常情况OutOfMemoryError: Java heap space,堆中没有足够内存完成对象实例分配,且堆无法再扩展时抛出。
  • 核心参数
    • -Xms:堆的初始内存大小,默认是物理内存的 1/64;
    • -Xmx:堆的最大内存大小,默认是物理内存的 1/4;
    • -Xmn:设置年轻代的大小,等价于同时设置-XX:NewSize-XX:MaxNewSize
    • -XX:+UseTLAB:开启 TLAB(默认开启)。
2. 方法区(Method Area)
  • 核心作用:存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据,是所有线程共享的区域。
  • 关键实现演进(HotSpot 虚拟机)
    1. JDK8 之前 :HotSpot 用 ** 永久代(Permanent Generation)** 实现方法区,永久代属于 JVM 堆内存的一部分,有固定的内存上限,无法动态扩展,容易出现OutOfMemoryError: PermGen space异常。
    2. JDK8 及之后:永久代被彻底移除,改用 ** 元空间(Metaspace)** 实现方法区,元空间不再使用 JVM 堆内存,而是直接使用本地操作系统内存,默认只受本地物理内存限制,大幅降低了方法区 OOM 的概率。
  • JDK8 替换永久代的核心原因
    1. 永久代有固定的内存上限,很难精准设置,调优复杂,容易出现 OOM;
    2. 永久代的 GC 和老年代绑定,只要其中一个满了就会触发 Full GC,回收效率低;
    3. 类和方法的元数据信息在程序运行中很难确定回收时机,放在本地内存更灵活,可由操作系统自动管理。
  • 核心子区域:运行时常量池
    • 是方法区的一部分,Class 文件中每个类都有一个常量池(存放编译期的字面量、符号引用),类加载后,这个常量池会被放入运行时常量池中。
    • 核心特性:具备动态性,不仅能存储编译期生成的常量,运行期也能将新的常量放入池中(最典型的就是String.intern()方法)。
  • 异常情况
    • JDK8 之前:OutOfMemoryError: PermGen space,永久代内存不足;
    • JDK8 及之后:OutOfMemoryError: Metaspace,元空间内存不足,类加载过多时出现(比如反射、动态代理、热部署场景)。
  • 核心参数
    • -XX:MetaspaceSize:元空间初始大小,达到该值会触发 GC;
    • -XX:MaxMetaspaceSize:元空间最大内存,默认无上限(受本地内存限制)。

三、字符串常量池(String Table)

  • 核心作用:专门存储字符串对象的引用,避免重复创建字符串对象,节省内存,是 Java 对 String 类型的优化设计。
  • 位置演进(重点,面试高频)
    1. JDK6 及之前:字符串常量池存放在永久代中,和运行时常量池绑定;
    2. JDK7:字符串常量池从永久代移到了 Java 堆中,可被堆的 GC 回收;
    3. JDK8 及之后:永久代被元空间替换,字符串常量池依然存放在 Java 堆中。
  • 核心特性
    • 编译期确定的字符串字面量,会直接放入字符串常量池;
    • 运行期通过new String()创建的字符串对象,会在堆中分配内存,不会自动进入常量池,可通过intern()方法手动将字符串的引用放入常量池。

四、直接内存(Direct Memory)

  • 核心定义:不属于 JVM 运行时数据区的一部分,也不是 JVM 规范中定义的内存区域,但被 JVM 频繁使用,且会抛出 OOM 异常。
  • 核心作用:JDK1.4 引入的 NIO(New Input/Output),使用 Channel+Buffer 的 IO 模型,可通过 Native 函数直接分配堆外的本地内存,然后通过堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了 Java 堆和 Native 堆之间来回拷贝数据,大幅提升 IO 效率。
  • 特性
    • 直接内存的分配不受 Java 堆大小限制,但受本地物理内存和操作系统进程内存限制;
    • 直接内存的 GC 回收效率低,只能通过 Full GC 触发回收(System.gc ()),因为 JVM 只能通过 DirectByteBuffer 对象的引用管理这块内存,对象被回收后,对应的直接内存才会被释放。
  • 异常情况OutOfMemoryError: Direct buffer memory,直接内存达到上限时抛出。
  • 核心参数-XX:MaxDirectMemorySize,设置直接内存的最大大小,默认和 Java 堆的-Xmx值一致。

第二章 JVM 垃圾回收机制与垃圾回收器

垃圾回收(Garbage Collection,GC)是 JVM 的核心能力之一,负责自动回收堆内存中不再使用的对象,避免手动内存管理的内存泄漏、野指针等问题,提升开发效率。

一、垃圾回收核心基础

1. 什么是垃圾?

垃圾是指堆内存中已经死亡、不会再被任何途径使用的对象,GC 的核心就是找到这些垃圾对象,释放其占用的内存空间。

2. 如何判断对象是垃圾?
(1)引用计数法
  • 原理:给每个对象添加一个引用计数器,每当有一个地方引用它,计数器 + 1;引用失效时,计数器 - 1;任何时刻计数器为 0 的对象,就是可回收的垃圾对象。
  • 优点:实现简单,判断效率高。
  • 致命缺点 :无法解决循环引用问题。比如两个对象互相引用,除此之外没有任何其他引用,它们的引用计数器都不为 0,无法被回收,造成内存泄漏。
  • 使用情况:主流 JVM(HotSpot)没有采用该算法。
(2)可达性分析算法(主流 JVM 采用)
  • 原理 :以一系列名为GC Roots的根对象作为起点,从这些起点开始向下遍历,遍历的路径称为引用链;如果一个对象到 GC Roots 之间没有任何引用链相连,说明该对象不可达,就是可回收的垃圾对象。
  • 核心:GC Roots 包含哪些对象?(面试高频)
    1. 虚拟机栈(局部变量表)中引用的对象(方法中定义的局部变量、参数);
    2. 本地方法栈中 JNI(Native 方法)引用的对象;
    3. 方法区中类静态属性引用的对象(static 修饰的静态变量);
    4. 方法区中常量引用的对象(final 修饰的常量);
    5. 同步锁(synchronized)持有的对象;
    6. JVM 内部的基础对象(比如系统类加载器、核心异常对象、常驻的字符串对象)。
  • 优点:解决了循环引用问题,判断精准,是 HotSpot 的核心算法。
3. Java 中的 4 种引用类型

JDK1.2 之后,Java 将引用分为 4 种类型,不同引用类型的回收策略不同,可灵活控制对象的生命周期。

表格

引用类型 定义 回收时机 核心使用场景
强引用(Strong Reference) 最常见的引用,Object obj = new Object()就是强引用 只要强引用存在,垃圾回收器永远不会回收该对象,哪怕 OOM 也不回收 绝大多数的对象创建,日常开发的默认引用
软引用(SoftReference) 用 SoftReference 包装的对象,非必需的引用 当 JVM 内存不足,即将发生 OOM 时,会回收软引用对象;内存充足时不会回收 内存敏感的缓存(比如图片缓存、网页缓存),内存不足时自动释放,避免 OOM
弱引用(WeakReference) 用 WeakReference 包装的对象,比软引用更弱 无论内存是否充足,只要触发垃圾回收,就会立即回收弱引用对象 临时缓存、ThreadLocal 的 key 实现,避免内存泄漏
虚引用(PhantomReference) 也叫幽灵引用,用 PhantomReference 包装,最弱的引用,无法通过虚引用获取对象实例 完全无法决定对象的生命周期,对象被回收时会收到一个系统通知 跟踪对象的垃圾回收过程,管理堆外内存(比如 DirectByteBuffer 的回收)
4. 对象的回收过程(两次标记机制)

一个对象被判定为不可达后,不会立即被回收,需要经历两次标记过程:

  1. 第一次标记 :对象经过可达性分析后,发现没有和 GC Roots 相连的引用链,会被第一次标记,同时判断该对象是否重写了finalize()方法。
    • 如果对象没有重写finalize()方法,或者finalize()方法已经被 JVM 执行过,直接进入回收队列,等待回收;
    • 如果对象重写了finalize()方法,且从未被执行过,会被放入F-Queue队列中,等待 JVM 的 Finalizer 线程执行该方法。
  2. 第二次标记 :JVM 会执行F-Queue队列中对象的finalize()方法,执行过程中,如果对象重新和 GC Roots 建立了引用(比如把 this 赋值给某个静态变量),就会被移出回收队列,完成对象自救;如果没有建立引用,会被第二次标记,最终被回收。
  • 关键注意点finalize()方法只会被 JVM 执行一次,且执行时间不确定,不推荐使用,JDK9 已被标记为过时,推荐使用 try-with-resources 替代。
5. 垃圾回收核心算法
(1)标记 - 清除算法(Mark-Sweep)
  • 执行过程 :分为两个阶段,标记阶段 (标记出所有需要回收的垃圾对象)、清除阶段(统一回收所有标记的对象,释放内存空间)。
  • 优点:实现简单,不需要移动对象,执行效率高。
  • 缺点
    1. 会产生大量不连续的内存碎片,碎片太多会导致后续分配大对象时,无法找到足够的连续内存,提前触发 GC;
    2. 标记和清除两个阶段的效率都会随着对象数量的增加而降低。
  • 适用场景:老年代,对象存活时间长、回收频率低的场景(比如 CMS 收集器)。
(2)标记 - 复制算法(Mark-Copy)
  • 执行过程:将可用内存分为大小相等的两块,每次只使用其中一块;当这块内存用完了,就将存活的对象复制到另一块内存中,然后一次性清空当前使用的整块内存。
  • 优化版本(HotSpot 年轻代实现):年轻代中 98% 的对象都是朝生夕灭,不需要 1:1 划分内存,而是分为 Eden 区和两个 Survivor 区(8:1:1),每次使用 Eden 区和其中一个 Survivor 区,GC 时将存活的对象复制到另一个空的 Survivor 区,清空 Eden 和使用过的 Survivor 区。
  • 优点
    1. 不会产生内存碎片,分配对象时只需要指针移动,实现简单,分配效率高;
    2. 只需要复制存活对象,回收效率高,适合存活对象少的场景。
  • 缺点:可用内存被缩小了一部分,内存利用率低;如果存活对象过多,复制的开销会大幅增加。
  • 适用场景:年轻代,对象存活时间短、存活率低的场景(所有年轻代收集器的核心算法)。
(3)标记 - 整理算法(Mark-Compact)
  • 执行过程 :分为三个阶段,标记阶段 (标记所有存活对象)、整理阶段 (将所有存活对象向内存的一端移动,按顺序排列)、清除阶段(一次性清理掉边界以外的所有内存)。
  • 优点:不会产生内存碎片,内存利用率高,后续分配大对象时不会因为碎片问题提前触发 GC。
  • 缺点:需要移动存活对象,更新所有引用地址,执行过程中必须 STW,开销大,效率比标记 - 清除低。
  • 适用场景:老年代,对象存活时间长、存活率高的场景(比如 Parallel Old、Serial Old 收集器)。
(4)分代回收算法(Generational Collection)
  • 核心思想 :根据对象的存活周期,将堆内存分为年轻代和老年代,不同代采用不同的回收算法,兼顾回收效率和内存利用率。
    1. 年轻代:对象存活周期短,每次 GC 只有少量对象存活,采用标记 - 复制算法,只需要复制少量存活对象,效率极高;
    2. 老年代:对象存活周期长,存活率高,没有额外的内存空间做复制担保,采用标记 - 清除标记 - 整理算法
  • 是当前所有主流 JVM 的默认回收思想
6. GC 核心概念
(1)Stop-The-World(STW)
  • 定义:垃圾回收执行过程中,暂停所有用户线程,直到 GC 执行完成,这个暂停的过程就是 STW。
  • 为什么必须有 STW:可达性分析过程中,如果用户线程还在运行,会导致对象的引用关系不断变化,无法保证可达性分析结果的准确性,甚至会导致 GC 出错。
  • 核心优化目标:所有垃圾回收器的优化核心,就是尽可能缩短 STW 的时间,减少对用户线程的影响,提升系统响应速度。
(2)安全点(Safepoint)
  • 定义:用户线程执行过程中,特定的位置,只有到达这些位置,线程才可以暂停,响应 GC 的 STW 请求,这些位置就是安全点。
  • 安全点的选取原则:不能太少,导致 GC 等待线程时间过长;也不能太多,导致用户线程执行开销过大。通常会在方法调用、循环跳转、异常跳转等指令位置设置安全点。
(3)安全区域(Safe Region)
  • 定义:一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置,都可以安全地响应 GC 的 STW 请求,这个区域就是安全区域。
  • 解决的问题:对于处于 Sleep、Blocked 状态的线程,无法响应 GC 的安全点请求,无法走到安全点暂停,安全区域可以让这些线程在不执行的时候,也能安全地完成 GC。
(4)记忆集与卡表
  • 解决的问题:跨代引用问题。比如老年代的对象引用了年轻代的对象,Minor GC 时,为了判断年轻代的对象是否存活,需要扫描整个老年代,开销极大。
  • 记忆集:用于记录非收集区域指向收集区域的指针集合的抽象数据结构,避免全量扫描非收集区域。
  • 卡表:记忆集的具体实现,HotSpot 采用卡表实现记忆集。将堆内存划分为一系列大小固定的卡页(默认 512 字节),用一个字节数组维护每个卡页的状态,卡页中只要有一个对象存在跨代引用,就标记为脏卡,Minor GC 时只需要扫描脏卡即可,无需扫描整个老年代,大幅降低扫描开销。
(5)写屏障
  • 定义:在对象引用赋值的前后,加入的特定处理逻辑,类似 AOP 的切面。
  • 作用:维护卡表的状态,当对象的引用发生赋值时,通过写屏障将对应的卡页标记为脏卡,保证卡表数据的准确性。

二、主流垃圾回收器详解

垃圾回收器是垃圾回收算法的具体实现,不同的回收器适用于不同的场景,核心优化方向分为两类:吞吐量优先低延迟优先

吞吐量 = 用户代码执行时间 /(用户代码执行时间 + GC 执行时间),吞吐量越高,CPU 利用率越高,适合后台计算型任务;低延迟优先,就是尽可能缩短 STW 时间,适合用户交互、接口服务等对响应时间敏感的场景。

1. 串行回收器(单线程)
(1)Serial 收集器
  • 作用区域:年轻代
  • 核心算法:标记 - 复制算法
  • 核心特性:单线程执行 GC,执行 GC 时,必须暂停所有用户线程(STW),直到 GC 完成。
  • 优点:实现简单,内存开销小,没有线程交互的开销,单线程执行效率极高,是客户端模式下的默认年轻代收集器。
  • 缺点:STW 时间长,多核 CPU 下无法利用多核优势,不适合服务端应用。
(2)Serial Old 收集器
  • 作用区域:老年代
  • 核心算法:标记 - 整理算法
  • 核心特性:Serial 收集器的老年代版本,单线程执行,同样会触发全量 STW。
  • 使用场景:客户端模式下的默认老年代收集器;JDK5 之前和 Parallel Scavenge 配合使用;作为 CMS 收集器发生 Concurrent Mode Failure 时的后备预案。
2. 并行回收器(多线程,吞吐量优先)
(1)ParNew 收集器
  • 作用区域:年轻代
  • 核心算法:标记 - 复制算法
  • 核心特性:Serial 收集器的多线程版本,GC 时使用多线程执行,同样会触发 STW,除了多线程,其余行为和 Serial 完全一致。
  • 优点:多核 CPU 下,回收效率远高于 Serial,能充分利用多核优势;是唯一能和 CMS 收集器配合的年轻代收集器。
  • 缺点:单核 CPU 下,效率不如 Serial,因为有线程切换的开销。
(2)Parallel Scavenge 收集器
  • 作用区域:年轻代
  • 核心算法:标记 - 复制算法
  • 核心特性 :多线程并行回收,核心目标是达到可控制的吞吐量,也被称为 "吞吐量优先收集器"。
  • 核心优势
    1. 可精准控制吞吐量:提供-XX:MaxGCPauseMillis(最大 GC 停顿时间)、-XX:GCTimeRatio(吞吐量占比)两个参数,精准控制 GC 的行为;
    2. 自适应调节策略:开启-XX:+UseAdaptiveSizePolicy(默认开启)后,JVM 会自动监控系统的运行状态,动态调整年轻代大小、Eden 和 Survivor 的比例、晋升老年代的年龄阈值,无需手动调优,只需要设置堆的最大内存和吞吐量目标即可。
  • 和 ParNew 的核心区别:核心优化目标不同,ParNew 关注缩短 STW 时间,Parallel Scavenge 关注吞吐量;Parallel Scavenge 有自适应调节策略,ParNew 没有。
(3)Parallel Old 收集器
  • 作用区域:老年代
  • 核心算法:标记 - 整理算法
  • 核心特性:Parallel Scavenge 收集器的老年代版本,多线程并行回收,吞吐量优先。
  • 使用场景:JDK8 默认的垃圾回收器组合(Parallel Scavenge + Parallel Old),适合后台计算、大数据处理等吞吐量优先的场景。
3. 并发标记清除回收器(CMS,低延迟优先)

CMS(Concurrent Mark Sweep)是 HotSpot 虚拟机中第一款真正意义上的并发收集器,实现了 GC 线程和用户线程同时工作,核心目标是最短回收停顿时间,适合对响应时间要求高的互联网应用。

  • 作用区域:老年代
  • 核心算法:标记 - 清除算法
  • 配合的年轻代收集器:ParNew
  • 核心执行过程(4 个阶段,面试高频)
    1. 初始标记(Initial Mark) :STW,只标记 GC Roots 能直接关联到的对象,速度极快,停顿时间非常短。
    2. 并发标记(Concurrent Mark):和用户线程并发执行,从初始标记的对象出发,遍历整个对象图,标记所有可达的存活对象,这个过程耗时较长,但不需要暂停用户线程。
    3. 重新标记(Remark):STW,修正并发标记期间,用户线程运行导致的标记变动的对象,停顿时间比初始标记长,但远短于并发标记的时间。
    4. 并发清除(Concurrent Sweep):和用户线程并发执行,清理所有标记的垃圾对象,释放内存空间,耗时较长,但不需要暂停用户线程。
  • 优点
    1. 低延迟:核心耗时的并发标记和并发清除阶段,都和用户线程并发执行,只有两个短暂的 STW 阶段,极大缩短了停顿时间,用户体验好;
    2. 并发执行:充分利用多核 CPU 的优势,GC 和用户线程同时运行,CPU 利用率高。
  • 缺点
    1. CPU 资源敏感:并发阶段会占用一部分 CPU 资源,导致用户线程的执行速度下降,CPU 核心数越少,影响越明显;
    2. 无法处理浮动垃圾:并发清除阶段,用户线程还在运行,会产生新的垃圾对象,这些对象只能等到下一次 GC 才能处理,称为浮动垃圾;因此 CMS 不能等到老年代完全满了再触发 GC,需要预留一部分内存给用户线程使用;
    3. Concurrent Mode Failure:CMS 运行期间,预留的内存无法满足用户线程创建新对象的需求,会触发该失败,此时 JVM 会降级使用 Serial Old 收集器,单线程执行 Full GC,导致长时间的 STW;
    4. 内存碎片 :基于标记 - 清除算法,会产生大量不连续的内存碎片,碎片过多会导致分配大对象时,无法找到足够的连续内存,提前触发 Full GC。CMS 提供了-XX:+UseCMSCompactAtFullCollection参数,在 Full GC 时开启内存碎片整理(STW),还有-XX:CMSFullGCsBeforeCompaction参数,设置多少次 Full GC 后执行一次碎片整理。
  • 使用场景:JDK8 及之前,对响应时间要求高的互联网服务、Web 应用,JDK9 之后逐渐被 G1 替代,JDK14 中被彻底移除。
4. G1 收集器(Garbage-First,兼顾吞吐量和低延迟)

G1 是 JDK9 默认的垃圾回收器,面向服务端应用,兼顾吞吐量和低延迟,打破了传统的分代物理划分,采用 Region 化的内存布局,支持可预测的停顿时间模型。

  • 核心内存布局 :不再将堆分为物理上隔离的年轻代和老年代,而是将整个堆划分为多个大小相等的独立区域(Region),每个 Region 的大小为 1~32MB,必须是 2 的幂次,可通过-XX:G1HeapRegionSize设置。
  • 逻辑分代 :每个 Region 可以根据需要,扮演 Eden 区、Survivor 区、老年代区,JVM 会动态调整每个角色的 Region 数量;同时新增了Humongous Region,专门存放大对象(对象大小超过 Region 容量的 50%),大对象直接存放在 Humongous Region,不会进入年轻代,避免了大对象在年轻代频繁复制的开销。
  • 核心算法:整体上是标记 - 整理算法,每个 Region 之间是标记 - 复制算法,不会产生内存碎片。
  • 核心目标 :在延迟可控的情况下,实现高吞吐量,支持设置最大停顿时间目标(-XX:MaxGCPauseMillis,默认 200ms),JVM 会根据停顿时间模型,优先回收垃圾最多的 Region,保证 GC 的效率和停顿时间可控。
  • 核心执行过程(4 个阶段)
    1. 初始标记(Initial Mark):STW,标记 GC Roots 直接关联的对象,修改 TAMS 指针,为下一阶段用户线程在 Region 中分配对象做准备,停顿时间极短,借助 Minor GC 同步完成,几乎没有额外停顿。
    2. 并发标记(Concurrent Mark):和用户线程并发执行,从 GC Roots 出发,遍历整个堆的对象图,标记存活对象,耗时较长,可被用户线程中断;同时计算每个 Region 的存活对象占比、回收价值。
    3. 最终标记(Final Mark):STW,修正并发标记期间用户线程导致的标记变动,停顿时间比初始标记长,但远短于并发标记。
    4. 筛选回收(Live Data Counting and Evacuation):STW,根据设置的最大停顿时间,排序各个 Region 的回收价值和成本,优先选择回收收益最高的 Region 组成回收集,将存活对象复制到空的 Region 中,清空原 Region 的内存。这个阶段因为只处理一部分 Region,停顿时间可控,可多线程并行执行。
  • Mixed GC(混合 GC):G1 的核心回收模式,不是 Full GC,回收范围包括整个年轻代和一部分垃圾多的老年代 Region,根据停顿时间目标,动态调整回收的老年代 Region 数量,绝大多数场景下,G1 都是执行 Mixed GC,只有堆内存极度不足时,才会触发 Full GC(单线程 Serial Old,STW,需要避免)。
  • 优点
    1. 可预测的停顿时间模型:用户可设置最大停顿时间,JVM 会自动调整回收范围,保证停顿时间不超过目标;
    2. 无内存碎片:整体采用标记 - 整理,Region 之间采用复制算法,不会产生内存碎片,避免了 CMS 的碎片问题;
    3. 大对象优化:专门的 Humongous Region 处理大对象,避免了大对象频繁复制和提前进入老年代的问题;
    4. 兼顾吞吐量和低延迟:既可以实现接近 CMS 的低延迟,也可以实现接近 Parallel 的高吞吐量,适用场景更广。
  • 缺点
    1. 内存占用和执行开销比 CMS 高:需要为每个 Region 维护卡表,内存占用更高;写屏障的执行逻辑更复杂,用户线程运行时的开销更大;
    2. 小内存场景下,表现不如 CMS,堆内存大于 8G 时,优势才会明显体现。
  • 使用场景:JDK9 + 默认回收器,适合堆内存较大、对响应时间有要求的服务端应用,是当前企业中使用最广泛的回收器。
5. 超低延迟回收器(ZGC/Shenandoah)
(1)ZGC
  • 发布历程:JDK11 作为实验特性引入,JDK15 正式发布,JDK17 成为服务端模式默认回收器之一。
  • 核心目标:实现 TB 级别的堆内存下,最大停顿时间不超过 1ms,且停顿时间不会随着堆内存的增大而增加,极致的低延迟。
  • 核心技术 :染色指针、读屏障、Region 化布局、不分代(JDK21 开始支持分代 ZGC)、并发整理。
    • 染色指针:将对象的标记信息直接存储在指针的高地址位中,不需要在对象头中设置标记位,实现了并发标记和并发整理,无需 STW;
    • 读屏障:在读取对象引用时,执行少量的额外逻辑,保证并发整理过程中,对象引用的正确性,无需暂停用户线程。
  • 核心特性:几乎所有阶段都和用户线程并发执行,只有初始标记和最终标记有短暂的 STW,停顿时间不超过 1ms,支持 8MB~16TB 的堆内存,无内存碎片。
  • 使用场景:对延迟要求极高的金融交易、实时计算、互联网高并发接口服务,JDK11 + 版本可使用。
(2)Shenandoah
  • 由 RedHat 开发,和 ZGC 目标一致,都是超低延迟回收器,核心差异是通过转发指针和读屏障实现并发整理,JDK12 引入,OpenJDK 支持,OracleJDK 不支持。

三、垃圾回收器选型建议

表格

业务场景 推荐回收器组合 JDK 版本
客户端应用、单核 CPU、内存小 Serial + Serial Old 全版本
后台计算、大数据处理、吞吐量优先 Parallel Scavenge + Parallel Old JDK8 默认
互联网服务、响应时间要求高、堆内存 8G 以上 G1 JDK9 + 默认,JDK8 可手动开启
金融交易、实时系统、极致低延迟要求 ZGC JDK17+

第三章 JVM 内存调优全攻略

JVM 调优的核心不是盲目调整参数,而是基于业务场景,定位内存和 GC 问题,针对性优化,最终实现降低 GC 频率、减少 Full GC 次数、缩短 STW 时间、避免 OOM、提升系统吞吐量和稳定性的目标。

一、调优核心原则

  1. 优先优化业务代码,而非 JVM 参数:80% 以上的 JVM 问题都是业务代码导致的(比如内存泄漏、创建过多大对象、频繁调用 System.gc ()),优先排查和优化代码,再考虑 JVM 调优。
  2. 不盲目调优,先监控后优化:没有监控数据,就没有调优的依据,必须先收集 GC 日志、内存监控数据,定位问题根源,再针对性调整参数。
  3. 优先让 JVM 自适应,避免过度调优:JVM 有完善的自适应调节策略,比如 G1 的停顿时间模型、Parallel 的自适应大小调整,不要手动设置过多参数,限制 JVM 的自适应能力。
  4. 调优是迭代过程,必须有基准测试:每次调优后,必须通过压测验证效果,对比调优前后的吞吐量、响应时间、GC 情况,确认优化有效,避免调优后出现新的问题。
  5. Minor GC 优先,避免频繁 Full GC:Full GC 的 STW 时间远长于 Minor GC,调优的核心目标之一,就是减少 Full GC 的次数,让对象尽可能在年轻代被回收。

二、调优前置准备:核心 JVM 参数分类

1. 堆内存核心参数

表格

参数 含义 调优建议
-Xms 堆初始内存大小 建议和-Xmx设置为相同值,避免堆内存动态扩容和缩容带来的性能损耗
-Xmx 堆最大内存大小 建议设置为物理内存的 50%~70%,预留足够的内存给操作系统、元空间、直接内存和其他进程
-Xmn 年轻代大小 经典分代模型下,建议设置为堆内存的 30%~50%;IO 密集型业务可适当调大,CPU 密集型业务可适当调小;G1 不建议手动设置,由 JVM 自适应调整
-XX:NewRatio 老年代和年轻代的比例 默认 2,即老年代:年轻代 = 2:1;Parallel 回收器可根据业务调整,G1 不建议设置
-XX:SurvivorRatio Eden 区和单个 Survivor 区的比例 默认 8,即 Eden:S0:S1=8:1:1;如果 Survivor 区经常满,对象提前晋升老年代,可适当调大 Survivor 比例
-XX:MaxTenuringThreshold 对象晋升老年代的最大年龄阈值 默认 15,Parallel 回收器默认 6;如果对象存活时间长,可适当调大,避免对象提前进入老年代
-XX:+UseTLAB 开启线程本地分配缓冲区 默认开启,无需关闭,提升对象分配效率
-XX:PretenureSizeThreshold 大对象直接进入老年代的阈值 只对 Serial 和 ParNew 有效,默认 0,即所有对象优先在 Eden 分配;可设置该值,让超过阈值的大对象直接进入老年代,避免年轻代频繁复制
2. 元空间参数

表格

参数 含义 调优建议
-XX:MetaspaceSize 元空间初始大小,达到该值触发 GC 建议根据应用加载的类数量设置,默认 21MB,类加载多的应用可适当调大,减少元空间 GC 次数
-XX:MaxMetaspaceSize 元空间最大大小 建议设置一个上限(比如 512MB),避免元空间无限占用本地内存,默认无上限
3. 栈内存参数

表格

参数 含义 调优建议
-Xss 每个线程的栈内存大小 Linux/x64 默认 1MB;如果线程数过多,可适当调小(比如 256KB/512KB),避免内存不足;如果方法递归深度大,可适当调大
4. 直接内存参数

表格

参数 含义 调优建议
-XX:MaxDirectMemorySize 直接内存最大大小 默认和-Xmx一致;如果使用 NIO、Netty 等框架,可根据业务设置,避免直接内存 OOM
5. 垃圾回收器相关参数
(1)通用回收器参数

表格

参数 含义
-XX:+UseSerialGC 开启 Serial+Serial Old 回收器
-XX:+UseParallelGC 开启 Parallel Scavenge+Parallel Old 回收器(JDK8 默认)
-XX:+UseParNewGC 开启 ParNew 回收器,配合 CMS 使用
-XX:+UseConcMarkSweepGC 开启 CMS+ParNew 回收器
-XX:+UseG1GC 开启 G1 回收器(JDK9 + 默认)
-XX:+UseZGC 开启 ZGC 回收器(JDK11+)
-XX:MaxGCPauseMillis GC 最大停顿时间目标,G1、ZGC 有效
-XX:+DisableExplicitGC 禁用显式的 System.gc () 调用,避免手动触发 Full GC
(2)CMS 专属参数

表格

参数 含义
-XX:CMSInitiatingOccupancyFraction 老年代内存占用达到该比例时,触发 CMS GC,默认 68%
-XX:+UseCMSCompactAtFullCollection Full GC 时开启内存碎片整理
-XX:CMSFullGCsBeforeCompaction 多少次 Full GC 后执行一次碎片整理,默认 0,每次 Full GC 都整理
-XX:+CMSConcurrentMTEnabled 并发阶段开启多线程执行
(3)G1 专属参数

表格

参数 含义
-XX:G1HeapRegionSize 设置每个 Region 的大小,1~32MB,必须是 2 的幂次
-XX:G1MixedGCCountTarget 一次并发标记后,执行 Mixed GC 的次数,默认 8
-XX:G1HeapWastePercent 堆内存允许的浪费比例,默认 5%,当可回收内存低于该比例,停止 Mixed GC
-XX:InitiatingHeapOccupancyPercent 堆内存占用达到该比例时,触发并发标记周期,默认 45%
6. GC 日志与 OOM 诊断参数

表格

参数 含义 调优建议
-XX:+HeapDumpOnOutOfMemoryError OOM 时自动生成堆 dump 文件 必须开启,用于排查 OOM 问题
-XX:HeapDumpPath=/path/heapdump.hprof dump 文件的保存路径 建议设置到有足够磁盘空间的目录
-Xloggc:/path/gc.log 输出 GC 日志到指定文件(JDK8 及之前) 必须开启,用于分析 GC 情况
-XX:+PrintGCDetails 打印 GC 详细信息(JDK8 及之前) 配合 GC 日志使用
-XX:+PrintGCDateStamps 打印 GC 的时间戳(JDK8 及之前) 配合 GC 日志使用
-Xlog:gc*:file=/path/gc.log JDK9 + 统一日志框架,输出 GC 详细日志 JDK9 + 使用

三、JVM 调优全流程

步骤 1:确定业务指标与基准线

调优前必须明确业务的核心指标,建立性能基准线,否则无法判断调优效果:

  1. 核心指标
    • 吞吐量:系统每秒能处理的请求数(QPS/TPS);
    • 响应时间:接口的平均响应时间、P95/P99 响应时间;
    • 可用性:系统的错误率、超时率;
    • GC 指标:Minor GC 的频率和平均停顿时间、Full GC 的频率和停顿时间。
  2. 压测获取基准线:使用 JMeter、Gatling 等压测工具,模拟线上流量,获取系统在正常负载和峰值负载下的基准数据,同时收集 GC 日志、内存使用情况。
步骤 2:监控与数据收集

通过监控工具收集 JVM 运行数据,定位问题,核心收集两类数据:

  1. 实时运行数据:堆内存各区域的使用情况、GC 的实时频率和停顿时间、线程状态、CPU 和内存占用;
  2. 离线日志数据:完整的 GC 日志、OOM 时的堆 dump 文件、线程 dump 文件。
步骤 3:问题定位与根因分析

根据收集的数据,定位核心问题,常见的 JVM 问题分为以下几类:

表格

问题现象 常见根因
频繁 Minor GC 新生代设置过小、Eden 区比例不合理、业务代码频繁创建大量短生命周期对象、Survivor 区过小,对象频繁晋升老年代
频繁 Full GC 老年代内存不足、元空间溢出、显式调用 System.gc ()、内存泄漏、大对象过多、CMS 的 Concurrent Mode Failure、老年代内存碎片过多
OOM 异常 堆内存溢出(对象创建过多、内存泄漏)、元空间溢出(类加载过多)、直接内存溢出、栈内存溢出、堆外内存泄漏
STW 时间过长、系统卡顿 GC 收集器选型不合理、堆内存过大、Full GC 频繁、安全点设置不合理、YGC 停顿时间过长、内存碎片过多
系统吞吐量低 GC 占用过多 CPU 资源、频繁 GC 导致用户线程执行时间不足、自适应策略被限制、内存分配效率低
步骤 4:针对性优化

根据问题根因,先优化业务代码,再调整 JVM 参数:

  1. 业务代码优化(优先)
    • 避免频繁创建大量临时对象,复用对象,减少垃圾生成;
    • 避免创建大对象(比如超大 byte 数组、大字符串),减少大对象对内存的影响;
    • 排查内存泄漏,释放无效的对象引用(比如静态集合类只添加不删除、ThreadLocal 未手动 remove、流资源未关闭);
    • 移除代码中显式的 System.gc () 调用,避免手动触发 Full GC;
    • 优化循环、递归,减少方法调用深度,避免栈溢出。
  2. JVM 参数优化
    • 内存大小优化:调整堆内存、年轻代、元空间的大小,匹配业务场景;
    • 回收器选型优化:根据业务的吞吐量 / 延迟需求,更换合适的回收器;
    • 回收策略优化:调整 Survivor 比例、晋升阈值、GC 触发阈值等参数,优化 GC 行为;
    • 日志与诊断优化:开启 GC 日志和 OOM 自动 dump,方便后续排查。
步骤 5:压测验证与迭代
  1. 调整参数后,使用相同的压测场景,重新执行压测,对比调优前后的核心指标;
  2. 确认指标是否达到预期,GC 情况是否改善,是否出现新的问题;
  3. 如果未达到预期,重新分析数据,调整优化方案,重复上述步骤,直到达到业务目标。
步骤 6:线上监控与持续优化
  1. 调优完成后,上线应用,通过 Prometheus+Grafana、SkyWalking、Pinpoint 等 APM 工具,持续监控线上 JVM 的运行状态;
  2. 关注业务高峰期的 GC 情况、内存使用情况,及时发现和解决潜在问题;
  3. 随着业务迭代,流量和代码发生变化,定期重新评估 JVM 参数,持续优化。

四、常用 JVM 监控与故障排查工具

1. 命令行工具(JDK 自带,线上必备)

表格

工具 核心作用 常用命令
jps 查看当前系统中所有的 Java 进程,获取进程 PID jps -l:输出进程 PID 和主类全限定名
jstat 实时监控 JVM 的 GC 情况、类加载情况、内存使用情况 jstat -gc <pid> 1000 10:每秒输出一次 GC 统计信息,共输出 10 次
jmap 生成堆 dump 文件,查看堆内存使用情况、对象统计信息 jmap -dump:format=b,file=heapdump.hprof <pid>:生成堆 dump 文件;jmap -histo <pid>:输出堆中对象的统计信息
jstack 生成线程 dump 文件,查看线程状态、死锁、阻塞问题 jstack <pid> > threaddump.txt:输出线程栈信息到文件
jcmd JDK 全能工具,整合了 jps、jstat、jmap、jstack 的所有功能 jcmd <pid> GC.heap_dump heapdump.hprof:生成堆 dump;jcmd <pid> Thread.print:输出线程栈;jcmd <pid> GC.class_histogram:输出对象统计
jhat 分析堆 dump 文件,生成 HTML 页面,查看对象信息 现在基本被 MAT 替代,很少使用
2. 可视化分析工具

表格

工具 核心作用 适用场景
MAT(Memory Analyzer Tool) Eclipse 出品的堆 dump 分析工具,功能强大,可快速定位内存泄漏、大对象占用、对象引用关系 线下分析 OOM 的堆 dump 文件,定位内存泄漏根因
VisualVM JDK 自带的可视化工具,可实时监控 JVM 的内存、GC、线程、CPU 情况,生成和分析堆 dump 本地开发、测试环境监控和问题排查
JConsole JDK 自带的轻量级可视化监控工具,可实时查看内存、线程、类加载、MBean 信息 简单的实时监控,入门级工具
JProfiler 商业级性能分析工具,功能全面,可监控内存、CPU、线程、GC,支持线上远程监控 复杂的性能问题排查,深度调优
Arthas 阿里开源的 Java 线上诊断神器,无需重启应用,可实时查看 JVM 状态、方法执行耗时、类加载信息、修改日志级别,支持在线热更新代码 线上生产环境问题排查,无需停机,功能极其强大
GCEasy 在线 GC 日志分析工具,上传 GC 日志,自动生成可视化分析报告,给出优化建议 快速分析 GC 日志,定位 GC 问题,无需安装工具

五、常见问题调优实战案例

案例 1:频繁 Young GC 优化
  • 现象:压测时,QPS 上不去,CPU 占用高,Minor GC 每秒触发 1 次以上,每次停顿时间 10ms 以上,Survivor 区使用率经常达到 100%,对象频繁提前晋升老年代。
  • 根因分析:新生代设置过小,Eden 区容量不足,业务代码频繁创建大量临时对象,导致 Eden 区快速填满,触发 YGC;Survivor 区设置过小,存活对象无法容纳,直接晋升老年代。
  • 优化方案
    1. 调大新生代大小,-Xmn从 2G 调整到 4G,堆总内存-Xms/-Xmx从 8G 调整到 12G;
    2. 调整-XX:SurvivorRatio从 8 调整到 6,增大 Survivor 区的容量,避免对象提前晋升;
    3. 优化业务代码,复用对象,减少循环中临时对象的创建,降低垃圾生成速度。
  • 优化效果:YGC 频率降低到每 10 秒 1 次,每次停顿时间缩短到 5ms 以内,对象晋升老年代的频率大幅降低。
案例 2:频繁 Full GC 优化
  • 现象:线上系统每隔 1 小时触发 1 次 Full GC,每次停顿时间超过 1s,系统卡顿,老年代内存占用在 Full GC 后快速上涨。
  • 根因分析:通过 MAT 分析堆 dump 文件,发现静态 HashMap 中缓存了大量的业务对象,只添加不删除,没有过期策略,导致内存泄漏,老年代被占满,触发 Full GC;Full GC 只能回收少量无效对象,很快又会被填满。
  • 优化方案
    1. 修复业务代码,给缓存添加过期时间和最大容量限制,使用 Guava Cache 替代手动实现的 HashMap,避免内存泄漏;
    2. 开启-XX:+DisableExplicitGC,禁用代码中显式的 System.gc () 调用;
    3. 调整老年代大小,优化 CMS 的触发阈值-XX:CMSInitiatingOccupancyFraction从 68% 调整到 75%,减少 CMS GC 的频率。
  • 优化效果:内存泄漏问题解决,Full GC 频率降低到每天 1 次以内,系统卡顿问题消失。
案例 3:OOM 问题排查与优化
  • 现象 :线上系统频繁抛出OutOfMemoryError: Java heap space异常,服务宕机。
  • 排查步骤
    1. 开启-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动生成堆 dump 文件;
    2. 用 MAT 打开 dump 文件,查看占用内存最大的对象,发现是一个超大的 List 集合,占用了 80% 的堆内存;
    3. 查看引用链,找到该集合的创建位置,发现是业务代码中,一次性从数据库查询了全表 1000 万条数据,加载到内存中处理,导致堆内存溢出。
  • 优化方案
    1. 优化业务代码,全表查询改为分页查询,分批处理数据,避免一次性加载全量数据到内存;
    2. 调整堆内存大小,-Xms/-Xmx从 4G 调整到 8G,预留足够的内存空间;
    3. 增加参数校验,限制查询的最大条数,避免非法请求导致的全表查询。
  • 优化效果:OOM 问题彻底解决,系统运行稳定。

六、JVM 调优最佳实践

  1. 堆内存设置-Xms-Xmx必须设置为相同值,避免堆动态扩容的性能损耗;堆最大内存不要超过物理内存的 70%,预留足够的内存给操作系统和其他进程。
  2. 新生代设置:经典分代模型下,新生代大小建议为堆的 30%~50%,避免新生代过小导致频繁 YGC,也避免新生代过大导致老年代空间不足;G1 回收器不建议手动设置新生代大小,让 JVM 自适应调整。
  3. 回收器选型:JDK8 优先使用 Parallel(吞吐量优先)或 CMS(低延迟优先);JDK9 + 优先使用 G1;JDK17 + 对延迟要求极高的场景使用 ZGC。
  4. 日志与诊断:线上环境必须开启 GC 日志和 OOM 自动 dump,方便问题排查;日志路径要设置到有足够磁盘空间的目录,避免磁盘占满。
  5. 线上操作规范 :线上环境不要随便执行jmap -dumpjstack等命令,尤其是大堆内存场景,可能会触发长时间 STW,导致系统卡顿;优先使用 Arthas 等非侵入式工具排查问题。
  6. 避免过度调优:不要盲目复制网上的参数配置,每个业务场景的特性不同,必须基于自己的监控数据调优;优先使用 JVM 的默认自适应策略,只调整核心参数。

第四章 全网高频 JVM 面试题详解

一、JVM 基础篇

1. 什么是 JVM?JVM 的主要作用是什么?

标准答案:JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是一个虚构出来的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现,是 Java 跨平台特性的核心。核心作用:

  1. 执行 Java 字节码 :将编译后的.class字节码文件,通过解释器或即时编译器(JIT)翻译成机器码,在操作系统上执行;
  2. 内存管理:自动管理 Java 程序的内存分配和回收,实现垃圾自动回收,避免手动内存管理的泄漏和野指针问题;
  3. 跨平台实现:屏蔽了底层操作系统和硬件的差异,Java 程序只需要编译成字节码,就可以在任意安装了对应平台 JVM 的设备上运行,实现 "一次编写,到处运行";
  4. 安全保障:提供了字节码验证、安全沙箱等机制,限制 Java 程序的非法操作,保障执行安全。
2. JDK、JRE、JVM 的区别和联系?

标准答案 :三者是包含关系,范围从大到小:JDK > JRE > JVM

  1. JVM:Java 虚拟机,核心负责执行字节码,是 Java 程序能够运行的核心,本身无法单独使用,需要配合类库等资源。
  2. JRE :Java Runtime Environment(Java 运行时环境),是 Java 程序运行的最小环境,包含JVM + Java 核心类库(rt.jar 等)、运行时的工具和资源。如果只需要运行已经编译好的 Java 程序,安装 JRE 即可。
  3. JDK :Java Development Kit(Java 开发工具包),是 Java 开发的完整环境,包含JRE + 编译工具(javac)、调试工具(jdb)、监控工具(jps/jstat 等)、开发所需的类库和文档。Java 开发必须安装 JDK。
3. 一个 Java 程序从编写到执行的全过程是什么?

标准答案 :Java 程序从编写到执行,分为编译期运行期两个核心阶段:

  1. 编译期
    • 开发者编写.java后缀的 Java 源代码文件;
    • 通过 JDK 的javac编译器,将 Java 源代码编译成 JVM 可识别的.class字节码文件,编译过程中会进行语法校验、语义分析,生成字节码;
    • 字节码文件不面向任何特定的操作系统,只面向 JVM,是跨平台的核心。
  2. 运行期
    • 通过java命令启动 JVM,JVM 的类加载器(ClassLoader)将字节码文件加载到 JVM 的方法区中;
    • 类加载完成后,JVM 会通过字节码解释器,将字节码逐行翻译成机器码,交给操作系统执行;
    • 对于热点代码(频繁执行的方法、循环体),JVM 的即时编译器(JIT)会将其编译成本地机器码,进行优化,直接执行,提升执行效率;
    • 程序运行过程中,JVM 自动进行内存分配和垃圾回收,程序执行结束后,JVM 退出。
4. 什么是字节码?字节码的好处是什么?

标准答案 :字节码是 Java 源代码经过javac编译后生成的.class文件中的内容,由一系列 JVM 可识别的字节指令组成,每个指令占 1 个字节,因此称为字节码。字节码的核心好处:

  1. 实现跨平台:字节码不面向特定的操作系统和硬件,只面向 JVM,不同平台的 JVM 都可以识别和执行相同的字节码,实现了 Java 的 "一次编写,到处运行";
  2. 屏蔽了底层硬件和操作系统的差异:开发者无需关注底层平台的细节,只需要编写一次代码;
  3. 提升了执行效率:相比源代码,字节码是经过编译优化的中间格式,JVM 执行字节码的效率远高于直接解释执行源代码;
  4. 提供了安全保障:字节码在加载时会经过 JVM 的字节码验证,校验格式、语义、安全性,避免恶意代码执行,保障了程序的安全。
5. JVM 的主要组成部分有哪些?

标准答案:JVM 主要分为 4 大核心组成部分,各司其职,共同完成 Java 程序的执行和管理:

  1. 类加载子系统 :负责从文件系统或网络中加载.class字节码文件,完成类的加载、验证、准备、解析、初始化全流程,将类信息加载到方法区中。
  2. 运行时数据区:也就是 JVM 内存结构,分为程序计数器、虚拟机栈、本地方法栈、堆、方法区,负责存储 JVM 运行过程中的数据、对象、指令等信息。
  3. 执行引擎 :JVM 的核心执行单元,负责执行字节码指令,分为 3 个部分:
    • 解释器:逐行解释字节码,翻译成机器码执行,启动快,执行效率低;
    • 即时编译器(JIT):将热点代码编译成本地机器码,进行深度优化,执行效率高;
    • 垃圾回收器:自动回收堆内存中不再使用的对象,释放内存空间。
  4. 本地方法接口:负责调用 Native 本地方法(C/C++ 实现),通过本地方法栈管理 Native 方法的执行,扩展 JVM 的能力,与底层操作系统交互。

二、JVM 内存结构篇

1. 详细介绍 JVM 运行时数据区?

标准答案 :JVM 运行时数据区是 JVM 在运行时对内存的逻辑划分,根据线程隔离性,分为线程私有区域线程共享区域两大类,具体如下:

  1. 线程私有区域 :随线程创建而创建,线程结束而销毁,线程之间互不影响。
    • 程序计数器:记录当前线程执行的字节码指令的地址,是线程恢复执行的基础,唯一不会抛出 OOM 的区域。
    • Java 虚拟机栈:为 Java 方法执行服务,每个方法执行都会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态链接、方法返回地址,方法调用对应入栈,执行完成对应出栈,会抛出 StackOverflowError 和 OOM。
    • 本地方法栈:和虚拟机栈功能一致,为 Native 本地方法服务,HotSpot 将其与虚拟机栈合二为一。
  2. 线程共享区域 :随 JVM 启动而创建,JVM 关闭而销毁,所有线程共享。
    • Java 堆:JVM 中最大的内存块,唯一目的是存放对象实例和数组,是垃圾回收的核心区域,分为年轻代(Eden+2 个 Survivor)和老年代,会抛出 OOM。
    • 方法区:存储已加载的类信息、常量、静态变量、JIT 编译后的代码缓存,JDK8 之前用永久代实现,JDK8 之后用元空间实现,会抛出 OOM;运行时常量池是方法区的一部分,存储类加载后的常量信息。
  3. 直接内存:不属于 JVM 运行时数据区,是堆外的本地内存,被 NIO 频繁使用,通过 DirectByteBuffer 引用管理,避免堆内和堆外的数据拷贝,会抛出 OOM。
2. 程序计数器为什么是线程私有的?为什么不会出现 OOM?

标准答案

(1)为什么是线程私有的?

JVM 的多线程是通过 CPU 时间片轮转实现的,同一时刻,一个 CPU 核心只会执行一个线程中的指令,当线程切换时,需要记录每个线程当前执行到的字节码指令地址,切换回来后才能从正确的位置继续执行。如果程序计数器是线程共享的,多个线程切换时,会互相覆盖指令地址,无法保证线程执行的正确性,因此程序计数器必须是线程私有的,每个线程独立维护,互不干扰。

(2)为什么不会出现 OOM?

程序计数器只存储当前执行的字节码指令的地址,这个地址的大小是固定的,在 JVM 启动时就确定了内存占用,不会随着程序的运行而动态分配和扩展内存,也不会存储大量的动态数据,因此永远不会出现内存不足的情况,也就不会抛出 OutOfMemoryError 异常。

3. 方法区、永久代、元空间的区别和联系?JDK8 为什么用元空间替换永久代?

标准答案

(1)三者的区别和联系
  • 方法区:是 JVM 规范中定义的内存区域,是一个抽象的规范,用于存储已加载的类信息、常量、静态变量、JIT 编译后的代码等数据,所有 JVM 实现都必须遵守这个规范。
  • 永久代:是 HotSpot 虚拟机在 JDK8 之前,对方法区规范的具体实现,位于 JVM 的堆内存中,有固定的内存上限,和老年代的 GC 绑定,满了就会触发 Full GC,容易出现 OOM。
  • 元空间:是 HotSpot 虚拟机在 JDK8 及之后,对方法区规范的新实现,彻底替代了永久代,元空间不再使用 JVM 堆内存,而是直接使用操作系统的本地内存,默认只受物理内存限制,大幅降低了 OOM 的概率。

简单来说:方法区是接口规范,永久代和元空间是这个规范的两个不同实现版本,永久代是 JDK8 之前的实现,元空间是 JDK8 之后的实现。

(2)JDK8 用元空间替换永久代的核心原因
  1. 永久代的内存上限难以精准设置,容易出现 OOM :永久代的大小受-XX:MaxPermSize限制,设置过小会频繁触发永久代 OOM,设置过大会浪费堆内存;而元空间使用本地内存,默认无上限,只要物理内存足够,就不会出现类加载导致的 OOM。
  2. 永久代的 GC 效率极低:永久代的 GC 和老年代绑定,只要其中一个满了就会触发 Full GC,而类的元数据回收条件苛刻,很难被回收,导致 Full GC 频繁且效率低;元空间的 GC 独立,只在达到元空间初始阈值时触发,回收效率更高。
  3. 便于 JVM 的维护和扩展:永久代的代码和 GC 代码耦合度高,维护难度大;元空间使用本地内存,由操作系统管理内存,降低了 JVM 的内存管理复杂度,同时可以更灵活地扩展元数据的存储。
  4. 避免了类加载器的内存泄漏问题:动态代理、反射、热部署等场景会频繁加载类,永久代中这些类的元数据很难被回收,容易出现内存泄漏;元空间使用本地内存,回收更彻底,大幅降低了内存泄漏的概率。
4. Class 常量池、运行时常量池、字符串常量池的区别?

标准答案:三者是完全不同的概念,存储的内容、位置、生命周期都不同,具体区别如下:

  1. Class 常量池(静态常量池)

    • 位置 :存在于.class字节码文件中,是每个 Class 文件自带的常量池,编译期就确定了内容。
    • 存储内容 :存放编译期生成的两大类常量:
      • 字面量:比如字符串字面量、final 修饰的常量、基本数据类型的值;
      • 符号引用:比如类和方法的全限定名、字段的名称和描述符、方法的名称和描述符。
    • 生命周期:编译后就固定在 Class 文件中,类加载时会被读取到运行时常量池中。
  2. 运行时常量池

    • 位置:JDK8 之前位于永久代,JDK8 之后位于元空间,属于方法区的一部分。
    • 存储内容 :Class 常量池加载到内存后,就会放入运行时常量池中,同时具备动态性,运行期也可以将新的常量放入池中(比如String.intern()方法)。
    • 生命周期:随类的加载而创建,随类的卸载而销毁,每个类对应一个运行时常量池。
  3. 字符串常量池(String Table)

    • 位置:JDK6 及之前位于永久代,JDK7 及之后移到了 Java 堆中,和运行时常量池完全分离。
    • 存储内容 :专门存储字符串对象的引用,底层是哈希表结构,避免重复创建字符串对象,节省内存。编译期的字符串字面量会自动放入池中,运行期可通过String.intern()手动将字符串引用放入池中。
    • 生命周期:随 JVM 启动而创建,全局唯一,所有线程共享,可被 GC 回收。
5. 字符串常量池在 JDK6、7、8 中的位置变化?为什么这么调整?

标准答案

(1)位置变化
  • JDK6 及之前:字符串常量池存放在永久代中,和运行时常量池绑定,属于方法区的一部分。
  • JDK7:字符串常量池从永久代中移出,放到了 Java 堆中,可被堆的垃圾回收器回收。
  • JDK8 及之后:永久代被元空间替换,字符串常量池依然存放在 Java 堆中,位置没有变化。
(2)调整的核心原因
  1. 永久代的 GC 效率极低,容易出现 OOM:永久代的 GC 只有在 Full GC 时才会触发,而字符串是开发中使用最频繁的对象,大量的字符串对象存放在永久代中,很难被回收,很容易导致永久代内存溢出;放到堆中后,Minor GC 和 Full GC 都可以回收字符串常量池中的无效对象,大幅降低了 OOM 的概率。
  2. 永久代有固定的内存上限,无法灵活调整 :永久代的大小受-XX:MaxPermSize限制,开发中经常会创建大量的字符串对象,很容易达到永久代的上限,出现 OOM;放到堆中后,字符串常量池可以使用整个堆的内存,空间更大,调整更灵活。
  3. 提升字符串的回收效率:堆中的 GC 频率远高于永久代,字符串对象如果没有引用,会被快速回收,释放内存,避免内存泄漏,提升内存利用率。

三、垃圾回收篇

1. 可达性分析算法的原理?GC Roots 包含哪些对象?

标准答案

(1)可达性分析算法的原理

可达性分析算法是当前主流 JVM 判断对象是否存活的核心算法,原理是:以一系列名为GC Roots的根对象作为起点,从这些起点开始,按照引用关系向下遍历,遍历的路径称为引用链;如果一个对象到 GC Roots 之间没有任何引用链相连,也就是这个对象不可达,就说明该对象已经死亡,是可回收的垃圾对象。该算法解决了引用计数法无法解决的循环引用问题,判断精准,是 HotSpot 虚拟机的核心实现。

(2)GC Roots 包含的核心对象

GC Roots 必须是当前肯定存活的对象,不会被回收,具体包括:

  1. 虚拟机栈(局部变量表)中引用的对象:方法中定义的局部变量、方法参数,这些对象正在被线程执行,肯定存活;
  2. 本地方法栈中 JNI(Native 方法)引用的对象:Native 方法中引用的对象,由底层代码持有,肯定存活;
  3. 方法区中类静态属性引用的对象:static 修饰的静态变量,属于类,类加载后就一直存在,作为根对象;
  4. 方法区中常量引用的对象:final 修饰的常量,一旦赋值就不会改变,引用的对象肯定存活;
  5. 同步锁(synchronized)持有的对象:被加锁的对象,正在被线程持有,不会被回收;
  6. JVM 内部的基础对象:比如系统类加载器、核心异常类对象、常驻的字符串对象等,是 JVM 运行的基础,肯定存活。
2. Java 中的四种引用类型?分别的特点和使用场景?

标准答案:JDK1.2 之后,Java 将引用分为强引用、软引用、弱引用、虚引用 4 种类型,不同类型的回收策略不同,具体如下:

  1. 强引用(Strong Reference)

    • 特点 :最常见的引用类型,代码中Object obj = new Object()就是强引用;只要强引用关系存在,垃圾回收器永远不会回收该对象,哪怕 JVM 即将发生 OOM,也不会回收;如果强引用的对象不再使用,没有手动置为 null,就会导致内存泄漏。
    • 使用场景:日常开发中绝大多数的对象创建,是 Java 的默认引用类型。
  2. 软引用(SoftReference)

    • 特点 :通过SoftReference类包装的对象,属于非必需的引用;当 JVM 内存充足时,不会回收软引用对象;当 JVM 内存不足,即将发生 OOM 时,会回收软引用对象,如果回收后内存还是不足,才会抛出 OOM。
    • 使用场景:内存敏感的缓存场景,比如图片缓存、网页缓存、大数据查询结果缓存,内存充足时缓存可用,提升性能;内存不足时自动释放缓存,避免 OOM。
  3. 弱引用(WeakReference)

    • 特点 :通过WeakReference类包装的对象,引用强度比软引用更弱;无论 JVM 内存是否充足,只要触发垃圾回收,就会立即回收弱引用对象,生命周期只到下一次 GC 之前。
    • 使用场景:临时缓存、ThreadLocal 的 key 实现,避免内存泄漏;比如 ThreadLocal 的 key 是弱引用,当 ThreadLocal 对象没有强引用时,会被 GC 回收,避免 ThreadLocalMap 的 key 一直存在,导致内存泄漏。
  4. 虚引用(PhantomReference)

    • 特点 :也叫幽灵引用,通过PhantomReference类包装,是最弱的引用类型;无法通过虚引用获取对象实例,对对象的生命周期完全没有影响;唯一的作用是,当对象被垃圾回收器回收时,会收到一个系统通知,必须配合引用队列(ReferenceQueue)使用。
    • 使用场景:跟踪对象的垃圾回收过程,管理堆外内存;比如 JDK 的 DirectByteBuffer,就是通过虚引用跟踪对象的回收,当 DirectByteBuffer 对象被回收时,释放对应的堆外直接内存,避免内存泄漏。
3. 详细介绍 CMS 收集器的回收过程?优缺点?

标准答案:CMS(Concurrent Mark Sweep)是 HotSpot 第一款真正意义上的并发收集器,核心目标是最短回收停顿时间,基于标记 - 清除算法实现,作用于老年代,配合 ParNew 年轻代收集器使用。

(1)核心回收过程(4 个阶段)

CMS 的回收过程分为 4 个阶段,其中 2 个阶段会 STW,2 个阶段和用户线程并发执行:

  1. 初始标记(Initial Mark)

    • 会触发 STW,停顿时间极短;
    • 核心工作:只标记 GC Roots 能直接关联到的对象,不需要遍历整个对象图,执行速度极快,对用户线程影响极小。
  2. 并发标记(Concurrent Mark)

    • 和用户线程并发执行,不会 STW;
    • 核心工作:从初始标记的对象出发,遍历整个老年代的对象图,标记所有可达的存活对象,这个过程耗时较长,但因为和用户线程并发执行,不会影响用户线程的运行。
  3. 重新标记(Remark)

    • 会触发 STW,停顿时间比初始标记长,但远短于并发标记的时间;
    • 核心工作:修正并发标记期间,用户线程运行导致的对象引用关系变动,重新标记这些变动的对象,保证标记结果的准确性。
  4. 并发清除(Concurrent Sweep)

    • 和用户线程并发执行,不会 STW;
    • 核心工作:清理所有标记为垃圾的对象,释放对应的内存空间,耗时较长,和用户线程并发执行,不会暂停用户线程。
(2)核心优点
  1. 低延迟,停顿时间短:核心耗时的并发标记和并发清除阶段,都和用户线程并发执行,只有两个短暂的 STW 阶段,极大缩短了 GC 的停顿时间,对用户交互的影响极小,适合对响应时间要求高的互联网应用。
  2. 并发执行,充分利用多核 CPU:并发阶段会利用多核 CPU 的优势,GC 线程和用户线程同时运行,CPU 利用率高,在多核环境下表现优异。
(3)核心缺点
  1. CPU 资源敏感:并发阶段会占用一部分 CPU 核心和资源,导致用户线程的执行速度下降,CPU 核心数越少,影响越明显;比如单核 CPU 下,CMS 的并发执行会导致用户线程的执行效率下降 50% 以上。
  2. 无法处理浮动垃圾:并发清除阶段,用户线程还在运行,会持续产生新的垃圾对象,这些对象在本次标记阶段已经结束,无法被本次 GC 回收,只能等到下一次 GC 才能处理,这些垃圾称为浮动垃圾;因此 CMS 不能等到老年代完全满了再触发 GC,必须预留一部分内存给用户线程使用,内存利用率较低。
  3. Concurrent Mode Failure 风险:CMS 运行期间,如果预留的内存无法满足用户线程创建新对象的需求,就会触发 Concurrent Mode Failure,此时 JVM 会立即降级,使用 Serial Old 单线程收集器执行 Full GC,导致长时间的 STW,严重影响系统性能。
  4. 内存碎片问题:CMS 基于标记 - 清除算法实现,回收后会产生大量不连续的内存碎片,碎片过多会导致后续分配大对象时,无法找到足够的连续内存,提前触发 Full GC,影响系统稳定性。
4. G1 收集器的核心特点?和 CMS 的核心区别?

标准答案

(1)G1 收集器的核心特点

G1(Garbage-First)是 JDK9 默认的垃圾回收器,面向服务端应用,兼顾吞吐量和低延迟,核心特点如下:

  1. Region 化的内存布局:打破了传统分代回收的物理隔离,将整个堆划分为多个大小相等的 Region,每个 Region 可以根据需要,动态扮演 Eden 区、Survivor 区、老年代区、大对象区(Humongous Region),无需固定的分代大小,内存管理更灵活。
  2. 可预测的停顿时间模型 :用户可通过-XX:MaxGCPauseMillis设置最大 GC 停顿时间目标,G1 会根据每个 Region 的回收价值(垃圾占比、回收耗时),优先选择回收收益最高的 Region 组成回收集,保证 GC 的停顿时间不超过用户设置的目标,实现了停顿时间的可控性。
  3. Mixed GC 混合回收模式:G1 的核心回收模式,不是整堆的 Full GC,回收范围包括整个年轻代和一部分垃圾占比高的老年代 Region,根据停顿时间目标,动态调整回收的老年代 Region 数量,既保证了 GC 效率,又控制了停顿时间。
  4. 无内存碎片:整体上基于标记 - 整理算法,Region 之间基于标记 - 复制算法,回收后会将存活对象复制到空的 Region 中,清空原 Region,不会产生内存碎片,避免了 CMS 的碎片问题,减少了提前触发 Full GC 的概率。
  5. 大对象优化:专门设计了 Humongous Region,存放超过 Region 容量 50% 的大对象,大对象直接存放在老年代的 Humongous Region,不会在年轻代频繁复制,也不会提前晋升老年代,大幅降低了大对象对 GC 的影响。
  6. 兼顾吞吐量和低延迟:既可以实现接近 CMS 的低延迟,也可以实现接近 Parallel 的高吞吐量,适用场景更广,堆内存越大,优势越明显。
(2)G1 和 CMS 的核心区别

表格

对比维度 G1 收集器 CMS 收集器
内存布局 Region 化逻辑分代,无物理隔离的年轻代和老年代 物理隔离的年轻代和老年代,固定分代结构
核心算法 整体标记 - 整理,Region 间标记 - 复制,无内存碎片 标记 - 清除算法,会产生大量内存碎片
回收模式 Mixed GC 为主,优先回收垃圾多的 Region,停顿时间可控 整堆的老年代回收,无法选择回收范围
停顿时间控制 支持可预测的停顿时间模型,用户可设置最大停顿目标 无明确的停顿时间控制,只能优化参数降低停顿
大对象处理 专门的 Humongous Region 存放大对象,优化大对象回收 大对象直接进入老年代,无专门优化,容易触发 Full GC
适用场景 堆内存 8G 以上,兼顾吞吐量和低延迟的服务端应用 堆内存较小,对延迟要求高的应用,大堆下表现不佳
内存开销 每个 Region 都需要维护卡表,内存占用和执行开销更高 卡表维护简单,内存开销更低
Full GC 触发 堆内存极度不足时才会触发,日常以 Mixed GC 为主 老年代占比达到阈值就会触发,碎片过多也会提前触发
5. 什么是 STW?为什么必须有 STW?

标准答案

(1)STW 的定义

STW 是 Stop-The-World 的缩写,指的是垃圾回收器执行 GC 的过程中,暂停 JVM 中所有的用户线程,直到 GC 执行完成,这个暂停的过程就是 STW。无论是哪种垃圾回收器,都无法完全避免 STW,只是停顿时间的长短不同,STW 是影响 Java 程序响应时间的核心因素之一,也是垃圾回收器优化的核心目标。

(2)必须有 STW 的核心原因

STW 的核心目的,是保证垃圾回收器的可达性分析结果的准确性,具体原因如下:

  1. 避免对象引用关系动态变化:可达性分析是判断对象是否存活的核心,分析过程中,如果用户线程还在运行,会持续修改对象的引用关系,比如新建对象、删除对象引用、对象赋值等,会导致可达性分析的结果不准确,出现已经标记为存活的对象被回收,或者垃圾对象没有被标记的情况,导致 JVM 崩溃。
  2. 保证垃圾回收的原子性:垃圾回收的标记、复制、整理过程中,对象的内存地址会发生变化,如果用户线程同时访问这些对象,会出现访问到错误的内存地址,导致程序异常,STW 可以保证垃圾回收的原子性,避免这种情况发生。
  3. 安全点的要求:用户线程只有到达安全点才能暂停,STW 可以保证所有用户线程都到达安全点,暂停执行,不会出现部分线程还在运行的情况,保证 GC 的顺利执行。

简单来说,如果没有 STW,用户线程和 GC 线程同时运行,会导致 GC 的标记结果混乱,对象的内存地址变动无法同步,最终导致 GC 出错,程序崩溃,因此 STW 是必须的。

四、JVM 调优篇

1. JVM 调优的核心目标是什么?调优的原则是什么?

标准答案

(1)JVM 调优的核心目标

JVM 调优不是盲目调整参数,而是基于业务场景,实现以下核心目标,优先级从高到低:

  1. 避免 OOM 和内存泄漏:这是最基础的目标,保证系统稳定运行,不会因为内存问题宕机;
  2. 减少 Full GC 的次数:Full GC 的 STW 时间远长于 Minor GC,是导致系统卡顿的核心原因,调优的核心就是尽可能减少 Full GC 的频率,让对象尽可能在年轻代被回收;
  3. 降低 GC 的停顿时间:缩短 Minor GC 和 Full GC 的 STW 时间,降低对用户线程的影响,提升系统的响应速度,降低接口的 P99 响应时间;
  4. 降低 GC 的频率:减少 Minor GC 的次数,降低 GC 对 CPU 资源的占用,提升系统的吞吐量;
  5. 提升系统的吞吐量和稳定性:最终目标是让系统在峰值流量下,依然保持高吞吐量、低响应时间、高可用性,稳定运行。
(2)JVM 调优的核心原则
  1. 优先优化业务代码,而非 JVM 参数:80% 以上的 JVM 问题都是业务代码导致的,比如内存泄漏、频繁创建大量对象、大对象加载等,优先排查和优化代码,再考虑 JVM 参数调优,不要本末倒置。
  2. 先监控后调优,无监控不调优:调优必须有明确的监控数据支撑,比如 GC 日志、堆内存使用情况、压测指标,没有监控数据,就无法定位问题,盲目调优只会带来新的问题。
  3. 优先使用 JVM 默认的自适应策略,避免过度调优:JVM 有完善的自适应调节策略,比如 G1 的停顿时间模型、Parallel 的新生代大小自适应,不要手动设置过多参数,限制 JVM 的自适应能力,大多数场景下,默认参数已经足够优秀。
  4. 调优是迭代过程,必须有基准测试:每次调优后,必须通过压测验证效果,对比调优前后的核心指标,确认优化有效,不达标则继续迭代,避免调优后性能反而下降。
  5. Minor GC 优先,避免频繁 Full GC:调优的核心是让对象在年轻代被回收,减少对象进入老年代,从而减少 Full GC 的次数,而不是一味调大堆内存。
  6. 线上环境谨慎操作:线上环境不要随便执行 dump、jstack 等命令,避免触发 STW,导致系统卡顿;不要盲目复制网上的参数配置,每个业务场景的特性不同,必须适配自己的业务。
2. 频繁 Full GC 的常见原因和解决方案?

标准答案:频繁 Full GC 是线上最常见的 JVM 问题,会导致系统长时间卡顿、CPU 占用高、响应时间变长,常见原因和对应的解决方案如下:

(1)内存泄漏,老年代被无效对象占满
  • 现象:Full GC 后老年代内存占用下降很少,很快又会上涨,Full GC 频率越来越高,最终 OOM;堆 dump 分析发现大量无效对象被引用,无法回收。
  • 常见根因:静态集合类只添加不删除、ThreadLocal 未手动 remove、流资源未关闭、缓存没有过期策略、监听器注册后未注销。
  • 解决方案
    1. 用 MAT 分析堆 dump 文件,定位内存泄漏的对象和引用链,修复业务代码,释放无效引用;
    2. 替换手动实现的缓存为 Guava Cache、Redis 等,添加过期时间和最大容量限制;
    3. 线程池使用 ThreadLocal 时,任务执行完成后必须手动 remove,避免线程复用导致的内存泄漏。
(2)老年代内存设置过小,无法容纳长期存活的对象
  • 现象:Full GC 后老年代内存占用正常,但很快就被占满,触发 Full GC,Survivor 区频繁满,对象提前晋升老年代。
  • 解决方案
    1. 调大堆总内存,同时调大老年代的占比,调整-XX:NewRatio,给老年代预留足够的空间;
    2. 调整-XX:SurvivorRatio-XX:MaxTenuringThreshold,增大 Survivor 区,提高对象晋升老年代的年龄阈值,让对象在年轻代多存活一段时间,避免提前进入老年代。
(3)大对象过多,频繁直接进入老年代
  • 现象:业务代码频繁创建超大对象(比如全表查询的 List、超大 byte 数组),大对象直接进入老年代,快速占满老年代,触发 Full GC。
  • 解决方案
    1. 优化业务代码,全表查询改为分页查询,分批处理数据,避免一次性加载全量数据到内存;
    2. 避免创建超大字符串、超大数组,拆分大对象为多个小对象;
    3. G1 收集器会自动处理大对象,大对象场景优先使用 G1,避免大对象频繁触发 Full GC。
(4)显式调用 System.gc (),手动触发 Full GC
  • 现象:GC 日志中频繁出现 System.gc () 触发的 Full GC,时间和业务操作对应。
  • 解决方案
    1. 排查代码,移除所有显式的 System.gc () 调用;
    2. 开启 JVM 参数-XX:+DisableExplicitGC,禁用显式的 System.gc () 调用,避免手动触发 Full GC。
(5)元空间溢出,触发 Full GC
  • 现象:Full GC 频繁,元空间内存占用持续上涨,GC 日志中出现 Metadata GC Threshold 触发的 Full GC。
  • 常见根因:动态代理、反射、热部署、框架动态生成类,导致类加载过多,元空间占满。
  • 解决方案
    1. 调大元空间的初始大小和最大大小,设置-XX:MetaspaceSize-XX:MaxMetaspaceSize,减少元空间 GC 的频率;
    2. 排查频繁生成动态类的代码,优化类加载逻辑,避免重复加载类。
(6)CMS 的 Concurrent Mode Failure,触发 Full GC
  • 现象:CMS GC 过程中,出现 Concurrent Mode Failure,随后触发 Serial Old 单线程 Full GC,长时间 STW。
  • 根因:CMS 并发清除阶段,用户线程创建的新对象超过了预留的内存,导致老年代不足。
  • 解决方案
    1. 调大老年代内存,降低 CMS 的触发阈值,调整-XX:CMSInitiatingOccupancyFraction,提前触发 CMS GC,预留更多的内存;
    2. 开启-XX:+UseCMSInitiatingOccupancyOnly,只按设置的阈值触发 CMS GC,避免 JVM 自动调整导致的 GC 不及时;
    3. 替换 CMS 为 G1 收集器,避免 Concurrent Mode Failure 问题。
3. OOM 的常见类型、排查步骤和解决方案?

标准答案:OOM(OutOfMemoryError)是 JVM 中最严重的内存问题,会导致服务宕机,常见的 OOM 类型、排查步骤和解决方案如下:

(1)OOM 的常见类型和根因
  1. java.lang.OutOfMemoryError: Java heap space

    • 最常见的 OOM 类型,堆内存不足,无法分配新的对象实例。
    • 根因:堆内存设置过小、内存泄漏、一次性加载大量数据到内存(比如全表查询)、频繁创建大对象。
  2. java.lang.OutOfMemoryError: Metaspace

    • 元空间内存不足,JDK8 之前是 PermGen space 永久代溢出。
    • 根因:动态代理、反射、热部署等场景加载了过多的类,元空间设置过小,类无法卸载。
  3. java.lang.OutOfMemoryError: Direct buffer memory

    • 直接内存溢出,堆外内存不足。
    • 根因:NIO、Netty 等框架频繁分配直接内存,直接内存设置过小,堆外内存泄漏,DirectByteBuffer 对象被回收后,堆外内存未释放。
  4. java.lang.StackOverflowError

    • 虚拟机栈溢出,线程栈深度超过 JVM 允许的最大值。
    • 根因:无限递归调用、方法调用链过长、-Xss设置的栈内存过小。
  5. java.lang.OutOfMemoryError: unable to create new native thread

    • 无法创建新的线程,操作系统内存不足。
    • 根因:创建了过多的线程,超过了操作系统允许的最大线程数,-Xss设置过大,导致每个线程占用内存过多,操作系统内存不足。
(2)OOM 的标准排查步骤
  1. 保留现场,获取诊断数据

    • 开启 JVM 参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/,OOM 时自动生成堆 dump 文件,这是排查的核心;
    • 收集 OOM 时的 GC 日志、线程 dump 文件、系统和 JVM 的监控数据,记录 OOM 的时间和业务场景。
  2. 定位 OOM 的类型

    • 查看异常日志,确定 OOM 的具体类型,不同类型的排查方向完全不同。
  3. 分析堆 dump 文件,定位根因

    • 使用 MAT、JProfiler 等工具打开堆 dump 文件,查看占用内存最大的对象,分析对象的引用链;
    • 确定是内存泄漏(对象无效但被引用,无法回收)还是内存溢出(对象确实太多,堆内存不足);
    • 找到占用内存的对象对应的业务代码,定位问题位置。
  4. 结合业务代码和监控数据,复现问题

    • 结合业务场景,分析问题代码的执行逻辑,在测试环境复现 OOM 问题,验证根因的准确性。
(3)通用解决方案
  1. 内存泄漏导致的 OOM:核心是修复业务代码,释放无效的对象引用,解决内存泄漏问题,比如给缓存添加过期策略、ThreadLocal 手动 remove、关闭流资源、移除无效的静态引用。
  2. 内存溢出导致的 OOM
    • 优化业务代码,避免一次性加载大量数据到内存,比如分页查询、分批处理;
    • 避免频繁创建大对象和临时对象,复用对象,减少垃圾生成;
    • 调大对应的内存区域,比如堆内存、元空间、直接内存,给 JVM 预留足够的内存。
  3. 栈溢出 :优化递归逻辑,避免无限递归,拆分过长的方法调用链,适当调大-Xss栈内存大小。
  4. 无法创建线程 :优化线程池,避免无限制创建线程,设置线程池的最大线程数,适当调小-Xss栈内存大小,降低每个线程的内存占用。

五、进阶篇

1. 什么是双亲委派模型?好处是什么?哪些场景破坏了双亲委派?

标准答案

(1)双亲委派模型的定义

双亲委派模型是 JVM 的类加载机制的核心模型,用于保证类加载的安全性,核心规则是:当一个类加载器收到类加载的请求时,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶层的启动类加载器(Bootstrap ClassLoader)中;只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,子类加载器才会尝试自己去加载。

(2)JVM 的三层类加载器
  1. 启动类加载器(Bootstrap ClassLoader) :顶层类加载器,由 C++ 实现,属于 JVM 的一部分,负责加载JAVA_HOME/lib目录下的核心类库(比如 rt.jar),无法被 Java 程序直接引用。
  2. 扩展类加载器(Extension ClassLoader) :由 Java 实现,负责加载JAVA_HOME/lib/ext目录下的扩展类库,开发者可以直接使用。
  3. 应用程序类加载器(Application ClassLoader):也叫系统类加载器,负责加载用户类路径(ClassPath)下的类库,开发者日常开发中默认使用的类加载器。
(3)双亲委派模型的核心好处
  1. 保证类的全局唯一性:避免同一个类被多个类加载器重复加载,保证全限定名相同的类,最终都是由顶层的启动类加载器加载,保证类的唯一性。
  2. 保证 Java 核心类库的安全性:防止开发者恶意编写和核心类库同名的类(比如自定义 java.lang.String),替换核心类库,导致安全问题;因为双亲委派模型下,核心类永远由启动类加载器加载,自定义的同名类不会被加载,避免了恶意代码的注入。
  3. 类加载的层级性:父类加载器加载的类,子类加载器都可以使用,保证了 Java 类库的基础类在所有的类加载器环境中都是统一的。
(4)破坏双亲委派模型的场景
  1. SPI 机制(Service Provider Interface):比如 JDBC、JNDI 等 SPI 机制,核心接口由启动类加载器加载,但是实现类由厂商提供,在 ClassPath 下,启动类加载器无法加载实现类,因此通过线程上下文类加载器(Thread Context ClassLoader),反向委托应用程序类加载器加载实现类,破坏了双亲委派模型。
  2. 热部署 / 热加载:比如 Tomcat、Spring Boot DevTools、JRebel 等热部署工具,每个 Web 应用都有自己独立的类加载器,优先加载自己目录下的类,不委托给父类加载器,实现了不同应用之间的类隔离,以及热更新,破坏了双亲委派模型。
  3. 自定义类加载器,重写 loadClass 方法 :双亲委派模型的核心逻辑在ClassLoader.loadClass()方法中,如果开发者自定义类加载器,重写了这个方法,不遵循双亲委派的规则,就会破坏双亲委派模型。
  4. JDK9 的模块化系统:JDK9 引入了模块化系统,类加载的规则发生了变化,启动类加载器可以加载模块路径下的类,打破了原有的双亲委派的层级结构,对双亲委派模型做了修改。
2. 什么是 JIT 即时编译器?分层编译是什么?

标准答案

(1)JIT 即时编译器的定义

JIT 是 Just-In-Time Compiler(即时编译器)的缩写,是 JVM 执行引擎的核心组成部分,用于提升 Java 程序的执行效率。Java 程序默认是通过解释器逐行解释字节码执行,执行效率较低;JIT 即时编译器会将热点代码(频繁执行的方法、循环体)在运行时直接编译成对应平台的本地机器码,并且进行深度的优化,之后执行这段代码时,直接执行编译后的机器码,无需解释,大幅提升执行效率。

(2)JIT 的核心编译对象:热点代码

热点代码分为两类:

  1. 被多次调用的方法;
  2. 被多次执行的循环体。JVM 通过热点探测机制判断热点代码,HotSpot 使用的是基于计数器的热点探测:给每个方法和循环体设置调用计数器,每调用一次,计数器 + 1,当计数器达到阈值(默认方法调用阈值 10000 次,循环体阈值 10700 次),就会被判定为热点代码,提交给 JIT 编译器编译。
(3)HotSpot 的两种 JIT 编译器
  1. C1 编译器(Client Compiler):客户端编译器,编译速度快,优化程度较低,注重启动速度,适合客户端应用、桌面程序。
  2. C2 编译器(Server Compiler):服务端编译器,编译速度慢,优化程度极高,注重执行效率,适合服务端应用,是服务端模式下的默认编译器。
(4)分层编译

分层编译是 JDK7 之后默认开启的编译模式,JDK8 默认开启,将 JVM 的编译过程分为 5 个层次,结合了解释器、C1 编译器、C2 编译器的优势,兼顾启动速度和执行效率,具体分层如下:

  1. 第 0 层:纯解释执行,不开启任何性能监控,解释器直接执行字节码。
  2. 第 1 层:C1 简单编译,C1 编译器将字节码编译成机器码,不开启性能监控,只做简单的优化,编译速度快。
  3. 第 2 层:C1 受限编译,C1 编译器编译,开启简单的性能监控(比如方法调用次数、循环回边次数),优化程度比第 1 层高。
  4. 第 3 层:C1 完全编译,C1 编译器编译,开启全部性能监控,优化程度最高,为 C2 编译提供数据支撑。
  5. 第 4 层:C2 完全编译,C2 编译器编译,进行极致的优化,比如方法内联、逃逸分析、标量替换、循环展开、空值消除等,优化程度最高,执行效率最高,编译速度慢。
(5)分层编译的核心优势
  1. 兼顾启动速度和执行效率:程序启动初期,用解释器和 C1 编译器快速编译执行,提升启动速度;程序运行一段时间后,热点代码用 C2 编译器深度优化,提升执行效率。
  2. 降低编译的资源占用:避免 C2 编译器编译大量非热点代码,浪费 CPU 资源,只对核心热点代码进行深度优化。
  3. 优化更精准:通过 C1 编译器的性能监控,收集代码的运行数据,给 C2 编译器提供更精准的优化依据,优化效果更好。
3. 什么是逃逸分析?标量替换?栈上分配?

标准答案:逃逸分析、标量替换、栈上分配,是 JIT 即时编译器的核心优化技术,JDK6 之后开始支持,JDK8 默认开启,用于减少对象在堆中的分配,降低 GC 的压力,提升程序执行效率。

(1)逃逸分析
  • 定义:逃逸分析是 JIT 编译器的一种动态分析技术,用于分析一个对象的引用范围,判断这个对象是否会逃逸出方法体或者线程,从而决定是否对这个对象进行优化。
  • 逃逸的两种类型
    1. 方法逃逸:对象在方法内部创建,被方法外部的其他方法引用,比如作为方法返回值返回、作为参数传递给其他方法。
    2. 线程逃逸:对象在方法内部创建,被其他线程访问,比如赋值给其他线程可以访问的静态变量、实例变量。
  • 逃逸程度从低到高:不逃逸 < 方法逃逸 < 线程逃逸;对象的逃逸程度越低,优化的空间越大。
  • 开启参数-XX:+DoEscapeAnalysis,JDK8 默认开启。
(2)栈上分配
  • 定义:对于没有发生逃逸的对象,JIT 编译器会将这个对象直接分配在虚拟机栈中,而不是分配在 Java 堆中。
  • 核心优势:对象分配在栈中,方法执行结束后,栈帧出栈,对象会随着栈帧一起被销毁,不需要垃圾回收器回收,大幅降低了 GC 的压力;同时栈的分配效率远高于堆,访问速度更快。
  • 注意:HotSpot 虚拟机没有真正实现完整的栈上分配,而是通过标量替换实现了栈上分配的效果,对象并没有直接分配在栈中,而是被拆解为标量分配在栈中。
(3)标量替换
  • 定义:标量是指无法再拆解的基本数据类型(比如 int、long、float 等),而聚合量是指可以拆解的对象(比如 Java 对象)。对于没有发生逃逸的对象,JIT 编译器不会创建这个对象的完整实例,而是将这个对象拆解为多个成员变量,这些成员变量被分配到虚拟机栈的局部变量表中,和普通的局部变量一样,这个过程就是标量替换。
  • 核心优势
    1. 不需要在堆中分配对象的内存,避免了对象的内存分配和 GC 回收的开销;
    2. 对象的成员变量分配在栈中,访问速度更快,还可以进行进一步的优化;
    3. 是 HotSpot 实现栈上分配效果的核心方式。
  • 开启参数-XX:+EliminateAllocations,JDK8 默认开启,依赖逃逸分析。
(4)额外的优化:同步消除
  • 定义:对于没有发生线程逃逸的对象,这个对象只会被当前线程访问,不会出现多线程竞争,JIT 编译器会消除掉这个对象上的同步锁(synchronized),这个过程就是同步消除,也叫锁消除。
  • 核心优势:消除了不必要的同步锁,避免了加锁和解锁的开销,提升程序执行效率。
  • 开启参数-XX:+EliminateLocks,JDK8 默认开启,依赖逃逸分析。
相关推荐
小江的记录本2 小时前
【JEECG Boot】 《JEECG Boot 数据字典使用教程》(完整版)
java·前端·数据库·spring boot·后端·spring·mybatis
鲸渔2 小时前
【C++ 变量与常量】变量的定义、初始化、const 与 constexpr
java·开发语言·c++
i220818 Faiz Ul2 小时前
教育资源共享平台|基于springboot + vue教育资源共享平台系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·教育资源共享平台
玛卡巴卡ldf2 小时前
【Springboot7】ApachePOI文件导入导出
java·spring boot·sql
编程大师哥2 小时前
VSCode中如何搭建JAVA+MAVEN
java·vscode·maven
不会写DN2 小时前
SQL 单表操作全解
java·服务器·开发语言·数据库·sql
Devin~Y2 小时前
大厂 Java 面试实战:从电商微服务到 AI 智能客服(含 Spring 全家桶、Redis、Kafka、RAG/Agent 解析)
java·spring boot·redis·elasticsearch·spring cloud·docker·kafka
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第十五期 - 策略模式】策略模式 —— 算法封装与动态替换实现、优缺点与适用场景
java·后端·设计模式·软件工程·策略模式
珍朱(珠)奶茶2 小时前
Spring Boot3整合FreeMark、itextpdf 5/7 实现pdf文件导出及注意问题
java·spring boot·后端·pdf·itextpdf