欢迎光临
我们一直在努力

JVM 常见八股面试问答:深入底层原理

🎯 适用:Java 后端开发、架构师、中间件开发岗位面试 📖 内容覆盖:内存模型 · 对象创建 · 类加载 · 垃圾回收算法 · 垃圾回收器 · JIT编译 · JVM调优 · 内存排查 · 字节码 · 线程模型 · G1/ZGC/Shenandoah · 工具监控 · 场景实战


目录

  • 一、JVM 内存模型(Q1-Q25)
  • 二、对象创建与内存分配(Q26-Q35)
  • 三、类加载机制(Q36-Q50)
  • 四、垃圾回收算法(Q51-Q65)
  • 五、垃圾回收器(Q66-Q85)
  • 六、JIT 编译与执行引擎(Q86-Q95)
  • 七、JVM 参数与调优(Q96-Q110)
  • 八、内存泄漏与溢出排查(Q111-Q125)
  • 九、字节码与执行引擎深入(Q126-Q135)
  • 十、线程模型与并发深入(Q136-Q145)
  • 十一、G1/ZGC/Shenandoah 深入(Q146-Q160)
  • 十二、JVM 工具与监控(Q161-Q170)
  • 十三、高级主题(Q171-Q185)
  • 十四、场景实战题(Q186-Q200)

一、JVM 内存模型

Q1: JVM 的内存模型(运行时数据区)由哪些部分组成?

答案: 根据 JVM 规范,运行时数据区分为以下部分:

区域线程共享存储内容异常
程序计数器(PC Register) ❌ 私有 当前执行的字节码指令地址 无 OOM
Java 虚拟机栈 ❌ 私有 栈帧(局部变量表、操作数栈、动态链接、返回地址) StackOverflowError / OOM
本地方法栈 ❌ 私有 Native 方法的栈帧 StackOverflowError / OOM
堆(Heap) ✅ 共享 对象实例、数组 OOM
方法区(元空间) ✅ 共享 类信息、常量、静态变量、JIT 代码缓存 OOM
运行时常量池 ✅ 共享 字面量、符号引用(方法区的一部分) OOM
直接内存 ✅ 共享 NIO 的 ByteBuffer(不属于 JVM 规范) OOM

底层实现:HotSpot VM 中,方法区在 JDK 8 后由元空间(Metaspace) 实现,使用本地内存(Native Memory)而非堆内存。元空间由 Metaspace 类管理,底层通过 ChunkManager 和 SpaceManager 分配内存。


Q2: 堆和栈的区别是什么?

答案:

对比项栈(Stack)堆(Heap)
用途 存储局部变量、方法调用上下文 存储对象实例、数组
线程 线程私有 线程共享
生命周期 方法结束即销毁 GC 回收时销毁
存取速度 快(LIFO,指针移动) 慢(需要 GC 管理)
空间大小 小(-Xss,默认 1MB) 大(-Xms/-Xmx)
异常 StackOverflowError OutOfMemoryError
内存分配 编译期确定大小(连续分配) 运行时动态分配(可能不连续)

底层原理:栈的分配/释放只需要移动栈指针(SP 寄存器),是一个 O(1) 的操作。堆的分配需要查找空闲内存块(通过空闲链表或 TLAB),释放需要 GC 标记-清除/复制,复杂度更高。


Q3: 栈中存的是对象还是引用?

答案: 栈中存储的是基本类型的值和对象的引用(reference),不是对象本身。

MyObject obj = new MyObject();

  • obj 是一个存储在栈上的引用(4 或 8 字节的指针)
  • new MyObject() 创建的对象存储在堆上
  • obj 的值是堆中对象的内存地址

底层实现:

  • 在 64 位 JVM 中,引用通常是 8 字节。如果开启指针压缩(-XX:+UseCompressedOops,默认开启),引用压缩为 4 字节
  • 指针压缩将 64 位地址压缩为 32 位偏移量,通过堆基地址 + 偏移量 × 8 的方式还原,支持最大 32GB 的堆

Q4: 堆分为哪几部分?

答案:

堆(Heap)
├── 新生代(Young Generation)—— 占堆的 1/3
│ ├── Eden 区 —— 占新生代的 80%
│ ├── Survivor 0(S0)—— 占新生代的 10%
│ └── Survivor 1(S1)—— 占新生代的 10%
└── 老年代(Old Generation)—— 占堆的 2/3

底层细节:

  • Eden:S0:S1 的比例由 -XX:SurvivorRatio=8 控制(默认 8:1:1)
  • 新生代与老年代的比例由 -XX:NewRatio=2 控制(默认老年代:新生代 = 2:1)
  • G1 收集器中,堆被划分为大小相等的 Region(1MB~32MB),不再严格分代

Q5: 程序计数器的作用是什么?为什么是私有的?

答案: 程序计数器记录当前线程正在执行的字节码指令地址(行号)。

为什么私有:

  • 多线程通过时间片轮转实现并发
  • 线程切换时,需要保存/恢复执行位置
  • 每个线程执行的代码位置不同,必须各自独立
  • 如果执行 Native 方法,PC 值为 undefined

底层实现:程序计数器是 CPU 寄存器的抽象,HotSpot 中直接使用线程的 PC 寄存器实现。线程切换时,操作系统保存/恢复寄存器状态。


Q6: Java 虚拟机栈的栈帧包含哪些部分?

答案: 每个方法调用时创建一个栈帧,包含:

部分作用
局部变量表 存储方法参数和局部变量(基本类型 + 引用)
操作数栈 字节码指令的操作数栈(方法执行的工作区)
动态链接 将符号引用转换为直接引用
方法返回地址 方法返回后继续执行的位置
附加信息 调试信息等

局部变量表的底层结构:

  • 以变量槽(Slot) 为单位,每个 Slot 32 位
  • long 和 double 占 2 个 Slot
  • 非静态方法的第 0 个 Slot 存储 this 引用
  • Slot 可复用(超出作用域的变量的 Slot 可被后续变量复用)

操作数栈的底层实现:

  • 操作数栈是一个栈结构,字节码指令从操作数栈中弹出操作数、执行运算、将结果压回
  • 例如 iadd 指令:弹出两个 int,相加,将结果压回

Q7: 方法区中存储什么内容?

答案: 方法区(JDK 8+ 为元空间)存储:

  • 类的元数据:类名、访问修饰符、父类、接口列表
  • 方法信息:方法名、参数类型、返回类型、字节码
  • 字段信息:字段名、类型、修饰符
  • 运行时常量池:字面量、符号引用
  • 静态变量:类的静态字段(JDK 7+ 移到堆中)
  • JIT 编译后的代码缓存:热点方法编译的机器码(CodeCache)
  • 方法表(Method Table):虚方法分派使用
  • JDK 7 vs JDK 8 的变化:

    • JDK 7:字符串常量池从永久代移到堆中
    • JDK 8:永久代被元空间取代,静态变量移到堆中
    • 元空间使用本地内存,不再受堆大小限制

    Q8: 字符串常量池在哪里?

    答案:

    • JDK 6 及之前:永久代(方法区) 中
    • JDK 7+:堆 中

    底层实现:

    • 字符串常量池本质是一个 StringTable,底层是哈希表
    • 每个字符串常量在表中有一个唯一的 String 对象
    • String.intern() 方法:如果常量池中已有相同内容的字符串,返回常量池中的引用;否则将当前字符串的引用放入常量池

    String s1 = "abc"; // 在常量池中创建
    String s2 = new String("abc"); // 在堆中创建新对象
    String s3 = s2.intern(); // 返回常量池中的引用
    System.out.println(s1 == s3); // true(同一个引用)


    Q9: String s = new String("abc") 创建了几个对象?

    答案:

    • 如果常量池中没有 "abc":创建 2 个对象(常量池中的 “abc” + 堆中的 new String)
    • 如果常量池中已有 "abc":创建 1 个对象(堆中的 new String)

    底层执行过程:

  • 检查字符串常量池中是否有 “abc”
  • 如果没有,在常量池中创建 “abc” 字符串对象
  • 在堆中创建一个新的 String 对象,内容为 “abc”
  • 栈中的引用 s 指向堆中的新对象

  • Q10: 直接内存(Direct Memory)是什么?

    答案: 直接内存不属于 JVM 运行时数据区,是通过 NIO 的 ByteBuffer.allocateDirect() 分配的堆外内存。

    底层原理:

    • Unsafe.allocateMemory() 直接调用 malloc() 分配本地内存
    • 不受 GC 直接管理,通过 Cleaner(虚引用)在 GC 时释放
    • 避免了堆内存和本地内存之间的数据复制,I/O 性能更高
    • Netty、NIO 大量使用直接内存

    OOM 场景:java.lang.OutOfMemoryError: Direct buffer memory——直接内存分配超过 -XX:MaxDirectMemorySize 限制。


    Q11: 强引用、软引用、弱引用、虚引用有什么区别?

    答案:

    引用类型GC 行为使用场景实现类
    强引用 不回收(只要引用存在) 普通对象引用 赋值操作 A a = new A()
    软引用 内存不足时回收 缓存(如图片缓存) SoftReference<T>
    弱引用 下次 GC 时一定回收 WeakHashMap 的键 WeakReference<T>
    虚引用 随时回收,无法通过虚引用获取对象 堆外内存管理、对象回收跟踪 PhantomReference<T>

    底层实现:

    • 所有引用类型都继承自 java.lang.ref.Reference
    • JVM 维护一个 Reference Handler 线程,负责处理引用队列
    • 软引用的回收时机:JVM 在抛出 OOM 之前,会清理所有软引用
    • 虚引用必须配合 ReferenceQueue 使用,当对象被 GC 后,虚引用被加入队列

    Q12: 内存泄漏和内存溢出有什么区别?

    答案:

    对比项内存泄漏(Memory Leak)内存溢出(OOM)
    定义 对象不再使用但仍被引用,无法被 GC 回收 内存不足,无法分配新对象
    结果 可用内存逐渐减少 直接抛出 OutOfMemoryError
    原因 代码缺陷(如静态集合未清理) 内存太小 / 对象太多 / 泄漏导致
    解决 找到并修复引用未释放的代码 增大内存 / 修复泄漏 / 优化代码

    常见内存泄漏场景:

  • 静态集合持有对象引用
  • 未关闭的资源(数据库连接、IO 流)
  • 监听器未注销
  • 内部类持有外部类引用
  • ThreadLocal 使用后未 remove

  • Q13: 堆溢出(OOM: Java heap space)的常见原因?

    答案:

  • 内存泄漏:对象无法被 GC 回收(如静态 Map 持续添加)
  • 大对象分配:一次加载超大文件到内存
  • 堆设置过小:-Xmx 设置太小
  • 频繁 Full GC 但无法释放足够空间
  • 排查步骤:

  • 添加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump.hprof
  • 使用 MAT 或 JVisualVM 分析堆转储
  • 查看 Dominator Tree 和 Leak Suspects Report

  • Q14: 栈溢出(StackOverflowError)的常见原因?

    答案:

  • 递归过深:无限递归或递归层级太深
  • 栈帧过大:方法中定义大量局部变量或大数组
  • -Xss 设置过小
  • 解决方案:

  • 检查递归终止条件
  • 将递归改为迭代
  • 适当增大 -Xss(但会减少可创建的线程数)

  • Q15: 元空间溢出(OOM: Metaspace)的常见原因?

    答案:

  • 动态生成类过多:CGLIB 代理、反射、Groovy 动态脚本
  • 类加载器泄漏:Web 容器反复部署/卸载应用
  • 大量 JSP 页面:每个 JSP 编译为一个类
  • 底层原因:

    • 元空间使用本地内存,由 Metaspace 的 ClassLoaderDataGraph 管理
    • 每个类加载器有独立的 Metaspace 分配
    • 类加载器未被回收 → 其加载的所有类都无法回收 → 元空间持续增长

    Q16: 什么是逃逸分析?

    答案: 逃逸分析是 JIT 编译器的一种优化技术,分析对象的作用域是否逃逸出方法或线程。

    逃逸类型:

    • 方法逃逸:对象被外部方法引用(如作为参数传递或返回值)
    • 线程逃逸:对象被其他线程访问

    基于逃逸分析的优化:

  • 栈上分配(Stack Allocation):不逃逸的对象直接在栈上分配,方法结束自动释放,不需要 GC
  • 标量替换(Scalar Replacement):将对象拆分为基本类型变量,直接在栈上存储
  • 同步消除(Lock Elision):不逃逸线程的对象,去掉同步锁
  • // 栈上分配示例
    public void method() {
    Point p = new Point(1, 2); // p 不逃逸出方法
    System.out.println(p.x + p.y);
    // JIT 优化后,p 直接在栈上分配,不需要堆分配和 GC
    }

    💡 要点:-XX:+DoEscapeAnalysis(默认开启)启用逃逸分析。栈上分配是最重要的优化之一,能显著减少 GC 压力。


    Q17: 什么是 TLAB(Thread Local Allocation Buffer)?

    答案: TLAB 是 JVM 为每个线程在 Eden 区中预先分配的一小块私有内存,用于加速对象分配。

    为什么需要 TLAB:

    • 堆是线程共享的,多线程同时分配对象需要加锁(CAS)
    • TLAB 是线程私有的,分配时不需要加锁,提高了分配效率

    底层实现:

    • 每个线程有一个 TLAB,大小由 -XX:TLABSize 控制
    • 对象优先在 TLAB 中分配
    • TLAB 用完后,从 Eden 区申请新的 TLAB
    • TLAB 中剩余空间不足时,使用指针碰撞(Bump the Pointer) 分配

    // 对象分配流程
    if (TLAB 有足够空间) {
    在 TLAB 中分配(无需加锁)
    } else if (对象是大对象) {
    直接在老年代分配
    } else {
    在 Eden 区分配(需要 CAS 加锁)
    }

    💡 要点:TLAB 默认开启(-XX:+UseTLAB),是 JVM 对象分配的第一选择。


    Q18: JVM 中对象的内存布局是怎样的?

    答案: 在 HotSpot 中,一个对象在内存中由三部分组成:

    ┌────────────────────────────────────┐
    │ 对象头(Header) │
    │ ├── Mark Word(8 字节) │
    │ │ ├── 哈希码(HashCode) │
    │ │ ├── GC 分代年龄(4 bit) │
    │ │ ├── 锁状态标志(2 bit) │
    │ │ ├── 线程持有的锁 │
    │ │ └── 偏向线程 ID │
    │ └── 类型指针(Klass Pointer,4/8B)│
    │ 指向方法区中的类元数据 │
    ├────────────────────────────────────┤
    │ 实例数据(Instance Data) │
    │ 存储对象的字段值 │
    ├────────────────────────────────────┤
    │ 对齐填充(Padding) │
    │ 凑齐 8 字节的整数倍 │
    └────────────────────────────────────┘

    Mark Word 的结构(64 位 JVM):

    锁状态Mark Word 内容标志位
    无锁 hashCode(31) + age(4) + biased(1) 01
    偏向锁 threadId(54) + epoch(2) + age(4) 01
    轻量级锁 指向栈中锁记录的指针(62) 00
    重量级锁 指向 Monitor 对象的指针(62) 10
    GC 标记 空(62) 11

    Q19: 对象的创建过程是怎样的?

    答案:

    Object obj = new Object();

    完整创建过程(底层 6 步):

  • 类加载检查:检查类是否已加载、解析、初始化。如果没有,先执行类加载
  • 分配内存:
    • 指针碰撞(Bump the Pointer):堆内存规整时使用(Serial、ParNew 收集器)
    • 空闲列表(Free List):堆内存不规整时使用(CMS 收集器)
    • 优先在 TLAB 中分配
  • 初始化零值:将分配的内存空间初始化为零值(int=0, boolean=false, 引用=null)
  • 设置对象头:设置 Mark Word(哈希码、GC 年龄、锁状态)和类型指针
  • 执行 <init> 方法:执行构造方法,按照程序员的意愿初始化对象
  • 栈上引用指向堆中对象

  • Q20: 对象的内存分配策略是怎样的?

    答案:

    对象类型分配位置条件
    大多数对象 Eden 区 TLAB 优先
    大对象 老年代 -XX:PretenureSizeThreshold 控制
    长期存活对象 老年代 年龄 ≥ -XX:MaxTenuringThreshold(默认 15)
    动态年龄判断 老年代 Survivor 中相同年龄对象总和 ≥ Survivor 空间 50%
    空间分配担保 老年代 Minor GC 后 Survivor 放不下

    大对象直接进入老年代的原因:

    • 避免在 Eden 和 Survivor 之间来回复制(开销大)
    • 大对象需要连续内存,新生代碎片化后可能分配失败

    动态年龄判断的底层实现:

    • GC 时,从年龄 1 开始累加 Survivor 中各年龄对象的大小
    • 当累加到某个年龄 N 时,总和 ≥ Survivor 空间的 50%
    • 则年龄 ≥ N 的对象直接晋升老年代

    Q21: 什么是对象的可达性分析?GC Roots 有哪些?

    答案: 可达性分析是 JVM 判断对象是否存活的算法。从 GC Roots 出发,沿引用链遍历,不可达的对象判定为可回收。

    GC Roots 包括:

  • 虚拟机栈中引用的对象(局部变量表中的引用)
  • 方法区中静态变量引用的对象(static Object obj)
  • 方法区中常量引用的对象(static final Object)
  • 本地方法栈中 JNI 引用的对象
  • JVM 内部引用(如 Class 对象、异常对象、系统类加载器)
  • 被同步锁(synchronized)持有的对象
  • 底层实现:

    • HotSpot 使用 OopMap(Ordinary Object Pointer Map) 记录栈帧中哪些位置存储了引用
    • GC 不需要遍历整个栈帧,直接查 OopMap 即可
    • 安全点(Safepoint):只有在安全点才会生成/更新 OopMap

    Q22: 什么是安全点(Safepoint)和安全区域(Safe Region)?

    答案:

    安全点:

    • 程序执行过程中,只有在特定位置才能暂停进行 GC
    • 安全点位置:方法调用、循环跳转、异常跳转等
    • GC 时,所有线程必须跑到最近的安全点才能暂停(主动式中断)

    安全区域:

    • 线程处于 Sleep 或 Blocked 状态时,无法主动跑到安全点
    • 安全区域是代码片段中引用关系不会发生变化的区域
    • 线程进入安全区域时设置标志,GC 时不再等待这些线程

    底层实现:

    • JVM 维护一个安全点计数器,线程到达安全点时递增
    • GC 线程设置安全点标志,等待所有线程到达安全点后开始 GC

    Q23: 什么是记忆集(Remembered Set)和卡表(Card Table)?

    答案: 记忆集和卡表用于解决跨代引用问题(老年代引用新生代对象)。

    问题:Minor GC 只回收新生代,但老年代可能持有新生代对象的引用。如果不处理跨代引用,会误回收存活对象。

    卡表(Card Table):

    • 一个字节数组,每个元素对应老年代的一块内存区域(512 字节,称为卡页)
    • 当老年代对象的引用字段被修改时,对应的卡页标记为脏页(dirty)
    • Minor GC 时,只扫描脏页对应的卡页,找到跨代引用

    记忆集(Remembered Set):

    • G1 中使用,每个 Region 维护一个记忆集
    • 记录其他 Region 中有哪些引用指向本 Region
    • 使用 Point-Out 方式(谁引用了我)

    写屏障(Write Barrier):

    • 在引用赋值操作前后插入屏障代码
    • 赋值时将对应的卡页标记为 dirty
    • 类似于 AOP 的切面编程

    Q24: 对象什么时候进入老年代?

    答案:

  • 年龄达到阈值:经过 -XX:MaxTenuringThreshold(默认 15)次 Minor GC 后晋升
  • 动态年龄判断:Survivor 中相同年龄对象总和 ≥ Survivor 空间 50%,该年龄及以上对象晋升
  • 大对象直接分配:超过 -XX:PretenureSizeThreshold 的大对象直接进入老年代
  • 空间分配担保:Minor GC 后 Survivor 放不下的对象直接进入老年代

  • Q25: 什么是空间分配担保?

    答案: Minor GC 前,JVM 检查老年代的可用空间是否大于新生代所有对象的总空间。

    担保流程:

  • 如果老年代可用空间 > 新生代所有对象总空间 → 安全执行 Minor GC
  • 如果不成立,检查 -XX:+HandlePromotionFailure 是否允许担保失败
  • 如果允许,检查老年代可用空间 > 历次晋升的平均大小
  • 如果大于平均值 → 冒险执行 Minor GC
  • 如果小于平均值 → 触发 Full GC
  • JDK 6 Update 24 之后:规则简化为只要老年代可用空间 > 新生代所有对象总空间,就执行 Minor GC;否则 Full GC。


    二、对象创建与内存分配

    Q26: 对象在堆中的分配方式有哪些?

    答案:

  • 指针碰撞(Bump the Pointer):

    • 堆内存是规整的(已用和空闲分开)
    • 分配时移动指针即可
    • 使用 Serial、ParNew 等带有压缩整理功能的收集器时使用
  • 空闲列表(Free List):

    • 堆内存不规整(已用和空闲交错)
    • 维护一个空闲列表,分配时找到足够大的空闲块
    • 使用 CMS 等不带压缩功能的收集器时使用
  • 并发安全:

    • 多线程同时分配需要同步,使用 CAS + 失败重试
    • 优先在 TLAB 中分配(无需加锁)

    Q27: 对象的生命周期是怎样的?

    答案:

    创建(new)→ 使用 → 不可达 → 被标记 → 被回收 → 内存释放

    详细流程:

  • 创建:new 关键字 → 类加载 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造方法
  • 使用:被引用、调用方法
  • 不可达:所有引用被置 null 或离开作用域
  • 第一次标记:可达性分析发现不可达
  • 第二次标记(finalize):如果重写了 finalize() 且未被调用过,放入 F-Queue
  • Finalizer 线程执行 finalize():如果在 finalize 中重新建立引用,对象复活
  • 第二次标记完成:如果仍然不可达,标记为可回收
  • GC 回收:标记-清除/复制/整理算法回收内存
  • 💡 要点:finalize() 只会被调用一次,且执行时间不确定,不推荐使用。Java 9+ 已标记为 @Deprecated。


    Q28: 对象头中的 Mark Word 有什么作用?

    答案: Mark Word 是对象头的核心部分(64 位 JVM 中占 8 字节),存储对象的运行时元数据。

    存储的信息:

    • 哈希码(HashCode):System.identityHashCode() 计算的值
    • GC 分代年龄:4 bit,最大值 15(所以 MaxTenuringThreshold 最大为 15)
    • 锁状态标志:标识当前锁的状态(无锁/偏向锁/轻量级锁/重量级锁)
    • 偏向线程 ID:偏向锁模式下记录持有锁的线程
    • Epoch:偏向锁的时间戳,用于批量撤销

    Mark Word 的复用:

    • 无锁状态:存储 hashCode + age
    • 偏向锁状态:存储 threadId + epoch + age(hashCode 被覆盖,需要时重新计算)
    • 轻量级锁:存储指向栈中 Lock Record 的指针
    • 重量级锁:存储指向 Monitor 对象的指针

    Q29: 什么是指针压缩(Compressed Oops)?

    答案: 指针压缩是 64 位 JVM 的优化技术,将 64 位的对象引用压缩为 32 位。

    原理:

    • 64 位引用占 8 字节,32 位引用占 4 字节
    • 压缩后的引用 = (实际地址 – 堆基地址) / 8
    • 解压 = 压缩值 × 8 + 堆基地址
    • 支持最大 32GB 的堆(2^32 × 8 = 32GB)

    开启条件:

    • -XX:+UseCompressedOops(默认开启,堆 < 32GB 时)
    • 堆 ≥ 32GB 时自动关闭

    💡 要点:堆大小在 4GB~32GB 之间时,指针压缩效果最好。超过 32GB 后引用膨胀为 8 字节,实际可用内存反而可能不如 32GB。


    Q30: 对象的访问定位方式有哪些?

    答案:

  • 句柄访问:

    • 堆中划分一块"句柄池"
    • 引用存储的是句柄地址,句柄中包含对象实例数据指针和类型数据指针
    • 优点:引用稳定(对象移动时只需改句柄,不改引用)
    • 缺点:两次指针定位,性能较低
  • 直接指针访问(HotSpot 使用):

    • 引用直接存储对象的堆地址
    • 对象头中包含类型指针,指向方法区中的类元数据
    • 优点:一次定位,性能更高
    • 缺点:对象移动时需要更新所有引用
  • 句柄访问: 引用 → 句柄池 → 对象实例数据
    → 类型数据(方法区)

    直接指针: 引用 → 对象实例数据 → 类型数据(方法区)


    Q31: 什么是对象的标量替换?

    答案: 标量替换是逃逸分析的优化之一。如果 JIT 发现对象不逃逸,会将对象拆解为基本类型变量,直接在栈上存储。

    // 原始代码
    public int method() {
    Point p = new Point(1, 2);
    return p.x + p.y;
    }

    // JIT 标量替换后(等价于)
    public int method() {
    int x = 1;
    int y = 2;
    return x + y;
    }

    💡 要点:标量替换消除了堆分配和 GC 开销,是最激进的栈上分配优化。


    Q32: 什么是偏向锁、轻量级锁、重量级锁?

    答案: 这是 JVM 对 synchronized 的锁升级优化:

    锁状态Mark Word适用场景性能
    无锁 hashCode + age 无竞争 最高
    偏向锁 threadId + epoch 只有一个线程访问 极高(无 CAS)
    轻量级锁 指向 Lock Record 少量竞争(自旋) 高
    重量级锁 指向 Monitor 激烈竞争(阻塞) 低

    锁升级过程:

    无锁 → 偏向锁(第一个线程访问)
    → 轻量级锁(第二个线程竞争,撤销偏向)
    → 重量级锁(自旋失败,膨胀为 Monitor)

    底层实现:

    • 偏向锁:在 Mark Word 中记录 threadId,后续该线程进入时无需 CAS
    • 轻量级锁:在栈帧中创建 Lock Record,将 Mark Word 复制到 Lock Record,CAS 将 Mark Word 指向 Lock Record
    • 重量级锁:膨胀为 Monitor 对象(ObjectMonitor),使用互斥量(Mutex)实现阻塞

    Q33: 什么是自旋锁?什么是自适应自旋?

    答案:

    自旋锁:

    • 轻量级锁竞争时,线程不立即阻塞,而是循环等待(自旋)
    • 避免了线程阻塞和唤醒的开销(用户态/内核态切换)
    • 适合锁持有时间短的场景

    自适应自旋:

    • JVM 根据历史数据自动调整自旋次数
    • 如果上次自旋成功获得锁 → 下次多自旋几次
    • 如果某个锁很少自旋成功 → 少自旋或直接阻塞

    参数:

    • -XX:+UseSpinning(JDK 6+ 默认开启)
    • -XX:PreBlockSpin=10(默认自旋 10 次)

    Q34: 对象的 hashCode 存在哪里?

    答案:

    • 未加锁时:hashCode 存储在 Mark Word 中(31 bit)
    • 加锁后:Mark Word 被锁信息覆盖,hashCode 被移出
      • 轻量级锁:hashCode 复制到 Lock Record 中
      • 重量级锁:hashCode 存储在 Monitor 对象中

    hashCode 的计算:

    • System.identityHashCode() 基于对象的内存地址计算(不重写 hashCode 时)
    • 重写 hashCode() 后,使用自定义逻辑

    Q35: 什么是内存对齐(Padding)?

    答案: HotSpot 要求对象的起始地址必须是 8 字节的整数倍。

    原因:

  • CPU 缓存行对齐:CPU 缓存以缓存行(Cache Line,通常 64 字节)为单位读取,8 字节对齐减少跨缓存行访问
  • 指针压缩:压缩指针以 8 字节为偏移单位
  • 性能:对齐后内存访问更快
  • 对象大小计算:

    对象大小 = 对象头(12 字节)+ 实例数据 + 对齐填充(补齐到 8 的倍数


    三、类加载机制

    Q36: 类的加载过程是怎样的?

    答案: 类的生命周期包含 7 个阶段:

    加载(Loading) → 验证(Verification) → 准备(Preparation) → 解析(Resolution)
    → 初始化(Initialization) → 使用(Using) → 卸载(Unloading)

    其中,验证、准备、解析合称为连接(Linking)。

    各阶段详解:

    阶段工作内容
    加载 通过类的全限定名获取字节码 → 将字节码转化为方法区的运行时数据结构 → 在堆中生成 Class 对象
    验证 文件格式验证、元数据验证、字节码验证、符号引用验证
    准备 为类变量(static)分配内存并设置零值(不是初始值)
    解析 将符号引用替换为直接引用(指针/偏移量)
    初始化 执行 <clinit>() 方法(类构造器),为类变量赋初始值

    准备阶段的细节:

    static int value = 123; // 准备阶段 value = 0,初始化阶段 value = 123
    static final int VALUE = 123; // 准备阶段 VALUE = 123(编译期常量,ConstantValue 属性)


    Q37: 类加载器有哪些?

    答案:

    类加载器加载范围实现
    Bootstrap ClassLoader JAVA_HOME/lib(rt.jar 等) C++ 实现(非 Java 类)
    Extension ClassLoader JAVA_HOME/lib/ext Java 实现(sun.misc.Launcher$ExtClassLoader)
    Application ClassLoader CLASSPATH 下的类 Java 实现(sun.misc.Launcher$AppClassLoader)
    自定义 ClassLoader 用户自定义路径 继承 java.lang.ClassLoader

    底层实现:

    • 每个类加载器都有一个 parent 字段,指向父加载器(不是继承关系,是组合关系)
    • Bootstrap ClassLoader 由 C++ 实现,Java 中无法直接引用,getParent() 返回 null

    Q38: 什么是双亲委派模型?

    答案: 当一个类加载器收到类加载请求时,首先委派给父加载器加载,只有父加载器无法加载时才自己加载。

    自定义 ClassLoader → Application ClassLoader → Extension ClassLoader → Bootstrap ClassLoader
    ← 找不到才向下传递 ←

    底层实现(ClassLoader.loadClass() 源码):

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
    // 1. 检查是否已加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
    try {
    // 2. 委派给父加载器
    if (parent != null) {
    c = parent.loadClass(name, false);
    } else {
    // 3. 父加载器为 null,使用 Bootstrap ClassLoader
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // 父加载器无法加载
    }
    if (c == null) {
    // 4. 父加载器无法加载,自己加载
    c = findClass(name);
    }
    }
    if (resolve) {
    resolveClass(c);
    }
    return c;
    }
    }


    Q39: 双亲委派模型的作用是什么?

    答案:

  • 避免类的重复加载:父加载器已加载的类,子加载器不会重复加载
  • 保证核心类的安全:防止用户自定义 java.lang.String 等核心类被篡改
  • 保证类的唯一性:同一个类只被同一个类加载器加载一次
  • 为什么核心类不能被篡改:

    • 用户定义的 java.lang.String 会委派给 Bootstrap ClassLoader
    • Bootstrap ClassLoader 会加载 rt.jar 中的官方 String
    • 用户的自定义 String 永远不会被加载

    Q40: 如何打破双亲委派模型?

    答案:

  • 重写 loadClass() 方法:不委派父加载器,直接自己加载(如 Tomcat)
  • 线程上下文类加载器(Thread Context ClassLoader):父加载器通过线程上下文加载器委托子加载器加载(如 SPI 机制)
  • OSGi 模块化:网状的类加载结构
  • SPI 机制打破双亲委派的原因:

    • JDBC 的 Driver 接口由 Bootstrap ClassLoader 加载(在 rt.jar 中)
    • 但 Driver 的实现类在 CLASSPATH 中,需要 Application ClassLoader 加载
    • 使用 Thread.currentThread().getContextClassLoader() 获取 Application ClassLoader 来加载

    Tomcat 打破双亲委派的原因:

    • 不同 Web 应用可能依赖不同版本的同一个库
    • 需要每个 Web 应用有独立的类加载器,互不影响
    • WebAppClassLoader 优先加载自己目录下的类,不委派父加载器

    Q41: 什么是类的初始化时机?

    答案: 类的初始化(执行 <clinit>())在以下情况下触发:

    主动引用(会触发初始化):

  • new 实例化对象
  • 读取/设置类的静态字段(final 常量除外)
  • 调用类的静态方法
  • 反射调用(Class.forName())
  • 初始化子类时,如果父类未初始化,先初始化父类
  • JVM 启动时的主类
  • 被动引用(不会触发初始化):

  • 子类引用父类的静态字段(只初始化父类)
  • 定义类的数组(MyClass[] arr = new MyClass[10])
  • 引用类的常量(static final,编译期已放入常量池)

  • Q42: <clinit>() 方法有什么特点?

    答案: <clinit>() 是编译器自动生成的类构造器方法,用于初始化类变量和静态代码块。

    特点:

  • 由编译器自动收集所有类变量的赋值动作和静态代码块合并而成
  • JVM 保证 <clinit>() 在多线程环境中被正确加锁同步(只会执行一次)
  • 如果 <clinit>() 执行缓慢,会造成死锁(其他线程一直等待)
  • 父类的 <clinit>() 先于子类执行
  • 接口的 <clinit>() 不会先执行父接口的
  • public class Test {
    static int a = 1; // 收集到 <clinit>()
    static {
    a = 2; // 收集到 <clinit>()
    System.out.println("static block");
    }
    // 编译器生成:
    // static {
    // a = 1;
    // a = 2;
    // System.out.println("static block");
    // }
    }


    Q43: 什么是类的卸载?什么条件下会卸载?

    答案: 类的卸载是指从方法区(元空间)中移除类的元数据。

    卸载条件(三个条件同时满足):

  • 该类的所有实例都已被 GC 回收
  • 加载该类的类加载器已被 GC 回收
  • 该类的 java.lang.Class 对象没有被引用
  • 实际场景:

    • 由 Bootstrap、Extension、Application ClassLoader 加载的类基本不会被卸载
    • 自定义类加载器加载的类可能被卸载(如 OSGi、热部署场景)

    Q44: 什么是字节码?Java 如何实现跨平台?

    答案: 字节码(Bytecode)是 Java 源代码编译后的中间代码,存储在 .class 文件中。

    跨平台原理:

    Java 源码 → javac 编译 → .class 字节码 → JVM(各平台不同)→ 机器码

    • 字节码是平台无关的,可以在任何 JVM 上运行
    • 每个平台有对应的 JVM 实现,负责将字节码转换为该平台的机器码

    字节码指令:

    • 由一个字节的操作码(opcode)+ 操作数组成
    • 共约 200 条指令,如 aload_0、invokevirtual、ireturn 等

    Q45: 类加载器的命名空间是什么?

    答案: 每个类加载器有自己的命名空间,由该加载器及其所有父加载器加载的类组成。

    规则:

    • 同一个命名空间中,类名必须唯一
    • 不同命名空间中,可以存在同名的类(不同实例)
    • 判断两个类是否相同:类名相同 + 同一个类加载器加载

    // 两个不同类加载器加载的同名类是不同的类
    ClassLoader cl1 = new MyClassLoader();
    ClassLoader cl2 = new MyClassLoader();
    Class<?> c1 = cl1.loadClass("com.example.MyClass");
    Class<?> c2 = cl2.loadClass("com.example.MyClass");
    System.out.println(c1 == c2); // false


    Q46: 什么是类的连接阶段中的验证?

    答案: 验证是连接的第一步,确保字节码的安全性和正确性。

    四种验证:

  • 文件格式验证:是否以 0xCAFEBABE 开头、版本号是否合法、常量池类型是否正确
  • 元数据验证:是否有父类、是否继承了 final 类、是否实现了所有抽象方法
  • 字节码验证:操作数栈的数据类型是否正确、跳转指令是否合法、类型转换是否安全
  • 符号引用验证:符号引用对应的类/方法/字段是否存在、是否有访问权限

  • Q47: 如何自定义类加载器?

    答案: 继承 java.lang.ClassLoader,重写 findClass() 方法:

    public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
    this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 1. 读取字节码文件
    byte[] data = loadClassData(name);
    // 2. 将字节码转换为 Class 对象
    return defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String name) {
    String path = classPath + "/" + name.replace('.', '/') + ".class";
    // 读取文件内容到 byte[]
    // …
    }
    }

    💡 要点:重写 findClass() 而非 loadClass(),这样仍然遵循双亲委派模型。如果要打破双亲委派,需要重写 loadClass()。


    Q48: 什么是运行时常量池?

    答案: 运行时常量池是方法区的一部分,在类加载的加载阶段,将 .class 文件中的常量池(Constant Pool)加载到内存中形成。

    常量池内容:

    • 字面量:字符串、整数、浮点数等常量
    • 符号引用:类名、方法名、字段名、描述符

    动态性:

    • 运行时常量池不要求编译期产生,运行时也可以将新的常量放入
    • 例如 String.intern() 方法

    Q49: 什么是方法句柄(Method Handle)?

    答案: 方法句柄(java.lang.invoke.MethodHandle)是 JDK 7 引入的轻量级方法调用机制,类似于 C 语言的函数指针。

    与反射的区别:

    • 反射基于 Java 层面的 java.lang.reflect.Method
    • 方法句柄直接指向底层方法,经过 JIT 优化后性能接近直接调用
    • 方法句柄支持参数适配和返回值适配

    Q50: Java 中的动态代理有哪几种实现方式?

    答案:

  • JDK 动态代理:

    • 基于接口,通过 java.lang.reflect.Proxy 实现
    • 运行时生成代理类($Proxy0 等),实现目标接口
    • 底层使用 ProxyGenerator 生成字节码
  • CGLIB 动态代理:

    • 基于继承,通过生成目标类的子类实现代理
    • 使用 ASM 字节码框架生成子类的字节码
    • 不能代理 final 类和 final 方法
  • 底层差异:

    • JDK 代理:代理类继承 Proxy,实现目标接口,通过 InvocationHandler 调用
    • CGLIB:代理类继承目标类,重写目标方法,通过 MethodInterceptor 调用

    四、垃圾回收算法

    Q51: 如何判断对象是否可以被回收?

    答案:

  • 引用计数法(JVM 不使用):

    • 每个对象维护一个引用计数器
    • 引用 +1,断开 -1,计数为 0 时回收
    • 问题:无法解决循环引用
  • 可达性分析算法(JVM 使用):

    • 从 GC Roots 出发,沿引用链遍历
    • 不可达的对象判定为可回收
    • 需要两次标记确认(finalize 可能复活对象)

  • Q52: 垃圾回收算法有哪些?

    答案:

    算法原理优点缺点使用场景
    标记-清除 标记存活→清除未标记 简单 碎片、效率低 CMS 老年代
    标记-复制 存活对象复制到新区域 无碎片、高效 空间浪费 50% 新生代
    标记-整理 标记→存活对象向一端压缩 无碎片、空间高效 移动成本高 老年代
    分代收集 按生命周期分区,不同区用不同算法 综合最优 实现复杂 所有主流收集器

    标记-复制的优化:

    • 新生代 Eden:S0:S1 = 8:1:1
    • 每次只使用 Eden + 一个 Survivor(90% 空间利用率)
    • 只浪费 10% 的空间

    Q53: 什么是分代收集理论?

    答案: 分代收集基于两个分代假说:

  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • 强分代假说:熬过越多次 GC 的对象越难以消亡
  • 实践:

    • 新生代:对象生命周期短,使用标记-复制算法,频繁回收
    • 老年代:对象生命周期长,使用标记-清除/整理算法,低频回收
    • 新生代中存活的对象经过多次 GC 后晋升到老年代

    跨代引用假说:

    • 跨代引用相对于同代引用是极少数的
    • 使用记忆集(Remembered Set) 解决跨代引用问题

    Q54: Minor GC、Major GC、Full GC 的区别?

    答案:

    GC 类型回收区域触发条件频率耗时
    Minor GC / Young GC 新生代 Eden 区满 频繁 短(毫秒级)
    Major GC / Old GC 老年代 老年代空间不足 较少 较长
    Full GC 整个堆 + 方法区 多种(见下方) 最少 最长(秒级)

    Full GC 的触发条件:

  • System.gc() 建议 JVM 执行 Full GC
  • 老年代空间不足
  • 方法区(元空间)不足
  • 空间分配担保失败
  • CMS 的并发模式失败(Concurrent Mode Failure)

  • Q55: 什么是可达性分析的两次标记?

    答案: 对象被判定为不可达后,需要经过两次标记才能确定回收:

    第一次标记:可达性分析发现对象不可达

    第二次标记:检查对象是否需要执行 finalize()

    • 如果对象没有重写 finalize() 或 finalize() 已被调用过 → 标记为可回收
    • 如果对象重写了 finalize() 且未被调用过 → 放入 F-Queue
    • Finalizer 线程异步执行 F-Queue 中对象的 finalize()
    • 如果在 finalize() 中重新建立引用(如 this 赋值给类变量)→ 对象复活
    • 复活的对象在第二次标记时被移出回收集合

    public class FinalizeExample {
    static FinalizeExample instance;

    @Override
    protected void finalize() throws Throwable {
    super.finalize();
    instance = this; // 复活!
    }

    public static void main(String[] args) throws Exception {
    instance = new FinalizeExample();
    instance = null;
    System.gc();
    Thread.sleep(500);
    System.out.println(instance); // 非 null(复活了)
    }
    }

    💡 要点:finalize() 只会被调用一次。如果复活后再次不可达,finalize 不会再被调用,直接回收。


    Q56: 什么是三色标记法?

    答案: 三色标记法是并发标记阶段使用的对象标记算法:

    颜色含义
    白色 尚未被扫描的对象(GC 结束后仍为白色的对象被回收)
    灰色 已被扫描,但其引用的对象尚未全部扫描

    | 黑色 | 已被扫描,且其引用的对象全部扫描完成

    标记过程:

  • 初始:所有对象为白色,GC Roots 为灰色
  • 从灰色集合中取出对象,扫描其引用
  • 将引用的对象标为灰色,自身标为黑色
  • 重复直到灰色集合为空
  • 剩余白色对象不可达,可以回收
  • 并发标记的问题:

    • 漏标(对象消失):黑色对象新增了对白色对象的引用,但灰色对象删除了对该白色对象的引用
    • 解决方案:写屏障 + 增量更新(CMS) 或 写屏障 + 原始快照(G1)

    Q57: 什么是写屏障(Write Barrier)?

    答案: 写屏障是在引用赋值操作前后插入的一段代码,类似于 AOP 的 Around 通知。

    作用:

    • 维护记忆集(Remembered Set / Card Table)
    • 实现增量更新或原始快照

    写屏障的类型:

    • 写前屏障(Pre-Write Barrier):赋值前执行(G1 的 SATB 使用)
    • 写后屏障(Post-Write Barrier):赋值后执行(CMS 的增量更新使用)

    底层实现:

    // 写后屏障的伪代码
    void post_write_barrier(Object* obj, Object* new_ref) {
    // 将卡页标记为 dirty
    card_table[card_index(obj)] = DIRTY;
    }


    Q58: 什么是安全点与安全区域的配合?

    答案: GC 需要暂停所有用户线程(STW),但线程不能在任意位置暂停(可能导致数据不一致)。

    安全点:

    • 代码中的特定位置(方法调用、循环跳转、异常跳转)
    • GC 时,线程运行到最近的安全点才能暂停
    • 使用主动式中断:设置安全点标志,线程轮询检查

    安全区域:

    • 线程处于 Sleep/Blocked 等状态时,无法主动跑到安全点
    • 安全区域是引用关系不会变化的代码段
    • 线程进入安全区域时设置标志,GC 不再等待

    Q59: 什么是 STW(Stop The World)?

    答案: STW 是 GC 暂停所有用户线程的行为。

    为什么需要 STW:

    • 可达性分析需要在一个一致性快照上进行
    • 如果不暂停,对象引用关系不断变化,分析结果不准确

    哪些阶段会 STW:

    • 初始标记(Initial Mark):标记 GC Roots 直接关联的对象(很短)
    • 重新标记(Remark):修正并发标记期间的变动
    • 复制/整理阶段:部分收集器需要 STW

    减少 STW 的方法:

    • 使用低延迟收集器(CMS、G1、ZGC)
    • 增量标记(将标记过程分多次执行)
    • 并发标记(与用户线程同时执行)

    Q60: 什么是记忆集(Remembered Set)?

    答案: 记忆集用于解决跨代引用问题。

    问题:Minor GC 只回收新生代,但老年代可能引用新生代对象。如果不知道哪些老年代对象引用了新生代对象,就需要扫描整个老年代。

    记忆集:

    • 记录"谁引用了我"(Point-Out 方式)
    • Minor GC 时,只扫描记忆集中的引用,不需要扫描整个老年代

    粒度:

    • 字长精度:记录精确到字长
    • 对象精度:记录精确到对象
    • 卡精度:记录精确到卡页(Card Page,512 字节)—— JVM 实际使用

    Q61: 什么是卡表(Card Table)?

    答案: 卡表是记忆集的一种实现方式,使用字节数数组实现。

    结构:

    Card Table: [0][1][0][1][0][0][1][0]…
    ↑ ↑ ↑
    dirty dirty

    • 每个元素(卡页)对应老年代的一块 512 字节的内存区域
    • 值为 1(dirty)表示该区域中有引用指向新生代
    • 值为 0(clean)表示没有跨代引用

    写屏障维护卡表:

    // 引用赋值时
    obj.field = new_ref;
    // 写后屏障自动执行
    card_table[card_index(obj)] = DIRTY;


    Q62: 什么是对象的晋升(Promotion)?

    答案: 对象从新生代晋升到老年代的过程。

    晋升条件:

  • 年龄达到阈值:经过 MaxTenuringThreshold 次 Minor GC 后
  • 动态年龄判断:Survivor 中同龄对象 ≥ Survivor 空间 50%
  • 大对象直接晋升:超过 PretenureSizeThreshold
  • 空间分配担保:Minor GC 后 Survivor 放不下
  • 动态年龄判断的底层实现:

    GC 后,计算 Survivor 中各年龄对象的总大小:
    age=1: 10MB
    age=2: 15MB
    age=3: 20MB
    Total = 10 + 15 = 25MB ≥ Survivor空间(32MB) × 50% = 16MB
    → age ≥ 2 的对象(15+20=35MB)全部晋升到老年代


    Q63: 什么是空间分配担保?

    答案: Minor GC 前,JVM 检查老年代是否有足够空间容纳新生代可能晋升的对象。

    流程:

  • 检查老年代可用空间 > 新生代所有对象总空间 → 安全
  • 检查老年代可用空间 > 历次晋升平均大小 → 冒险执行
  • 以上都不满足 → 触发 Full GC

  • Q64: 什么是引用计数法?为什么 JVM 不使用?

    答案: 引用计数法为每个对象维护一个引用计数器:

    • 引用 +1,断开引用 -1
    • 计数为 0 时回收

    为什么 JVM 不使用:

    • 无法解决循环引用:两个对象互相引用,计数永远不为 0
    • 需要额外的计数器空间
    • 每次引用变更都需要更新计数器

    // 循环引用示例
    Object a = new Object();
    Object b = new Object();
    a.ref = b;
    b.ref = a;
    a = null;
    b = null;
    // 引用计数法无法回收 a 和 b(互相引用)
    // 可达性分析可以回收(从 GC Roots 不可达)


    Q65: 什么是根节点枚举(Root Scanning)?

    答案: 根节点枚举是 GC 的第一步,找出所有 GC Roots。

    HotSpot 的优化:

    • 使用 OopMap 记录栈帧中哪些位置存储了引用
    • GC 时不需要遍历整个栈帧,直接查 OopMap
    • OopMap 在安全点生成/更新
    • 类加载时,也会记录静态字段的 OopMap

    OopMap 的作用:

    • 加速根节点枚举
    • 精确确定哪些位置是引用,哪些是基本类型
    • 避免误将基本类型当作引用

    五、垃圾回收器

    Q66: 有哪些垃圾回收器?

    答案:

    收集器区域算法特点参数
    Serial 新生代 复制 单线程、STW -XX:+UseSerialGC
    ParNew 新生代 复制 Serial 的多线程版 -XX:+UseParNewGC
    Parallel Scavenge 新生代 复制 吞吐量优先 -XX:+UseParallelGC
    Serial Old 老年代 标记-整理 单线程、STW —
    Parallel Old 老年代 标记-整理 多线程 -XX:+UseParallelOldGC
    CMS 老年代 标记-清除 低延迟 -XX:+UseConcMarkSweepGC
    G1 整堆 复制+标记-整理 Region 化、可控停顿 -XX:+UseG1GC
    ZGC 整堆 染色指针+读屏障 停顿 < 10ms -XX:+UseZGC
    Shenandoah 整堆 转发指针 停顿 < 10ms -XX:+UseShenandoahGC

    Q67: Serial 收集器的特点?

    答案:

    • 单线程执行 GC,需要完全 STW
    • 使用复制算法回收新生代
    • 简单高效,适合客户端模式或小内存场景
    • 是所有收集器中内存消耗最小的

    用户线程 → STW → 单线程 GC → 恢复用户线程


    Q68: ParNew 收集器的特点?

    答案:

    • Serial 的多线程版本
    • 使用复制算法回收新生代
    • 是唯一能与 CMS 配合的新生代收集器
    • 并行 GC,但仍然需要 STW

    用户线程 → STW → 多线程 GC → 恢复用户线程


    Q69: Parallel Scavenge 收集器的特点?

    答案:

    • 目标是达到可控的吞吐量(Throughput)
    • 吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 时间)
    • 使用复制算法回收新生代
    • 支持自适应调节策略(-XX:+UseAdaptiveSizePolicy)

    参数:

    • -XX:MaxGCPauseMillis:最大 GC 停顿时间(毫秒)
    • -XX:GCTimeRatio:吞吐量大小(默认 99,即 GC 时间 ≤ 1%)

    Q70: CMS 收集器的特点和工作流程?

    答案: CMS(Concurrent Mark Sweep)以最短停顿时间为目标。

    四个阶段:

    1. 初始标记(STW)—— 标记 GC Roots 直接关联的对象(很快)
    2. 并发标记 —— 与用户线程并发执行,遍历对象图(最耗时)
    3. 重新标记(STW)—— 修正并发标记期间的变动
    4. 并发清除 —— 与用户线程并发执行,清除未标记对象

    缺点:

  • CPU 敏感:并发阶段占用 CPU 资源
  • 内存碎片:使用标记-清除算法,产生碎片
  • 浮动垃圾:并发清除阶段新产生的垃圾本次 GC 无法回收
  • Concurrent Mode Failure:并发阶段老年代满了,退化为 Serial Old
  • 参数:

    • -XX:CMSInitiatingOccupancyFraction:触发 CMS 的老年代占用比例(默认 92%)
    • -XX:+UseCMSCompactAtFullCollection:Full GC 后压缩(默认开启)
    • -XX:CMSFullGCsBeforeCompaction:多少次 Full GC 后压缩一次

    Q71: G1 收集器的特点和工作流程?

    答案: G1(Garbage-First)是 JDK 9 的默认收集器,面向大内存和可控停顿场景。

    核心设计:

    • 将堆划分为大小相等的 Region(1MB~32MB)
    • 每个 Region 可以是 Eden、Survivor、Old 或 Humongous
    • 通过优先回收垃圾最多的 Region(Garbage-First)实现可控停顿

    工作流程:

    1. 初始标记(STW)—— 标记 GC Roots 直接关联的对象(借用 Minor GC 的 STW)
    2. 并发标记 —— 与用户线程并发执行
    3. 最终标记(STW)—— 处理并发标记遗留的 SATB 记录
    4. 筛选回收(STW)—— 根据停顿时间目标,选择回收价值最高的 Region

    Mixed GC:

    • 回收所有新生代 Region + 部分老年代 Region
    • 通过 -XX:MaxGCPauseMillis(默认 200ms)控制停顿时间

    Humongous 对象:

    • 超过 Region 50% 大小的对象
    • 直接分配在连续的 Humongous Region 中
    • 属于老年代的一部分

    Q72: CMS 和 G1 的区别?

    答案:

    对比项CMSG1
    算法 标记-清除 复制+标记-整理
    碎片 有碎片 无碎片(Region 内压缩)
    停顿 不可预测 可设定目标(MaxGCPauseMillis)
    内存 连续分代 Region 化
    适用 中小堆(< 8GB) 大堆(> 4GB)
    回收范围 老年代 整堆(Mixed GC)
    并发阶段 并发标记+并发清除 仅并发标记
    JDK JDK 9 标记废弃 JDK 9 默认

    Q73: ZGC 收集器的特点?

    答案: ZGC 是 JDK 11 引入的超低延迟收集器,目标停顿时间 < 10ms,且不随堆大小增长。

    核心技术:

  • 染色指针(Colored Pointer):

    • 64 位指针中,高 4 位用于存储标记信息(Marked0/Marked1/Remapped/Finalizable)
    • 对象的引用信息直接存储在指针中,不需要读取对象头
  • 读屏障(Load Barrier):

    • 访问对象时检查指针的标记位
    • 如果指针被标记,先执行修正操作再访问
  • 并发整理:

    • 标记、转移、重定位几乎完全并发执行
    • 几乎无 STW
  • ZGC 的工作流程:

    1. 并发标记(几乎无 STW)
    2. 并发预备重分配
    3. 并发重分配(转移存活对象)
    4. 并发重映射(修正引用)

    参数:

    • -XX:+UseZGC(JDK 11+)
    • -XX:ZCollectionInterval:GC 间隔时间
    • -XX:SoftMaxHeapSize:软上限堆大小

    Q74: Shenandoah 收集器的特点?

    答案: Shenandoah 是 Red Hat 开发的低延迟收集器(JDK 12+),与 ZGC 类似。

    核心技术:

    • 转发指针(Forwarding Pointer):每个对象头部有一个转发指针,指向对象的新位置
    • Brooks Pointer:对象移动时,更新转发指针,后续访问自动转发到新位置
    • 并发整理:几乎完全并发

    与 ZGC 的区别:

    • ZGC 使用染色指针,Shenandoah 使用转发指针
    • Shenandoah 有初始标记和最终标记两个短暂 STW
    • ZGC 不受堆大小限制,Shenandoah 在超大堆下有一定压力

    Q75: Epsilon 收集器是什么?

    答案: Epsilon 是 JDK 11 引入的**"无操作"收集器**,只负责内存分配,不负责回收。

    使用场景:

    • 性能测试(排除 GC 的影响)
    • 生命周期很短的应用
    • 一次性任务

    java -XX:+UseEpsilonGC -Xmx1g MyApp


    Q76: 什么是吞吐量优先和低延迟优先的收集器选择?

    答案:

    场景推荐收集器原因
    吞吐量优先(后台计算) Parallel Scavenge + Parallel Old 最大化代码执行时间占比
    低延迟优先(Web 服务) G1 或 ZGC 最小化 STW 时间
    超低延迟(金融交易) ZGC 或 Shenandoah STW < 10ms
    小内存(客户端) Serial 简单高效
    JDK 8 默认 Parallel Scavenge + Parallel Old —
    JDK 9+ 默认 G1 —

    Q77: G1 的 Region 大小如何选择?

    答案:

    • 通过 -XX:G1HeapRegionSize=n 设置(1MB~32MB,2 的幂)
    • 默认:堆大小 / 2048

    选择原则:

    • Region 太小:记忆集(RSet)占用太多内存
    • Region 太大:回收效率降低(每个 Region 的垃圾比例不精确)
    • 一般使用默认值即可,大堆可以适当增大

    Q78: 什么是 SATB(Snapshot At The Beginning)?

    答案: SATB 是 G1 在并发标记阶段使用的算法,保证并发标记的正确性。

    原理:

    • 并发标记开始时,创建一个逻辑快照
    • 并发标记期间,如果引用被删除,通过写前屏障将旧引用记录到 SATB 缓冲区
    • 最终标记阶段处理 SATB 缓冲区中的记录
    • 保证了并发标记期间不会漏标对象

    与 CMS 的增量更新对比:

    • SATB:记录被删除的引用(原始快照)
    • 增量更新:记录新增的引用

    Q79: 什么是 G1 的 Mixed GC?

    答案: Mixed GC 是 G1 特有的回收方式,同时回收:

    • 所有新生代 Region(Eden + Survivor)
    • 部分老年代 Region(选择回收价值最高的)

    触发条件:

    • 老年代占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%)
    • 触发并发标记
    • 并发标记完成后,触发 Mixed GC

    回收过程:

    1. 初始标记(STW,借用 Minor GC)
    2. 并发标记
    3. 最终标记(STW)
    4. 筛选回收(STW)—— 选择回收价值最高的 Region


    Q80: G1 的停顿时间模型是什么?

    答案: G1 通过停顿预测模型(Pause Prediction Model)实现可控停顿。

    原理:

  • G1 维护每个 Region 的回收价值(垃圾量 / 回收耗时)
  • 根据 -XX:MaxGCPauseMillis 的目标停顿时间
  • 选择回收价值最高的 Region 集合,使得总耗时不超过目标
  • 为什么叫 Garbage-First:

    • 优先回收垃圾最多的 Region
    • 最大化每次 GC 的回收效率

    Q81: 什么是记忆集(RSet)在 G1 中的实现?

    答案: G1 的每个 Region 维护一个 Remembered Set(RSet),记录其他 Region 中有哪些引用指向本 Region。

    RSet 的结构:

    • 使用 Per-Region Table 实现
    • 每个 Card 对应一个 bit,标记其他 Region 的卡页是否 dirty
    • 使用哈希表存储,key 是引用来源的 Region,value 是 dirty card 列表

    RSet 的维护:

    • 引用赋值时,写后屏障更新 RSet
    • RSet 占用额外内存(约 5%~10% 的堆)

    Q82: 什么是写屏障的 SATB 队列?

    答案: G1 的写前屏障将删除的引用记录到 SATB 队列中。

    结构:

    • 每个线程有一个 SATB 队列(Thread Local Buffer)
    • 并发标记期间,引用被删除时,旧引用被放入队列
    • 队列满后,放入全局 SATB 队列
    • 最终标记阶段处理所有 SATB 队列

    Q83: ZGC 的染色指针如何工作?

    答案: ZGC 将对象引用的 64 位指针中的高 4 位用于存储元数据:

    64位指针:[4位标记信息][42位对象地址][未使用]
    Marked0/1, Remapped, Finalizable

    读屏障:

    • 访问对象时,先检查指针的标记位
    • 如果标记位不为期望值,先执行自愈(self-healing) 操作
    • 自愈:将指针的标记位修正为当前正确的值

    并发转移的原理:

  • 对象被转移后,旧地址的转发指针指向新地址
  • 访问旧地址时,读屏障检测到标记位变化
  • 自动修正为新地址,访问新位置的对象

  • Q84: 什么是 GC 的并发标记与用户线程的竞争?

    答案: 并发标记阶段,GC 线程和用户线程同时执行,可能导致:

  • 漏标:用户线程修改了引用关系,导致 GC 误认为对象不可达
  • 多标:用户线程删除了引用,导致 GC 误认为对象仍可达
  • 解决方案:

    • 增量更新(Incremental Update):记录新增的引用(CMS 使用)
    • 原始快照(SATB):记录删除的引用(G1 使用)

    Q85: 如何选择垃圾收集器?

    答案:

    堆 < 100MB → Serial
    堆 < 4GB,吞吐量优先 → Parallel
    堆 < 8GB,低延迟 → CMS(JDK 8)/ G1(JDK 9+)
    堆 > 4GB,可控停顿 → G1
    堆 > 8GB,超低延迟 → ZGC(JDK 11+)

    JDK 版本考虑:

    • JDK 8:Parallel Scavenge + Parallel Old(默认)或 G1
    • JDK 9~16:G1(默认)
    • JDK 17+:G1(默认),可选 ZGC

    六、JIT 编译与执行引擎

    Q86: Java 是解释执行还是编译执行?

    答案: Java 是解释执行 + 编译执行的混合模式。

    执行流程:

    Java 源码 → javac 编译 → 字节码(.class)
    → JVM 启动时:解释器逐条解释执行
    → 热点代码:JIT 编译器编译为本地机器码
    → 后续执行:直接运行机器码

    为什么需要混合模式:

    • 解释器:启动快,但执行慢
    • JIT 编译器:启动慢(需要编译),但执行快
    • 混合模式:启动时用解释器,热点代码用 JIT

    Q87: 什么是 JIT 编译器?

    答案: JIT(Just-In-Time)编译器在运行时将热点代码编译为本地机器码。

    HotSpot 中有两个 JIT 编译器:

  • C1(Client Compiler):

    • 编译速度快,优化少
    • 适合客户端应用
    • 参数:-client
  • C2(Server Compiler):

    • 编译速度慢,优化多
    • 适合服务器应用
    • 参数:-server
  • 分层编译(Tiered Compilation):

    • JDK 8 默认开启(-XX:+TieredCompilation)
    • Level 0:解释执行
    • Level 1:C1 编译,无性能分析
    • Level 2:C1 编译,有限性能分析
    • Level 3:C1 编译,完整性能分析
    • Level 4:C2 编译,完全优化

    Q88: 什么是热点代码?如何检测?

    答案: 热点代码是被频繁执行的代码段。

    检测方法:

  • 基于采样:定时采样线程的 PC 寄存器,统计哪些方法/循环被频繁执行
  • 基于计数器:维护方法调用计数器和回边计数器
  • HotSpot 的计数器:

    • 方法调用计数器:方法被调用的次数
      • 阈值:-XX:CompileThreshold(Client 1500,Server 10000)
    • 回边计数器:循环体执行的次数
      • 阈值:-XX:OnStackReplacePercentage

    热度衰减:

    • 方法调用计数器在半衰期内未达到阈值,计数减半
    • 由 -XX:-UseCounterDecay 控制是否开启

    Q89: JIT 编译器有哪些优化技术?

    答案:

    优化技术说明
    方法内联 将被调用方法的代码直接嵌入调用处
    逃逸分析 分析对象是否逃逸出方法/线程
    标量替换 将对象拆分为基本类型变量
    栈上分配 不逃逸的对象在栈上分配
    同步消除 不逃逸线程的对象去除同步锁
    循环展开 减少循环跳转次数
    空值检查消除 去除不必要的 null 检查
    公共子表达式消除 复用已计算的表达式
    内联缓存 缓存虚方法的调用目标

    方法内联的条件:

    • 方法体 ≤ 35 字节(C2 默认)自动内联
    • 方法体 ≤ 325 字节(C2)且调用频繁时内联
    • -XX:MaxInlineSize 控制内联阈值

    Q90: 什么是方法内联(Method Inlining)?

    答案: 方法内联是 JIT 最重要的优化,将被调用方法的代码直接嵌入调用处,消除方法调用开销。

    // 原始代码
    public int add(int a, int b) { return a + b; }
    public int compute() { return add(1, 2) * 3; }

    // 内联后
    public int compute() { return (1 + 2) * 3; }

    为什么内联重要:

    • 消除方法调用的开销(栈帧创建/销毁、参数传递)
    • 为后续优化创造条件(如常量折叠、逃逸分析)

    内联的限制:

    • 虚方法需要类型推断确认具体调用目标
    • 如果无法确定目标,使用内联缓存(Inline Cache)

    Q91: 什么是 AOT 编译?

    答案: AOT(Ahead-Of-Time)编译是在程序运行前将字节码编译为本地机器码。

    GraalVM Native Image:

    • 使用 Graal 编译器在构建时编译
    • 生成独立的可执行文件,不需要 JVM
    • 启动速度极快,内存占用小

    AOT vs JIT:

    对比项AOTJIT
    编译时机 运行前 运行时
    启动速度 极快 较慢
    峰值性能 较低(无法做运行时优化) 更高(可做运行时优化)
    编译时间 极长(分钟级) 短(毫秒级)

    Q92: 什么是逃逸分析的标量替换?

    答案: 标量替换是逃逸分析的优化之一。如果 JIT 发现对象不逃逸,将对象拆解为基本类型变量。

    // 原始代码
    public int method() {
    Point p = new Point(1, 2); // p 不逃逸
    return p.x + p.y;
    }

    // JIT 标量替换后
    public int method() {
    int x = 1;
    int y = 2;
    return x + y;
    }

    💡 要点:标量替换消除了堆分配和 GC 开销。-XX:+EliminateAllocations(默认开启)。


    Q93: 什么是即时编译的逆优化(Deoptimization)?

    答案: 逆优化是 JIT 编译代码被废弃,回退到解释执行的过程。

    触发条件:

  • 类被卸载:编译的代码依赖的类被 GC 回收
  • 类型推断失败:内联的虚方法的类型假设被打破
  • 代码缓存不足:CodeCache 满了,需要清理旧代码
  • 调试需要:断点调试时需要回到解释执行

  • Q94: 什么是 CodeCache?

    答案: CodeCache 是 JVM 存储JIT 编译后的本地机器码的区域。

    底层实现:

    • 使用本地内存(Native Memory)
    • 默认大小:240MB(JDK 8)或 240MB(JDK 11+)
    • 由 -XX:ReservedCodeCacheSize 控制

    CodeCache 满了会怎样:

    • JIT 编译停止,所有代码使用解释执行
    • 性能显著下降
    • JVM 日志中会出现 CodeCache is full 警告

    # 监控 CodeCache
    jcmd <pid> Compiler.codecache


    Q95: 什么是 GraalVM?

    答案: GraalVM 是 Oracle 开发的高性能多语言虚拟机。

    核心特性:

  • Graal 编译器:新一代 JIT 编译器,优化能力更强
  • Native Image:AOT 编译,生成独立可执行文件
  • 多语言支持:Java、JavaScript、Python、Ruby、R 等
  • Polyglot API:不同语言之间无缝互调
  • Graal 编译器的优势:

    • 更激进的内联优化
    • 更好的逃逸分析
    • 支持部分求值(Partial Evaluation)

    七、JVM 参数与调优

    Q96: 常用的 JVM 内存参数有哪些?

    答案:

    参数作用默认值建议
    -Xms 初始堆大小 物理内存的 1/64 设为与 -Xmx 相同
    -Xmx 最大堆大小 物理内存的 1/4 根据应用需要设置
    -Xmn 新生代大小 堆的 1/3 堆的 1/3~1/2
    -Xss 线程栈大小 1MB 一般不改
    -XX:MetaspaceSize 元空间初始大小 21MB 设为 256MB
    -XX:MaxMetaspaceSize 元空间最大大小 无限制 设为 512MB
    -XX:MaxDirectMemorySize 直接内存最大大小 -Xmx 根据 NIO 使用量设置

    Q97: 如何调优新生代?

    答案: 原则:

    • 新生代太小 → 频繁 Minor GC
    • 新生代太大 → Minor GC 耗时长,老年代空间不足

    参数:

    -Xmn2g # 新生代 2GB
    -XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
    -XX:MaxTenuringThreshold=15 # 最大晋升年龄
    -XX:TargetSurvivorRatio=90 # Survivor 目标使用率
    -XX:+UseAdaptiveSizePolicy # 自适应调节(Parallel Scavenge)

    调优步骤:

  • 观察 GC 日志,分析 Minor GC 频率和耗时
  • 如果 Minor GC 过于频繁 → 增大新生代
  • 如果 Minor GC 耗时过长 → 减小新生代
  • 如果对象过早晋升 → 增大 Survivor 或 MaxTenuringThreshold

  • Q98: 如何调优老年代?

    答案: 原则:

    • 老年代太小 → 频繁 Full GC
    • 老年代太大 → Full GC 耗时长

    参数:

    -XX:NewRatio=2 # 老年代:新生代 = 2:1
    -XX:CMSInitiatingOccupancyFraction=75 # CMS 触发阈值
    -XX:G1MixedGCCountTarget=8 # G1 Mixed GC 次数目标


    Q99: 如何开启和分析 GC 日志?

    答案: JDK 8 GC 日志参数:

    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:/path/to/gc.log
    -XX:+UseGCLogFileRotation
    -XX:NumberOfGCLogFiles=5
    -XX:GCLogFileSize=20M

    JDK 9+ 统一日志格式:

    -Xlog:gc*:file=/path/to/gc.log:time,tags,level:filecount=5,filesize=20M

    GC 日志示例解读:

    [GC (Allocation Failure)
    [PSYoungGen: 204800K->25600K(230400K)]
    204800K->128000K(757760K),
    0.0123456 secs]

    • Allocation Failure:触发原因
    • PSYoungGen:Parallel Scavenge 新生代
    • 204800K->25600K:GC 前后新生代占用
    • 230400K:新生代总大小
    • 204800K->128000K:GC 前后堆占用
    • 757760K:堆总大小
    • 0.0123456 secs:GC 耗时

    Q100: 什么是 GC 停顿时间?如何优化?

    答案: GC 停顿时间(STW)是 GC 暂停所有用户线程的时间。

    优化方法:

  • 选择低延迟收集器:G1、ZGC、Shenandoah
  • 调整堆大小:堆太大 → GC 耗时长;堆太小 → 频繁 GC
  • 调整新生代大小:新生代大 → Minor GC 耗时长但频率低
  • 减少对象分配:优化代码,减少临时对象
  • G1 停顿目标:-XX:MaxGCPauseMillis=200
  • ZGC:几乎无 STW

  • Q101: 如何设置 JVM 参数?

    答案:

    # 基本参数
    java -Xms4g -Xmx4g -Xmn2g -Xss512k -jar app.jar

    # GC 参数
    java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar

    # GC 日志
    java -XX:+PrintGCDetails -Xloggc:gc.log -jar app.jar

    # OOM 自动 Dump
    java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./dump.hprof -jar app.jar

    # 查看当前 JVM 参数
    jinfo -flags <pid>


    Q102: 什么是内存溢出自动 Dump?

    答案:

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/path/to/dump.hprof
    -XX:OnOutOfMemoryError="kill -9 %p" # OOM 后执行的命令

    JVM 在抛出 OOM 时自动生成堆转储文件,可以用 MAT 或 JVisualVM 分析。


    Q103: 什么是字符串去重(String Deduplication)?

    答案: G1 收集器的特性(JDK 8u20+),自动对堆中内容相同的 String 对象进行去重。

    原理:

    • G1 在并发标记阶段识别重复的 String
    • 将重复 String 的 char[] 数组指向同一个
    • 节省内存

    参数:

    -XX:+UseStringDeduplication # 开启字符串去重
    -XX:StringDeduplicationAgeThreshold=3 # 只去重存活 3 次 GC 的 String


    Q104: 什么是大页(Huge Pages)?

    答案: 操作系统默认内存页大小 4KB,大页使用 2MB 或 1GB 的页。

    优势:

    • 减少 TLB(Translation Lookaside Buffer)缺失
    • 减少页表项数量
    • 提高内存访问性能

    JVM 参数:

    -XX:+UseLargePages # 启用大页
    -XX:LargePageSizeInBytes=2m # 大页大小

    前提:操作系统需要配置大页支持(Linux: vm.nr_hugepages)。


    Q105: 如何判断堆内存是否设置合理?

    答案:

  • GC 日志分析:

    • Full GC 频率 < 1 天一次 → 合理
    • Full GC 后老年代使用率 < 70% → 合理
    • Minor GC 频率合理(几秒到几十秒一次)
  • 指标:

    • GC 时间占比 < 5% → 合理
    • Full GC 停顿时间可接受
    • 堆使用率峰值 < 80%
  • 不合理的信号:

    • Full GC 频繁(每分钟多次)
    • Full GC 后堆使用率仍然很高
    • GC 停顿时间过长(> 1 秒)

  • Q106: 什么是自适应调节策略(AdaptiveSizePolicy)?

    答案: Parallel Scavenge 收集器的特性,JVM 自动调整堆大小、新生代比例、晋升年龄等参数。

    原理:

    • 根据 GC 的实际表现(停顿时间、吞吐量)
    • 自动调整 -XX:SurvivorRatio、-XX:MaxTenuringThreshold 等
    • 以达到 -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio 的目标

    参数:-XX:+UseAdaptiveSizePolicy(Parallel Scavenge 默认开启)


    Q107: 如何排查 CPU 飙高问题?

    答案:

  • 找到 CPU 高的线程:

    top -Hp <pid> # 找到 CPU 高的线程 ID
    printf '%x' <tid> # 转换为 16 进制
    jstack <pid> | grep <tid_hex> # 查看线程栈

  • 常见原因:

    • Full GC 频繁(GC 线程占 CPU)
    • 死循环
    • 死锁
    • 正则表达式回溯

  • Q108: 如何排查内存飙高问题?

    答案:

  • 查看堆内存使用:

    jmap -heap <pid>
    jstat -gcutil <pid> 1000

  • 生成堆转储:

    jmap -dump:format=b,file=dump.hprof <pid>

  • 分析堆转储:

    • MAT:Dominator Tree、Leak Suspects
    • JVisualVM:类实例数、内存占用

  • Q109: 如何设置一个合理的 JVM 参数模板?

    答案:

    # 4GB 堆,G1 收集器
    java \\
    -Xms4g \\
    -Xmx4g \\
    -XX:+UseG1GC \\
    -XX:MaxGCPauseMillis=200 \\
    -XX:MetaspaceSize=256m \\
    -XX:MaxMetaspaceSize=512m \\
    -XX:MaxDirectMemorySize=1g \\
    -XX:+HeapDumpOnOutOfMemoryError \\
    -XX:HeapDumpPath=/opt/logs/heapdump.hprof \\
    -XX:+PrintGCDetails \\
    -XX:+PrintGCDateStamps \\
    -Xloggc:/opt/logs/gc.log \\
    -XX:+UseGCLogFileRotation \\
    -XX:NumberOfGCLogFiles=10 \\
    -XX:GCLogFileSize=50M \\
    -jar app.jar


    Q110: 什么是堆外内存?如何监控?

    答案: 堆外内存(Off-Heap Memory)包括:

    • 直接内存:NIO ByteBuffer
    • JNI 代码:Native 代码分配的内存
    • JVM 内部:GC 数据结构、JIT 代码缓存

    监控方法:

    # 查看 NIO 直接内存
    jcmd <pid> VM.native_memory summary

    # 开启 NMT(Native Memory Tracking)
    -XX:NativeMemoryTracking=summary


    八、内存泄漏与溢出排查

    Q111: 常见的内存泄漏场景有哪些?

    答案:

  • 静态集合持有对象:static List<Object> list 持续添加不清理
  • 未关闭的资源:数据库连接、IO 流、Socket
  • 监听器未注销:事件监听器、观察者模式
  • 内部类持有外部类引用:非静态内部类、匿名内部类
  • ThreadLocal 未 remove:线程池中 ThreadLocal 不清理
  • 缓存无过期策略:HashMap 做缓存,无淘汰机制
  • 自定义类加载器泄漏:类加载器未被 GC 回收

  • Q112: ThreadLocal 的内存泄漏问题?

    答案: ThreadLocal 使用 ThreadLocalMap 存储数据,key 是弱引用,value 是强引用。

    泄漏原因:

  • ThreadLocal 对象被 GC 回收(key 是弱引用)
  • 但 value 仍然被 Entry 强引用
  • 如果线程是线程池中的核心线程,不会销毁
  • value 永远无法被回收
  • 解决方案:

    try {
    threadLocal.set(value);
    // 使用 value
    } finally {
    threadLocal.remove(); // 必须清理!
    }


    Q113: 如何使用 MAT 分析堆转储?

    答案:

  • 打开 dump.hprof 文件
  • Leak Suspects Report:自动分析可能的泄漏点
  • Dominator Tree:查看占用内存最大的对象
  • Histogram:查看各类的实例数和内存占用
  • GC Roots → Path to GC Roots:找到对象的引用链
  • OQL:使用对象查询语言查找特定对象

  • Q114: 如何使用 jstat 监控 GC?

    答案:

    # 每 1 秒输出一次 GC 统计
    jstat -gcutil <pid> 1000

    # 输出示例
    # S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    # 0.00 50.00 30.00 45.00 95.00 92.00 100 0.500 5 0.200 0.700

    字段含义:

    • S0/S1:Survivor 使用率
    • E:Eden 使用率
    • O:老年代使用率
    • M:元空间使用率
    • YGC/YGCT:Young GC 次数/耗时
    • FGC/FGCT:Full GC 次数/耗时
    • GCT:总 GC 耗时

    Q115: 如何使用 jmap 生成堆转储?

    答案:

    # 生成堆转储
    jmap -dump:format=b,file=dump.hprof <pid>

    # 只 dump 存活对象(触发 Full GC)
    jmap -dump:live,format=b,file=dump.hprof <pid>

    # 查看堆使用情况
    jmap -heap <pid>

    # 查看对象实例统计
    jmap -histo <pid> | head -20


    Q116: 如何使用 jstack 分析线程状态?

    答案:

    # 输出线程栈
    jstack <pid>

    # 检测死锁
    jstack -l <pid>

    # 输出到文件
    jstack <pid> > thread_dump.txt

    线程状态:

    • RUNNABLE:正在运行或等待 CPU
    • BLOCKED:等待获取锁
    • WAITING:无限等待(Object.wait())
    • TIMED_WAITING:有限等待(Thread.sleep())
    • TERMINATED:已结束

    Q117: 什么是 Java Flight Recorder(JFR)?

    答案: JFR 是 JDK 内置的低开销性能监控工具,记录 JVM 运行时事件。

    事件类型:

    • GC 事件
    • 线程事件
    • I/O 事件
    • 方法采样
    • 内存分配

    使用方法:

    # 开始录制
    jcmd <pid> JFR.start duration=60s filename=recording.jfr

    # 或使用参数启动
    -XX:StartFlightRecording=duration=60s,filename=recording.jfr

    分析工具:JDK Mission Control(JMC)


    Q118: 如何排查 Full GC 频繁的问题?

    答案:

  • 查看 GC 日志:分析 Full GC 的频率、耗时、前后内存变化
  • 分析堆转储:查看哪些对象占用最多内存
  • 常见原因:
    • 老年代空间不足 → 增大堆或优化代码
    • 内存泄漏 → 修复泄漏
    • 元空间不足 → 增大 MetaspaceSize
    • System.gc() 调用 → 禁用 -XX:+DisableExplicitGC
  • 解决方案:
    • 增大堆内存
    • 优化代码减少对象分配
    • 选择合适的收集器
    • 调整新生代/老年代比例

  • Q119: 如何排查 OOM 问题?

    答案: 步骤:

  • 保留现场:-XX:+HeapDumpOnOutOfMemoryError
  • 分析堆转储:
    • MAT → Leak Suspects → Dominator Tree
    • 找到占用内存最多的对象
    • 追踪 GC Roots 引用链
  • 定位代码:找到创建/持有这些对象的代码
  • 修复:
    • 内存泄漏:修复引用未释放的代码
    • 内存不足:增大堆 / 优化代码 / 分批处理

  • Q120: 什么是 Metaspace 的类加载器泄漏?

    答案: 类加载器泄漏是指类加载器无法被 GC 回收,导致其加载的所有类都无法卸载。

    常见场景:

    • Web 容器(Tomcat)反复部署/卸载应用
    • 每次部署创建新的类加载器,旧的类加载器未被回收
    • 某些静态引用持有旧类加载器的引用

    排查:

    jmap -dump:live,format=b,file=dump.hprof <pid>
    # MAT 中查看 ClassLoader 的实例数和 GC Roots


    Q121: 什么是直接内存泄漏?

    答案: 直接内存(NIO ByteBuffer)不受 GC 直接管理,如果未正确释放会导致泄漏。

    泄漏原因:

    • ByteBuffer.allocateDirect() 分配的内存未释放
    • Cleaner 未被触发(GC 不及时)
    • Netty 的 ByteBuf 未 release

    排查:

    # 监控直接内存
    -XX:NativeMemoryTracking=summary
    jcmd <pid> VM.native_memory summary

    # 查看 NIO 直接内存使用
    jcmd <pid> VM.info


    Q122: 什么是堆的碎片化?

    答案: 堆碎片化是指堆内存中有大量不连续的小空闲块,无法分配大对象。

    产生原因:

    • 标记-清除算法不会压缩内存
    • CMS 收集器使用标记-清除,容易产生碎片

    解决:

    • 使用 G1(Region 化,无碎片)
    • CMS 的压缩参数:-XX:+UseCMSCompactAtFullCollection
    • 升级到 JDK 9+,默认使用 G1

    Q123: 如何分析 GC 日志中的 Full GC 原因?

    答案: GC 日志中 Full GC 前面的关键词表示触发原因:

    关键词原因
    Metadata GC Threshold 元空间达到阈值
    Ergonomics JVM 自适应调节
    Allocation Failure 分配失败
    System.gc() 代码调用 System.gc()
    Heap Inspection Initiated GC jmap -histo 触发
    Concurrent Mode Failure CMS 并发模式失败
    Promotion Failed 晋升失败
    G1 Humongous Allocation G1 大对象分配

    Q124: 如何监控 JVM 的线程数?

    答案:

    # 查看线程数
    jstack <pid> | grep 'java.lang.Thread.State' | wc -l

    # 查看线程状态分布
    jstack <pid> | grep 'java.lang.Thread.State' | sort | uniq -c

    # JMX 监控
    jconsole <pid>
    # Thread 标签页查看线程数和状态


    Q125: 什么是堆的监控指标?

    答案:

    指标说明工具
    堆使用率 已用/总大小 jstat, JMX
    GC 频率 每分钟 GC 次数 GC 日志
    GC 停顿时间 每次 GC 的 STW 时间 GC 日志
    Full GC 频率 每天 Full GC 次数 GC 日志
    对象分配速率 每秒分配的字节数 JFR
    晋升速率 每秒晋升到老年代的字节数 GC 日志
    元空间使用率 已用/总大小 jstat

    九、字节码与执行引擎深入

    Q126: 字节码指令有哪些类别?

    答案:

    类别示例作用
    加载/存储 iload, istore, aload, astore 局部变量表 ↔ 操作数栈
    运算 iadd, isub, imul, idiv 算术运算
    类型转换 i2l, i2f, l2d 基本类型转换
    对象创建 new, newarray, anewarray 创建对象/数组
    方法调用 invokevirtual, invokeinterface, invokespecial, invokestatic, invokedynamic 调用方法
    控制转移 ifeq, ifne, goto 条件/无条件跳转
    栈操作 pop, dup, swap 操作数栈操作
    同步 monitorenter, monitorexit 获取/释放监视器锁

    Q127: 方法调用的 5 条字节码指令有什么区别?

    答案:

    指令用途绑定时机
    invokevirtual 调用虚方法(普通实例方法) 运行时多态
    invokeinterface 调用接口方法 运行时多态
    invokespecial 调用构造方法、私有方法、父类方法 编译期确定
    invokestatic 调用静态方法 编译期确定
    invokedynamic 动态调用(Lambda、方法句柄) 运行时动态绑定

    invokedynamic:

    • JDK 7 引入,支持动态语言
    • JDK 8 Lambda 表达式使用此指令
    • 运行时通过 BootstrapMethod 确定调用目标

    Q128: 什么是栈帧中的动态链接?

    答案: 动态链接将符号引用转换为直接引用。

    过程:

    • 编译时:方法调用使用符号引用(如 com/example/MyClass.myMethod:(I)V)
    • 运行时:将符号引用解析为方法的直接地址

    静态解析 vs 动态解析:

    • invokespecial、invokestatic:在类加载的解析阶段静态解析
    • invokevirtual、invokeinterface:在运行时动态解析(多态)

    Q129: 什么是操作数栈?

    答案: 操作数栈是栈帧中的工作区,字节码指令在此进行运算。

    // 计算 1 + 2
    // 字节码:
    iconst_1 // 将 1 压入操作数栈
    iconst_2 // 将 2 压入操作数栈
    iadd // 弹出两个 int,相加,将结果 3 压回
    istore_1 // 弹出结果,存入局部变量表第 1 个 Slot


    Q130: 什么是方法的分派(Dispatch)?

    答案:

    静态分派:

    • 编译期确定方法目标
    • 对应 invokespecial 和 invokestatic
    • 也叫解析(Resolution)

    动态分派:

    • 运行时确定方法目标
    • 对应 invokevirtual 和 invokeinterface
    • 实现多态

    单分派 vs 多分派:

    • Java 是静态多分派 + 动态单分派
    • 静态分派考虑接收者类型和参数类型(多分派)
    • 动态分派只考虑接收者类型(单分派)

    虚方法表(vtable):

    • 每个类有一个虚方法表
    • 存储虚方法的实际入口地址
    • 子类重写的方法替换为子类的入口
    • 动态分派通过查 vtable 实现

    Q131: 什么是虚方法表(vtable)?

    答案: 虚方法表是 HotSpot 实现动态分派的核心数据结构。

    结构:

    Object 的 vtable:
    [0] hashCode() → Object.hashCode
    [1] equals() → Object.equals
    [2] toString() → Object.toString
    …

    String 的 vtable:
    [0] hashCode() → String.hashCode (重写)
    [1] equals() → String.equals (重写)
    [2] toString() → String.toString (重写)
    …

    创建时机:类加载的连接阶段,为类创建 vtable。


    Q132: 什么是接口方法表(itable)?

    答案: itable 是接口方法的分派表。

    与 vtable 的区别:

    • vtable:只存储类的方法
    • itable:存储接口方法,通过接口索引 + 方法索引定位
    • 一个类可以实现多个接口,itable 更复杂

    Q133: synchronized 在字节码层面是如何实现的?

    答案:

    • 同步代码块:使用 monitorenter 和 monitorexit 指令
    • 同步方法:方法的 access_flags 中设置 ACC_SYNCHRONIZED 标志

    // 同步代码块
    synchronized (obj) {
    // 代码
    }
    // 字节码:
    monitorenter // 获取 obj 的监视器锁
    // 代码
    monitorexit // 释放锁
    // 异常处理中的 monitorexit(保证异常时也能释放锁)


    Q134: 什么是局部变量表的 Slot 复用?

    答案: 局部变量表中的 Slot 可以被后续变量复用(超出作用域后)。

    public void method() {
    {
    int a = 1; // 占用 Slot 1
    } // a 超出作用域
    int b = 2; // 复用 Slot 1
    }

    影响:

    • 节省内存
    • 可能影响 GC:Slot 被复用后,原变量的引用被覆盖,对象可被 GC

    Q135: 什么是字节码的异常处理表?

    答案: 异常处理表记录了代码中哪些范围的代码需要处理什么类型的异常,跳转到哪个处理器。

    Exception table:
    from to target type
    0 4 8 Class java/lang/Exception

    含义:0~4 行的字节码如果抛出 Exception,跳转到第 8 行执行。


    十、线程模型与并发深入

    Q136: JVM 的线程模型是怎样的?

    答案: HotSpot 使用内核级线程模型(1:1 模型):

    • 每个 Java 线程对应一个操作系统内核线程
    • 线程的创建、调度、同步由操作系统内核完成
    • 优点:可以利用多核 CPU
    • 缺点:线程创建/切换开销大

    JVM 线程:

    • Java 线程(Thread)→ JVM 内部线程 → 操作系统内核线程

    Q137: JVM 有哪些后台线程?

    答案:

    线程作用
    VM Thread 处理 GC、反优化等 VM 操作
    GC Threads 并行 GC 线程
    Compiler Threads JIT 编译线程
    Signal Dispatcher 处理外部信号
    Reference Handler 处理引用队列(软/弱/虚引用)
    Finalizer 执行 finalize() 方法
    Service Thread 周期性任务

    Q138: 什么是线程栈的大小?如何影响线程数?

    答案: 每个线程的栈大小由 -Xss 控制(默认 1MB)。

    线程数限制:

    最大线程数 ≈ (堆内存 – 其他内存) / 线程栈大小

    • 堆 4GB,栈 1MB → 约 3000 个线程(需预留其他内存)
    • 如果栈设置过大(如 2MB),线程数减半
    • 如果线程数过多 → OutOfMemoryError: unable to create new native thread

    Q139: 什么是 Java 的内存模型(JMM)?

    答案: JMM(Java Memory Model)定义了多线程环境下变量的可见性、有序性和原子性规则。

    核心概念:

    • 主内存(Main Memory):所有线程共享的内存(对应堆)
    • 工作内存(Working Memory):每个线程私有的本地缓存(对应 CPU 缓存/寄存器)
    • 8 种原子操作:lock, unlock, read, load, use, assign, store, write

    三大特性:

  • 原子性:synchronized 和 Lock 保证
  • 可见性:volatile 和 synchronized 保证
  • 有序性:volatile 和 synchronized 保证(禁止指令重排序)

  • Q140: volatile 的底层实现是什么?

    答案: volatile 变量的读写使用内存屏障(Memory Barrier) 保证可见性和有序性。

    底层实现:

    • 写操作:在写操作后插入 StoreLoad 屏障(写回主内存)
    • 读操作:在读操作前插入 LoadLoad 屏障(从主内存读取)

    x86 架构:

    • volatile 写 → lock addl $0, (%rsp)(lock 前缀指令,刷新写缓冲)
    • volatile 写后读 → 插入 mfence 指令

    Q141: synchronized 的锁升级过程?

    答案:

    无锁 → 偏向锁 → 轻量级锁 → 重量级锁

    阶段实现性能
    偏向锁 Mark Word 存储 threadId 最高(无 CAS)
    轻量级锁 CAS 将 Mark Word 指向 Lock Record 高(自旋)
    重量级锁 Monitor 对象(互斥量) 低(阻塞)

    锁消除:JIT 发现锁对象不逃逸,直接去除同步。 锁粗化:JIT 将多次连续的锁操作合并为一次。


    Q142: 什么是 Monitor 对象?

    答案: Monitor 是 JVM 实现重量级锁的底层数据结构(C++ 的 ObjectMonitor)。

    结构:

    ObjectMonitor:
    _owner: 持有锁的线程
    _EntryList: 等待获取锁的线程队列
    _WaitSet: 调用 wait() 的线程队列
    _recursions: 重入计数

    工作流程:

  • 线程尝试获取锁 → CAS 设置 _owner
  • 成功 → 进入同步代码
  • 失败 → 进入 _EntryList 阻塞
  • 调用 wait() → 进入 _WaitSet 等待
  • notify() → 从 _WaitSet 移到 _EntryList

  • Q143: 什么是 AQS(AbstractQueuedSynchronizer)?

    答案: AQS 是 java.util.concurrent 包中锁和同步器的基础框架。

    核心思想:

    • 维护一个 volatile int state 表示同步状态
    • 使用 CLH 队列管理等待线程
    • 子类通过重写 tryAcquire()/tryRelease() 实现不同的同步器

    实现:

    • ReentrantLock:state = 重入次数
    • Semaphore:state = 可用许可数
    • CountDownLatch:state = 计数

    Q144: 什么是 CAS?底层如何实现?

    答案: CAS(Compare And Swap)是一种无锁的原子操作。

    原理:

    CAS(内存地址, 期望值, 新值)
    if (*内存地址 == 期望值) {
    *内存地址 = 新值;
    return true;
    } else {
    return false;
    }

    底层实现:

    • 使用 CPU 的原子指令 cmpxchg
    • x86:lock cmpxchg 指令
    • 保证在多核环境下的原子性

    问题:

    • ABA 问题:值从 A 变为 B 再变回 A,CAS 误认为没变化(使用版本号解决)
    • 自旋开销:CAS 失败后重试,消耗 CPU
    • 只能保证一个变量的原子操作

    Q145: 什么是伪共享(False Sharing)?

    答案: 伪共享是多线程环境下,不同变量存储在同一个缓存行中,导致不必要的缓存失效。

    问题:

    • CPU 缓存以缓存行(64 字节)为单位
    • 如果线程 A 修改变量 X,线程 B 读取变量 Y,X 和 Y 在同一缓存行
    • 线程 A 的修改导致整个缓存行失效,线程 B 需要重新读取

    解决:

    // 使用 @Contended 注解(JDK 8+)
    @sun.misc.Contended
    private volatile long counter;

    -XX:-RestrictContended 允许自定义类使用此注解。


    十一、G1/ZGC/Shenandoah 深入

    Q146: G1 的 Region 有哪些类型?

    答案:

    类型说明
    Eden Region 新对象分配的区域
    Survivor Region 存活对象的中转区域
    Old Region 长期存活对象的区域
    Humongous Region 大对象(> Region 50%)的区域
    Free Region 空闲区域

    Q147: G1 的并发标记如何工作?

    答案:

    1. 初始标记(STW)—— 标记 GC Roots 直接关联的对象(借用 Minor GC 的 STW)
    2. Root Region 扫描 —— 扫描 Survivor Region 引用的 Old Region
    3. 并发标记 —— 与用户线程并发执行,遍历对象图
    4. 最终标记(STW)—— 处理 SATB 缓冲区中的记录
    5. 清理(STW)—— 统计每个 Region 的存活对象,选择回收价值高的 Region


    Q148: G1 的 Mixed GC 触发条件?

    答案:

  • 老年代占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%)
  • 触发并发标记
  • 并发标记完成后,触发 Mixed GC
  • Mixed GC 的选择:

    • 根据 -XX:MaxGCPauseMillis 的目标停顿时间
    • 选择回收价值最高的老年代 Region
    • 同时回收所有新生代 Region

    Q149: ZGC 的染色指针具体如何工作?

    答案: ZGC 使用 64 位指针的高 4 位存储元数据:

    位名称含义
    42-43 Marked0/Marked1 标记位(用于并发标记)
    44 Remapped 重映射位(用于并发转移)
    45 Finalizable 只能通过 finalize() 访问
    0-41 Address 对象地址(支持 4TB 堆)

    读屏障:

    // 访问对象时
    Object o = ref.obj;
    // 读屏障:检查指针标记位
    if (指针标记位 != 期望值) {
    // 自愈:修正指针
    ref.obj = self_heal(ref.obj);
    o = ref.obj;
    }


    Q150: ZGC 的并发转移如何实现?

    答案: ZGC 的对象转移(Relocation)几乎完全并发:

  • 选择待转移的 Region
  • 并发转移:将存活对象复制到新 Region
  • 并发重映射:修正所有指向旧地址的引用
  • 关键:

    • 使用转发表(Forwarding Table) 记录旧地址到新地址的映射
    • 读屏障在访问旧地址时,自动修正为新地址
    • 不需要 STW

    Q151: Shenandoah 的转发指针如何工作?

    答案: Shenandoah 在每个对象头部添加一个转发指针(Brooks Pointer)。

    原理:

    • 对象移动时,更新转发指针指向新地址
    • 后续访问旧地址时,通过转发指针跳转到新地址
    • 并发转移时,使用 CAS 更新转发指针

    读屏障:

    • 每次读取对象时检查转发指针
    • 如果转发指针不指向自身,说明对象已移动,自动跳转

    Q152: ZGC 和 Shenandoah 的区别?

    答案:

    对比项ZGCShenandoah
    核心技术 染色指针 + 读屏障 转发指针 + 读屏障
    STW 时间 < 1ms < 10ms
    堆大小限制 支持 4TB+ 支持 TB 级
    指针大小 64 位(压缩后 32 位) 64 位
    维护者 Oracle Red Hat
    JDK 版本 JDK 11+ JDK 12+(实验性)

    Q153: 什么是 ZGC 的多重映射(Multi-Mapping)?

    答案: ZGC 使用多重映射实现染色指针:

    • 同一个物理内存被映射到多个虚拟地址
    • 不同的标记位对应不同的虚拟地址
    • 但指向同一个物理内存

    虚拟地址 A (Marked0=0, Remapped=0) ──→ 物理地址 X
    虚拟地址 B (Marked0=1, Remapped=0) ──→ 物理地址 X
    虚拟地址 C (Marked0=0, Remapped=1) ──→ 物理地址 X


    Q154: G1 的 RSet 维护开销有多大?

    答案: RSet 的维护开销:

    • 写屏障:每次引用赋值都需要更新 RSet
    • 内存开销:RSet 占用约 5%~10% 的堆内存
    • CPU 开销:写屏障和并发细化线程消耗 CPU

    优化:

    • -XX:G1RSetUpdatingPauseTimePercent:控制 RSet 更新的 STW 时间占比
    • -XX:G1ConcRefinementThreads:并发细化线程数

    Q155: 什么是 G1 的 Humongous 对象分配?

    答案: 超过 Region 50% 大小的对象被认定为 Humongous 对象。

    特点:

    • 直接分配在连续的 Humongous Region 中
    • 逻辑上属于老年代
    • 不参与 Young GC 的复制
    • Mixed GC 时回收
    • 可能导致提前触发并发标记

    问题:

    • Humongous 对象分配可能引起连续的 Region 分配,导致碎片
    • 频繁创建/销毁 Humongous 对象会影响 G1 性能

    Q156: 如何选择 G1、ZGC、Shenandoah?

    答案:

    场景推荐
    堆 4GB~64GB,通用场景 G1
    堆 > 64GB,低延迟 ZGC
    需要极致低延迟(< 1ms STW) ZGC
    Red Hat 技术栈 Shenandoah
    JDK 8 G1
    JDK 11+ G1 或 ZGC

    Q157: ZGC 的触发条件是什么?

    答案: ZGC 的触发条件相对简单:

    • 堆使用率达到某个阈值时触发
    • 由 -XX:ZAllocationSpikeTolerance 控制(默认 2.0)
    • ZGC 会根据分配速率自适应调整触发时机

    Q158: G1 的并发标记失败会怎样?

    答案: 并发标记失败会导致:

  • 退化为单线程的 Full GC(Serial Old)
  • 完全 STW,耗时很长
  • 原因:并发标记期间堆满了,无法继续
  • 避免:

    • 不要让堆使用率过高
    • 增大堆或调整 -XX:InitiatingHeapOccupancyPercent

    Q159: ZGC 的指针压缩如何工作?

    答案: ZGC 在 JDK 16+ 支持指针压缩(-XX:+UseCompressedOops):

    • 堆 < 4GB 时,指针压缩为 32 位
    • 堆 4GB~4TB 时,使用 64 位指针(带染色位)
    • 染色位存储在指针的高 4 位

    Q160: 什么是 G1 的疏散(Evacuation)?

    答案: 疏散是 G1 将存活对象从一个 Region 复制到另一个 Region 的过程。

    过程:

  • 选择要回收的 Region
  • 将存活对象复制到空闲 Region
  • 更新引用(RSet 辅助)
  • 回收原 Region
  • 疏散失败:

    • 目标 Region 没有足够空间
    • 退化为 Full GC

    十二、JVM 工具与监控

    Q161: 常用的 JVM 监控工具有哪些?

    答案:

    工具作用命令
    jps 列出 Java 进程 jps -l
    jstat 监控 GC 统计 jstat -gcutil <pid> 1000
    jmap 生成堆转储 jmap -dump:format=b,file=dump.hprof <pid>
    jstack 输出线程栈 jstack <pid>
    jinfo 查看/修改 JVM 参数 jinfo -flags <pid>
    jcmd 综合命令 jcmd <pid> help
    JConsole 图形化监控 jconsole <pid>
    JVisualVM 图形化分析 jvisualvm
    MAT 堆转储分析 Eclipse 插件
    JFR 低开销记录 jcmd <pid> JFR.start
    Arthas 在线诊断 java -jar arthas-boot.jar

    Q162: 如何使用 jcmd?

    答案:

    # 列出 Java 进程
    jcmd -l

    # 查看 JVM 参数
    jcmd <pid> VM.flags

    # 查看系统属性
    jcmd <pid> VM.system_properties

    # 生成堆转储
    jcmd <pid> GC.heap_dump dump.hprof

    # 查看线程栈
    jcmd <pid> Thread.print

    # 查看 GC 信息
    jcmd <pid> GC.heap_info

    # 查看类加载统计
    jcmd <pid> VM.classloader_stats

    # 查看 CodeCache
    jcmd <pid> Compiler.codecache

    # 触发 GC
    jcmd <pid> GC.run


    Q163: 如何使用 Arthas 进行在线诊断?

    答案: Arthas 是阿里巴巴开源的 Java 诊断工具。

    # 启动
    java -jar arthas-boot.jar

    # 查看 Dashboard
    dashboard

    # 查看线程
    thread

    # 查看方法调用耗时
    trace com.example.MyClass myMethod

    # 反编译类
    jad com.example.MyClass

    # 监控方法返回值
    watch com.example.MyClass myMethod "{returnObj}"

    # 查看方法调用栈
    stack com.example.MyClass myMethod

    # 热替换代码
    mc /tmp/MyClass.java
    redefine /tmp/MyClass.class


    Q164: 如何使用 JFR(Java Flight Recorder)?

    答案:

    # 启动录制(60 秒)
    jcmd <pid> JFR.start duration=60s filename=recording.jfr

    # 持续录制
    jcmd <pid> JFR.start settings=profile filename=continuous.jfr
    jcmd <pid> JFR.dump filename=dump.jfr

    # 停止录制
    jcmd <pid> JFR.stop

    # JVM 启动参数
    -XX:StartFlightRecording=duration=60s,filename=recording.jfr
    -XX:FlightRecorder=sampledThreads

    分析:使用 JDK Mission Control(JMC)打开 .jfr 文件。


    Q165: 如何使用 MAT 分析内存泄漏?

    答案:

  • 打开 .hprof 文件

  • Leak Suspects Report:自动分析泄漏可疑点

  • Dominator Tree:查看对象的内存占用排名

  • Path to GC Roots(排除弱引用/软引用):找到引用链

  • OQL 查询:

    SELECT * FROM java.util.HashMap WHERE size > 10000
    SELECT * FROM java.lang.String WHERE value.length > 1000


  • Q166: 如何监控元空间?

    答案:

    # jstat 查看元空间
    jstat -gcmetacapacity <pid>

    # JMX 监控
    # java.lang:type=MemoryPool,name=Metaspace

    # 参数
    -XX:MetaspaceSize=256m
    -XX:MaxMetaspaceSize=512m


    Q167: 如何监控直接内存?

    答案:

    # 开启 NMT
    -XX:NativeMemoryTracking=summary

    # 查看内存分布
    jcmd <pid> VM.native_memory summary

    # 查看内存基线
    jcmd <pid> VM.native_memory baseline

    # 对比变化
    jcmd <pid> VM.native_memory summary.diff


    Q168: 如何监控 JIT 编译?

    答案:

    # 查看 JIT 编译日志
    -XX:+PrintCompilation

    # 输出示例
    # 68 1 3 java.lang.String::hashCode (55 bytes)
    # 表示第 68 个编译任务,编译 ID=1,编译级别 3(C1),类名和方法名

    # 查看方法内联
    -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining

    # 查看 CodeCache
    jcmd <pid> Compiler.codecache


    Q169: 如何查看 JVM 的所有参数?

    答案:

    # 查看所有参数
    jcmd <pid> VM.flags

    # 查看特定参数
    java -XX:+PrintFlagsFinal -version | grep UseG1GC

    # 查看参数默认值
    java -XX:+PrintFlagsInitial

    # 查看参数最终值
    java -XX:+PrintFlagsFinal


    Q170: 如何生成和分析 GC 日志?

    答案: JDK 8:

    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -XX:+PrintHeapAtGC
    -Xloggc:/path/to/gc.log

    JDK 9+:

    -Xlog:gc*:file=/path/to/gc.log:time,tags,level:filecount=10,filesize=50M

    分析工具:

    • GCViewer:图形化分析 GC 日志
    • GCEasy:在线 GC 日志分析(gceasy.io)
    • GCPlot:GC 日志可视化

    十三、高级主题

    Q171: 什么是 Java 的 Platform Module System(JPMS)?

    答案: JPMS 是 JDK 9 引入的模块化系统,将 JDK 拆分为多个模块。

    核心概念:

    • module-info.java:定义模块的导出和依赖
    • 强封装:模块内部的包默认不对外可见
    • 减少了 JDK 的启动时间和内存占用

    与类加载的关系:

    • 每个模块有独立的类加载器命名空间
    • 模块之间的访问受 exports 和 requires 约束

    Q172: 什么是 Value Types(Valhalla 项目)?

    答案: Valhalla 是 OpenJDK 的项目,目标是引入值类型(Value Types)。

    核心思想:

    • 值类型是不可变的、无引用的数据类型
    • 直接在栈上或数组中内联存储,不需要堆分配和 GC
    • 类似于 C 语言的 struct

    优势:

    • 消除对象头开销
    • 提高缓存命中率
    • 减少 GC 压力

    Q173: 什么是 Project Loom(虚拟线程)?

    答案: Project Loom 是 JDK 21 正式引入的虚拟线程(Virtual Threads)。

    核心思想:

    • 虚拟线程是 JVM 管理的轻量级线程
    • 不直接映射到操作系统内核线程
    • 数量可以达到百万级
    • 由 JVM 自动调度到少量载体线程(Carrier Thread)上

    与传统线程的对比:

    对比项平台线程虚拟线程
    创建开销 大(~1MB 栈) 极小(~几 KB)
    数量限制 ~几千 ~百万
    调度 操作系统 JVM(ForkJoinPool)
    阻塞 阻塞内核线程 自动卸载(unmount)

    // 创建虚拟线程
    Thread.startVirtualThread(() -> {
    // 任务代码
    });

    // 虚拟线程池
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> task());
    }


    Q174: 什么是协程?Java 的虚拟线程是协程吗?

    答案: 协程是用户态的轻量级线程,可以在执行过程中挂起和恢复。

    Java 的虚拟线程:

    • 虚拟线程在概念上类似于协程
    • 但实现方式不同:虚拟线程通过 Continuation 实现挂起/恢复
    • 虚拟线程的 API 与普通线程完全兼容

    与其他语言的对比:

    • Go 的 Goroutine:M:N 调度
    • Kotlin 的协程:编译器 CPS 变换
    • Java 的虚拟线程:Continuation + ForkJoinPool

    Q175: 什么是 ZGC 的分代模式(Generational ZGC)?

    答案: JDK 21 引入了分代 ZGC(-XX:+UseZGC -XX:+ZGenerational)。

    核心思想:

    • 将堆分为年轻代和老年代
    • 优先回收年轻代(短命对象多)
    • 提高回收效率,降低内存占用

    与传统 ZGC 的区别:

    • 传统 ZGC:非分代,每次回收整个堆
    • 分代 ZGC:类似 G1 的分代回收,效率更高

    Q176: 什么是 Shenandoah 的并发整理?

    答案: Shenandoah 的并发整理通过转发指针实现:

  • 并发标记:确定存活对象
  • 并发整理:将存活对象复制到新 Region
  • 并发引用修正:通过读屏障修正引用
  • 关键:读屏障在访问对象时检查转发指针,自动跳转到新地址。


    Q177: 什么是 GC 的安全点(Safepoint)机制的优化?

    答案: JVM 不断优化安全点机制,减少 STW 时间:

  • JEP 312(JDK 10):Thread-Local Handshakes

    • 不需要所有线程同时到达安全点
    • 可以逐个线程执行握手操作
    • 减少了安全点的全局暂停时间
  • JEP 358(JDK 12):改进安全点统计

    • 更精确地测量安全点停顿时间

  • Q178: 什么是 JFR 的事件系统?

    答案: JFR 记录 JVM 运行时的事件,每个事件包含时间戳、持续时间、相关数据。

    事件类型:

    • 瞬时事件:发生即记录(如对象分配)
    • 持续事件:有开始和结束(如 GC 事件)
    • 采样事件:定期采样(如方法采样)

    关键事件:

    • jdk.GCHeapSummary:堆使用摘要
    • jdk.GCCause:GC 触发原因
    • jdk.ThreadStart / jdk.ThreadEnd:线程生命周期
    • jdk.ObjectAllocationInNewTLAB:TLAB 分配
    • jdk.ExecuteVMOperation:VM 操作(如 GC)

    Q179: 什么是 JVM 的 CDS(Class Data Sharing)?

    答案: CDS 是 JVM 的类数据共享技术,加速应用启动。

    原理:

  • 首次启动时,将类的元数据转储到共享归档文件(.jsa)
  • 后续启动时,直接映射共享归档文件到内存
  • 避免重复解析和加载类
  • JDK 版本演进:

    • JDK 8:基础 CDS(只支持 Bootstrap ClassLoader 加载的类)
    • JDK 10:AppCDS(支持应用类的共享)
    • JDK 13:Dynamic CDS(动态归档)

    # 生成共享归档
    java -Xshare:dump -XX:SharedArchiveFile=app.jsa -cp app.jar

    # 使用共享归档启动
    java -Xshare:on -XX:SharedArchiveFile=app.jsa -cp app.jar


    Q180: 什么是 JVM 的分层编译(Tiered Compilation)?

    答案: 分层编译结合了解释器、C1 编译器和 C2 编译器的优势。

    编译层级:

    Level说明
    0 解释执行
    1 C1 编译,无性能分析
    2 C1 编译,有限性能分析
    3 C1 编译,完整性能分析
    4 C2 编译,完全优化

    流程:

    方法首次调用 → Level 0(解释执行)
    → 调用次数达到阈值 → Level 3(C1 编译 + 性能分析)
    → 热点数据足够 → Level 4(C2 编译,完全优化)


    Q181: 什么是 JVM 的类数据压缩?

    答案: JVM 使用多种压缩技术减少内存占用:

  • 指针压缩(Compressed Oops):64 位引用压缩为 32 位
  • 类指针压缩(Compressed Class Pointers):类元数据指针压缩
  • Klass 压缩:类的 Klass 对象压缩存储

  • Q182: 什么是 JVM 的字节码验证?

    答案: 字节码验证在类加载的连接阶段进行,确保字节码的安全性。

    验证内容:

  • 类型安全:操作数栈的数据类型正确
  • 访问控制:private/protected 成员不被非法访问
  • 初始化安全:对象使用前已初始化
  • 分支安全:跳转目标在方法范围内
  • JDK 7+ 的优化:

    • StackMapTable 属性:编译期生成栈映射表,减少运行时验证
    • 类型检查验证(Type Checking Verification)替代类型推断验证

    Q183: 什么是 JVM 的安全点轮询?

    答案: 安全点轮询是线程检查是否需要暂停的机制。

    实现:

    • JVM 在安全点位置插入轮询代码
    • 轮询代码检查一个全局标志(安全点标志)
    • 如果标志被设置,线程暂停

    HotSpot 的优化:

    • 使用内存保护页实现轮询
    • 安全点标志是一个内存页
    • 正常情况:线程读取该页面(已映射)
    • GC 时:取消该页面的映射 → 线程访问时触发页错误 → 暂停

    Q184: 什么是 JVM 的去优化(Deoptimization)?

    答案: 去优化是将 JIT 编译的代码废弃,回退到解释执行。

    触发条件:

  • 类被卸载:编译代码依赖的类被回收
  • 类型推测失败:内联的虚方法假设被打破
  • 代码缓存满:需要清理旧代码
  • 调试需要:断点调试
  • 去优化的类型:

    • 部分去优化:只废弃部分编译代码
    • 完全去优化:废弃整个方法的编译代码

    Q185: 什么是 JVM 的逃逸分析的局限性?

    答案: 逃逸分析虽然强大,但有局限性:

  • 分析开销:逃逸分析本身消耗 CPU
  • 保守估计:如果无法确定是否逃逸,保守认为会逃逸
  • 不支持跨方法分析:只能分析单个方法内的逃逸
  • 依赖 JIT:只有被 JIT 编译的方法才会做逃逸分析
  • 不能替代 GC:逃逸分析只能减少短命对象的堆分配

  • 十四、场景实战题

    Q186: 线上出现 Full GC 频繁,如何排查?

    答案:

  • 查看 GC 日志:分析 Full GC 的频率、触发原因、耗时
  • 查看堆内存:jstat -gcutil <pid> 观察老年代使用率
  • 生成堆转储:jmap -dump:format=b,file=dump.hprof <pid>
  • 分析堆转储:MAT → Leak Suspects → 找到内存泄漏
  • 常见原因:
    • 内存泄漏 → 修复代码
    • 堆太小 → 增大 -Xmx
    • 新生代太小 → 增大 -Xmn
    • 元空间不足 → 增大 MetaspaceSize
    • System.gc() 调用 → -XX:+DisableExplicitGC

  • Q187: 线上出现 OOM,如何快速定位?

    答案:

  • 保留现场:确保启动参数有 -XX:+HeapDumpOnOutOfMemoryError
  • 获取堆转储:如果没配置自动 dump,用 jmap -dump:live 手动生成
  • MAT 分析:
    • Leak Suspects Report → 自动分析泄漏点
    • Dominator Tree → 找到占用内存最大的对象
    • Path to GC Roots → 追踪引用链
  • 定位代码:根据引用链找到创建/持有对象的代码
  • 修复:内存泄漏→修复引用;内存不足→增大堆/优化代码

  • Q188: 线上出现 CPU 飙高,如何排查?

    答案:

  • 找到 CPU 高的线程:

    top -Hp <pid>
    printf '%x' <tid>
    jstack <pid> | grep -A 20 <tid_hex>

  • 常见原因:

    • Full GC 频繁 → GC 线程占 CPU
    • 死循环 → 线程栈显示在循环代码处
    • 正则表达式回溯 → 正则匹配卡住
  • GC 导致的 CPU 飙高:查看 GC 日志,分析 Full GC 原因


  • Q189: 线上出现死锁,如何排查?

    答案:

    # 检测死锁
    jstack -l <pid>

    # 输出中会显示:
    # Found one Java-level deadlock:
    # "Thread-1":
    # waiting to lock monitor 0x00007f…
    # locked <0x000000076ab8b9d0> (a java.lang.Object)
    # "Thread-2":
    # waiting to lock monitor 0x00007f…
    # locked <0x000000076ab8b9e0> (a java.lang.Object)

    解决方案:

  • 按固定顺序获取锁
  • 使用 tryLock() 设置超时
  • 使用 jstack 找到死锁的线程和锁,修复代码

  • Q190: 如何优化一个频繁 GC 的服务?

    答案:

  • 分析 GC 日志:确定 GC 类型(Minor/Major/Full)、频率、耗时
  • 调整堆大小:
    • -Xms = -Xmx(避免动态扩展)
    • 新生代占堆的 1/3~1/2
  • 选择合适的收集器:
    • 低延迟:G1(-XX:MaxGCPauseMillis=200)
    • 吞吐量:Parallel
  • 优化代码:
    • 减少临时对象创建
    • 使用对象池
    • 避免大对象直接进入老年代
  • 监控:JFR + Prometheus + Grafana

  • Q191: 如何排查 Metaspace OOM?

    答案:

    # 查看元空间使用
    jstat -gcmetacapacity <pid>

    # 生成堆转储
    jmap -dump:format=b,file=dump.hprof <pid>

    # MAT 中查看 ClassLoader 和 Class 的实例数

    常见原因:

  • CGLIB 动态代理生成大量类
  • Groovy/JSP 动态编译
  • 类加载器泄漏(Web 容器热部署)
  • 解决:

    • 增大 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize
    • 排查类加载器泄漏
    • 限制动态生成类的数量

    Q192: 如何优化大对象的内存使用?

    答案:

  • 避免一次性加载大文件到内存:使用流式处理
  • 使用直接内存:NIO ByteBuffer,避免堆内复制
  • 压缩数据:使用压缩算法减少内存占用
  • 分块处理:将大对象拆分为多个小对象
  • 使用堆外缓存:如 Ehcache 的 Off-Heap 存储

  • Q193: 如何设计一个低延迟的 JVM 参数?

    答案:

    # 低延迟参数模板
    -Xms8g -Xmx8g # 固定堆大小
    -XX:+UseZGC # 使用 ZGC
    -XX:+ZGenerational # 分代 ZGC(JDK 21+)
    -XX:MaxGCPauseMillis=5 # 目标停顿 5ms
    -XX:+AlwaysPreTouch # 启动时预分配内存
    -XX:+UseNUMA # NUMA 感知
    -XX:-UseBiasedLocking # 禁用偏向锁(JDK 15+已移除)
    -XX:+UseTransparentHugePages # 大页
    -XX:NativeMemoryTracking=summary # NMT


    Q194: 如何排查内存泄漏中的 ThreadLocal 问题?

    答案:

  • 生成堆转储:jmap -dump:format=b,file=dump.hprof <pid>
  • MAT 中查找 ThreadLocalMap:
    • OQL: SELECT * FROM java.lang.ThreadLocal$ThreadLocalMap
  • 查看 Entry 的 value:
    • 找到没有被 remove 的 ThreadLocal
  • 追踪线程:查看哪个线程持有该 ThreadLocalMap
  • 修复:在 finally 中调用 threadLocal.remove()

  • Q195: 如何排查 NIO 的直接内存泄漏?

    答案:

    # 开启 NMT
    -XX:NativeMemoryTracking=detail

    # 查看直接内存
    jcmd <pid> VM.native_memory detail

    # 对比两次快照
    jcmd <pid> VM.native_memory baseline
    # … 运行一段时间 …
    jcmd <pid> VM.native_memory summary.diff

    排查:

    • 检查 ByteBuffer.allocateDirect() 是否正确释放
    • 检查 Netty 的 ByteBuf 是否 release()
    • 使用 Unsafe.allocateMemory() 的代码是否有 freeMemory()

    Q196: 如何监控 GC 对服务延迟的影响?

    答案:

  • GC 日志分析:

    • 记录每次 GC 的 STW 时间
    • 统计 P99/P999 GC 停顿时间
  • JFR 分析:

    • 记录 jdk.GCHeapSummary 事件
    • 分析 GC 停顿时间分布
  • 应用层监控:

    • 记录请求的 P99 延迟
    • 与 GC 时间关联分析

  • Q197: 如何优化 G1 的停顿时间?

    答案:

    # 设置目标停顿时间
    -XX:MaxGCPauseMillis=200

    # 调整触发并发标记的阈值
    -XX:InitiatingHeapOccupancyPercent=45

    # 控制 Mixed GC 的老年代 Region 数
    -XX:G1MixedGCCountTarget=8

    # 控制 RSet 更新时间
    -XX:G1RSetUpdatingPauseTimePercent=5

    # 调整 Region 大小
    -XX:G1HeapRegionSize=16m


    Q198: 如何排查类加载冲突?

    答案: 类加载冲突是指同一个类被多个类加载器加载,导致 ClassCastException 或行为不一致。

    排查:

    # 查看类的加载器
    jcmd <pid> VM.classloader_stats

    # Arthas 查看类的加载信息
    sc -d com.example.MyClass

    # 查看类的来源
    -XX:+TraceClassLoading
    -XX:+TraceClassUnloading

    解决:

    • 排除重复的 jar 包
    • 使用 mvn dependency:tree 分析依赖冲突
    • 统一类加载器

    Q199: 如何设计一个高并发服务的 JVM 参数?

    答案:

    # 16 核 32GB 机器
    -Xms16g -Xmx16g
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=100
    -XX:G1HeapRegionSize=16m
    -XX:InitiatingHeapOccupancyPercent=35
    -XX:MetaspaceSize=256m
    -XX:MaxMetaspaceSize=512m
    -XX:MaxDirectMemorySize=2g
    -Xss512k
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/opt/logs/dump.hprof
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -Xloggc:/opt/logs/gc.log
    -XX:+UseGCLogFileRotation
    -XX:NumberOfGCLogFiles=10
    -XX:GCLogFileSize=50M
    -XX:NativeMemoryTracking=summary


    Q200: 如何进行 JVM 的性能基准测试?

    答案:

  • JMH(Java Microbenchmark Harness):

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void testMethod() {
    // 被测代码
    }

  • 测试指标:

    • 吞吐量(ops/sec)
    • 延迟(P50/P95/P99)
    • GC 停顿时间
    • 内存使用
  • 测试方法:

    • 预热:先运行 5~10 轮
    • 测试:运行 10~20 轮,取平均
    • 对比:不同 JVM 参数/不同代码版本
  • 工具:

    • JMH:微基准测试
    • wrk/ab:HTTP 接口压测
    • JMeter:复杂场景压测

  • 附录:速查表

    JVM 内存区域速查

    区域线程存储异常
    程序计数器 私有 字节码指令地址 无
    虚拟机栈 私有 栈帧(局部变量、操作数栈) StackOverflowError / OOM
    本地方法栈 私有 Native 方法栈帧 StackOverflowError / OOM
    堆 共享 对象实例、数组 OOM
    方法区 共享 类信息、常量、静态变量 OOM
    直接内存 共享 NIO ByteBuffer OOM

    GC 算法速查

    算法优点缺点使用
    标记-清除 简单 碎片 CMS
    标记-复制 无碎片、高效 空间浪费 新生代
    标记-整理 无碎片 移动成本 老年代

    垃圾回收器速查

    收集器区域特点参数
    Serial 新生代 单线程 STW -XX:+UseSerialGC
    ParNew 新生代 多线程 STW -XX:+UseParNewGC
    Parallel Scavenge 新生代 吞吐量优先 -XX:+UseParallelGC
    CMS 老年代 低延迟 -XX:+UseConcMarkSweepGC
    G1 整堆 可控停顿 -XX:+UseG1GC
    ZGC 整堆 超低延迟 -XX:+UseZGC
    Shenandoah 整堆 超低延迟 -XX:+UseShenandoahGC

    常用 JVM 参数速查

    参数作用建议值
    -Xms / -Xmx 初始/最大堆 设为相同
    -Xmn 新生代 堆的 1/3~1/2
    -Xss 线程栈 512k~1m
    -XX:MetaspaceSize 元空间初始 256m
    -XX:MaxMetaspaceSize 元空间最大 512m
    -XX:MaxGCPauseMillis G1 停顿目标 200ms
    -XX:+HeapDumpOnOutOfMemoryError OOM 时 dump 开启

    常用诊断命令速查

    # 进程
    jps -l

    # GC 监控
    jstat -gcutil <pid> 1000

    # 堆转储
    jmap -dump:format=b,file=dump.hprof <pid>

    # 线程栈
    jstack <pid>

    # 综合
    jcmd <pid> VM.flags
    jcmd <pid> GC.heap_info
    jcmd <pid> Thread.print

    # 在线诊断
    java -jar arthas-boot.jar


    📝 总结:本文共 200 道 JVM 面试题,覆盖从内存模型到 GC 调优的全部核心知识。重点掌握:JVM 内存区域(堆/栈/方法区)、对象创建与内存分配、类加载与双亲委派、GC 算法与收集器(CMS/G1/ZGC)、JIT 编译与逃逸分析、JVM 调优参数、内存泄漏排查工具(MAT/jstack/jmap)。面试时结合线上排查经验,展示从原理到实践的完整能力。

    赞(0)
    未经允许不得转载:171主机测评 » JVM 常见八股面试问答:深入底层原理
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址