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

🎯 适用:Java 后端开发、架构师、中间件开发岗位面试

📖 内容覆盖:内存模型 · 对象创建 · 类加载 · 垃圾回收算法 · 垃圾回收器 · JIT编译 · JVM调优 · 内存排查 · 字节码 · 线程模型 · G1/ZGC/Shenandoah · 工具监控 · 场景实战


目录


一、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) ,不是对象本身。

java 复制代码
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+ 为元空间)存储:

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

JDK 7 vs JDK 8 的变化:

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

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

答案:

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

底层实现:

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

底层执行过程:

  1. 检查字符串常量池中是否有 "abc"
  2. 如果没有,在常量池中创建 "abc" 字符串对象
  3. 在堆中创建一个新的 String 对象,内容为 "abc"
  4. 栈中的引用 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
原因 代码缺陷(如静态集合未清理) 内存太小 / 对象太多 / 泄漏导致
解决 找到并修复引用未释放的代码 增大内存 / 修复泄漏 / 优化代码

常见内存泄漏场景:

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

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

答案:

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

排查步骤:

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

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

答案:

  1. 递归过深:无限递归或递归层级太深
  2. 栈帧过大:方法中定义大量局部变量或大数组
  3. -Xss 设置过小

解决方案:

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

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

答案:

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

底层原因:

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

Q16: 什么是逃逸分析?

答案 :

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

逃逸类型:

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

基于逃逸分析的优化:

  1. 栈上分配(Stack Allocation):不逃逸的对象直接在栈上分配,方法结束自动释放,不需要 GC
  2. 标量替换(Scalar Replacement):将对象拆分为基本类型变量,直接在栈上存储
  3. 同步消除(Lock Elision):不逃逸线程的对象,去掉同步锁
java 复制代码
// 栈上分配示例
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) 分配
java 复制代码
// 对象分配流程
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: 对象的创建过程是怎样的?

答案:

java 复制代码
Object obj = new Object();

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

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

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 包括:

  1. 虚拟机栈中引用的对象(局部变量表中的引用)
  2. 方法区中静态变量引用的对象 (static Object obj)
  3. 方法区中常量引用的对象 (static final Object)
  4. 本地方法栈中 JNI 引用的对象
  5. JVM 内部引用(如 Class 对象、异常对象、系统类加载器)
  6. 被同步锁(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: 对象什么时候进入老年代?

答案:

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

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

答案 :

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

担保流程:

  1. 如果老年代可用空间 > 新生代所有对象总空间 → 安全执行 Minor GC
  2. 如果不成立,检查 -XX:+HandlePromotionFailure 是否允许担保失败
  3. 如果允许,检查老年代可用空间 > 历次晋升的平均大小
  4. 如果大于平均值 → 冒险执行 Minor GC
  5. 如果小于平均值 → 触发 Full GC

JDK 6 Update 24 之后:规则简化为只要老年代可用空间 > 新生代所有对象总空间,就执行 Minor GC;否则 Full GC。


二、对象创建与内存分配

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

答案:

  1. 指针碰撞(Bump the Pointer):

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

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

并发安全:

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

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

答案:

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

详细流程:

  1. 创建 :new 关键字 → 类加载 → 分配内存 → 初始化零值 → 设置对象头 → 执行构造方法
  2. 使用:被引用、调用方法
  3. 不可达:所有引用被置 null 或离开作用域
  4. 第一次标记:可达性分析发现不可达
  5. 第二次标记(finalize) :如果重写了 finalize() 且未被调用过,放入 F-Queue
  6. Finalizer 线程执行 finalize():如果在 finalize 中重新建立引用,对象复活
  7. 第二次标记完成:如果仍然不可达,标记为可回收
  8. 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: 对象的访问定位方式有哪些?

答案:

  1. 句柄访问:

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

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

    句柄访问: 引用 → 句柄池 → 对象实例数据
    → 类型数据(方法区)

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


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

答案 :

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

java 复制代码
// 原始代码
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 字节的整数倍。

原因:

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

对象大小计算:

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

三、类加载机制

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

答案 :

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

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

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

各阶段详解:

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

准备阶段的细节:

java 复制代码
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() 源码):

java 复制代码
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: 双亲委派模型的作用是什么?

答案:

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

为什么核心类不能被篡改:

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

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

答案:

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

SPI 机制打破双亲委派的原因:

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

Tomcat 打破双亲委派的原因:

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

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

答案 :

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

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

  1. new 实例化对象
  2. 读取/设置类的静态字段(final 常量除外)
  3. 调用类的静态方法
  4. 反射调用(Class.forName())
  5. 初始化子类时,如果父类未初始化,先初始化父类
  6. JVM 启动时的主类

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

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

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

答案 :

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

特点:

  1. 由编译器自动收集所有类变量的赋值动作 和静态代码块合并而成
  2. JVM 保证 <clinit>() 在多线程环境中被正确加锁同步(只会执行一次)
  3. 如果 <clinit>() 执行缓慢,会造成死锁(其他线程一直等待)
  4. 父类的 <clinit>() 先于子类执行
  5. 接口的 <clinit>() 不会先执行父接口的
java 复制代码
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: 什么是类的卸载?什么条件下会卸载?

答案 :

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

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

  1. 该类的所有实例都已被 GC 回收
  2. 加载该类的类加载器已被 GC 回收
  3. 该类的 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: 类加载器的命名空间是什么?

答案 :

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

规则:

  • 同一个命名空间中,类名必须唯一
  • 不同命名空间中,可以存在同名的类(不同实例)
  • 判断两个类是否相同:类名相同 + 同一个类加载器加载
java 复制代码
// 两个不同类加载器加载的同名类是不同的类
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: 什么是类的连接阶段中的验证?

答案 :

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

四种验证:

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

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

答案 :

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

java 复制代码
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 中的动态代理有哪几种实现方式?

答案:

  1. JDK 动态代理:

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

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

底层差异:

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

四、垃圾回收算法

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

答案:

  1. 引用计数法(JVM 不使用):

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

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

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

答案:

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

标记-复制的优化:

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

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

答案 :

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

  1. 弱分代假说:绝大多数对象都是朝生夕灭的
  2. 强分代假说:熬过越多次 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 的触发条件:

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

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

答案 :

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

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

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

  • 如果对象没有重写 finalize() 或 finalize() 已被调用过 → 标记为可回收
  • 如果对象重写了 finalize() 且未被调用过 → 放入 F-Queue
  • Finalizer 线程 异步执行 F-Queue 中对象的 finalize()
  • 如果在 finalize() 中重新建立引用(如 this 赋值给类变量)→ 对象复活
  • 复活的对象在第二次标记时被移出回收集合
java 复制代码
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 结束后仍为白色的对象被回收)
灰色 已被扫描,但其引用的对象尚未全部扫描

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

标记过程:

  1. 初始:所有对象为白色,GC Roots 为灰色
  2. 从灰色集合中取出对象,扫描其引用
  3. 将引用的对象标为灰色,自身标为黑色
  4. 重复直到灰色集合为空
  5. 剩余白色对象不可达,可以回收

并发标记的问题:

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

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

答案 :

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

作用:

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

写屏障的类型:

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

底层实现:

java 复制代码
// 写后屏障的伪代码
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)表示没有跨代引用

写屏障维护卡表:

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

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

答案 :

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

晋升条件:

  1. 年龄达到阈值:经过 MaxTenuringThreshold 次 Minor GC 后
  2. 动态年龄判断:Survivor 中同龄对象 ≥ Survivor 空间 50%
  3. 大对象直接晋升:超过 PretenureSizeThreshold
  4. 空间分配担保: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 检查老年代是否有足够空间容纳新生代可能晋升的对象。

流程:

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

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

答案 :

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

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

为什么 JVM 不使用:

  • 无法解决循环引用:两个对象互相引用,计数永远不为 0
  • 需要额外的计数器空间
  • 每次引用变更都需要更新计数器
java 复制代码
// 循环引用示例
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. 并发清除 ------ 与用户线程并发执行,清除未标记对象

缺点:

  1. CPU 敏感:并发阶段占用 CPU 资源
  2. 内存碎片:使用标记-清除算法,产生碎片
  3. 浮动垃圾:并发清除阶段新产生的垃圾本次 GC 无法回收
  4. 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 的区别?

答案:

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

Q73: ZGC 收集器的特点?

答案 :

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

核心技术:

  1. 染色指针(Colored Pointer):

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

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

    • 标记、转移、重定位几乎完全并发执行
    • 几乎无 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 的影响)
  • 生命周期很短的应用
  • 一次性任务
bash 复制代码
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)实现可控停顿。

原理:

  1. G1 维护每个 Region 的回收价值(垃圾量 / 回收耗时)
  2. 根据 -XX:MaxGCPauseMillis 的目标停顿时间
  3. 选择回收价值最高的 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) 操作
  • 自愈:将指针的标记位修正为当前正确的值

并发转移的原理:

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

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

答案 :

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

  1. 漏标:用户线程修改了引用关系,导致 GC 误认为对象不可达
  2. 多标:用户线程删除了引用,导致 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 编译器:

  1. C1(Client Compiler):

    • 编译速度快,优化少
    • 适合客户端应用
    • 参数:-client
  2. 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: 什么是热点代码?如何检测?

答案 :

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

检测方法:

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

HotSpot 的计数器:

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

热度衰减:

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

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

答案:

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

方法内联的条件:

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

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

答案 :

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

java 复制代码
// 原始代码
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:

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

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

答案 :

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

java 复制代码
// 原始代码
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 编译代码被废弃,回退到解释执行的过程。

触发条件:

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

Q94: 什么是 CodeCache?

答案 :

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

底层实现:

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

CodeCache 满了会怎样:

  • JIT 编译停止,所有代码使用解释执行
  • 性能显著下降
  • JVM 日志中会出现 CodeCache is full 警告
bash 复制代码
# 监控 CodeCache
jcmd <pid> Compiler.codecache

Q95: 什么是 GraalVM?

答案 :

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

核心特性:

  1. Graal 编译器:新一代 JIT 编译器,优化能力更强
  2. Native Image:AOT 编译,生成独立可执行文件
  3. 多语言支持:Java、JavaScript、Python、Ruby、R 等
  4. 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 耗时长,老年代空间不足

参数:

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

调优步骤:

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

Q98: 如何调优老年代?

答案 :

原则:

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

参数:

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

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

答案 :

JDK 8 GC 日志参数:

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

JDK 9+ 统一日志格式:

bash 复制代码
-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 暂停所有用户线程的时间。

优化方法:

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

Q101: 如何设置 JVM 参数?

答案:

bash 复制代码
# 基本参数
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?

答案:

bash 复制代码
-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\[\] 数组指向同一个
  • 节省内存

参数:

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

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

答案 :

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

优势:

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

JVM 参数:

bash 复制代码
-XX:+UseLargePages            # 启用大页
-XX:LargePageSizeInBytes=2m   # 大页大小

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


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

答案:

  1. GC 日志分析:

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

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

    • 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 飙高问题?

答案:

  1. 找到 CPU 高的线程:

    bash 复制代码
    top -Hp <pid>  # 找到 CPU 高的线程 ID
    printf '%x' <tid>  # 转换为 16 进制
    jstack <pid> | grep <tid_hex>  # 查看线程栈
  2. 常见原因:

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

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

答案:

  1. 查看堆内存使用:

    bash 复制代码
    jmap -heap <pid>
    jstat -gcutil <pid> 1000
  2. 生成堆转储:

    bash 复制代码
    jmap -dump:format=b,file=dump.hprof <pid>
  3. 分析堆转储:

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

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

答案:

bash 复制代码
# 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 代码缓存

监控方法:

bash 复制代码
# 查看 NIO 直接内存
jcmd <pid> VM.native_memory summary

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

八、内存泄漏与溢出排查

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

答案:

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

Q112: ThreadLocal 的内存泄漏问题?

答案 :

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

泄漏原因:

  1. ThreadLocal 对象被 GC 回收(key 是弱引用)
  2. 但 value 仍然被 Entry 强引用
  3. 如果线程是线程池中的核心线程,不会销毁
  4. value 永远无法被回收

解决方案:

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

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

答案:

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

Q114: 如何使用 jstat 监控 GC?

答案:

bash 复制代码
# 每 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 生成堆转储?

答案:

bash 复制代码
# 生成堆转储
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 分析线程状态?

答案:

bash 复制代码
# 输出线程栈
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 事件
  • 方法采样
  • 内存分配

使用方法:

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

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

分析工具:JDK Mission Control(JMC)


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

答案:

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

Q119: 如何排查 OOM 问题?

答案 :

步骤:

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

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

答案 :

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

常见场景:

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

排查:

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

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

答案 :

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

泄漏原因:

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

排查:

bash 复制代码
# 监控直接内存
-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 的线程数?

答案:

bash 复制代码
# 查看线程数
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: 什么是操作数栈?

答案 :

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

java 复制代码
// 计算 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 标志
java 复制代码
// 同步代码块
synchronized (obj) {
    // 代码
}
// 字节码:
monitorenter    // 获取 obj 的监视器锁
// 代码
monitorexit     // 释放锁
// 异常处理中的 monitorexit(保证异常时也能释放锁)

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

答案 :

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

java 复制代码
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

三大特性:

  1. 原子性 :synchronized 和 Lock 保证
  2. 可见性 :volatile 和 synchronized 保证
  3. 有序性 :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:  重入计数

工作流程:

  1. 线程尝试获取锁 → CAS 设置 _owner
  2. 成功 → 进入同步代码
  3. 失败 → 进入 _EntryList 阻塞
  4. 调用 wait() → 进入 _WaitSet 等待
  5. 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 需要重新读取

解决:

java 复制代码
// 使用 @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 触发条件?

答案:

  1. 老年代占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%)
  2. 触发并发标记
  3. 并发标记完成后,触发 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 堆)

读屏障:

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

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

答案 :

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

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

关键:

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

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

答案 :

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

原理:

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

读屏障:

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

Q152: ZGC 和 Shenandoah 的区别?

答案:

对比项 ZGC Shenandoah
核心技术 染色指针 + 读屏障 转发指针 + 读屏障
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 的并发标记失败会怎样?

答案 :

并发标记失败会导致:

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

避免:

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

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

答案 :

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

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

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

答案 :

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

过程:

  1. 选择要回收的 Region
  2. 将存活对象复制到空闲 Region
  3. 更新引用(RSet 辅助)
  4. 回收原 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?

答案:

bash 复制代码
# 列出 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 诊断工具。

bash 复制代码
# 启动
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)?

答案:

bash 复制代码
# 启动录制(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 分析内存泄漏?

答案:

  1. 打开 .hprof 文件

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

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

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

  5. OQL 查询:

    sql 复制代码
    SELECT * FROM java.util.HashMap WHERE size > 10000
    SELECT * FROM java.lang.String WHERE value.length > 1000

Q166: 如何监控元空间?

答案:

bash 复制代码
# jstat 查看元空间
jstat -gcmetacapacity <pid>

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

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

Q167: 如何监控直接内存?

答案:

bash 复制代码
# 开启 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 编译?

答案:

bash 复制代码
# 查看 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 的所有参数?

答案:

bash 复制代码
# 查看所有参数
jcmd <pid> VM.flags

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

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

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

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

答案 :

JDK 8:

bash 复制代码
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:/path/to/gc.log

JDK 9+:

bash 复制代码
-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)
java 复制代码
// 创建虚拟线程
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 的并发整理通过转发指针实现:

  1. 并发标记:确定存活对象
  2. 并发整理:将存活对象复制到新 Region
  3. 并发引用修正:通过读屏障修正引用

关键:读屏障在访问对象时检查转发指针,自动跳转到新地址。


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

答案 :

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

  1. JEP 312(JDK 10):Thread-Local Handshakes

    • 不需要所有线程同时到达安全点
    • 可以逐个线程执行握手操作
    • 减少了安全点的全局暂停时间
  2. 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 的类数据共享技术,加速应用启动。

原理:

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

JDK 版本演进:

  • JDK 8:基础 CDS(只支持 Bootstrap ClassLoader 加载的类)
  • JDK 10:AppCDS(支持应用类的共享)
  • JDK 13:Dynamic CDS(动态归档)
bash 复制代码
# 生成共享归档
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 使用多种压缩技术减少内存占用:

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

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

答案 :

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

验证内容:

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

JDK 7+ 的优化:

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

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

答案 :

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

实现:

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

HotSpot 的优化:

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

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

答案 :

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

触发条件:

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

去优化的类型:

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

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

答案 :

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

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

十四、场景实战题

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

答案:

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

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

答案:

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

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

答案:

  1. 找到 CPU 高的线程:

    bash 复制代码
    top -Hp <pid>
    printf '%x' <tid>
    jstack <pid> | grep -A 20 <tid_hex>
  2. 常见原因:

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


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

答案:

bash 复制代码
# 检测死锁
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)

解决方案:

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

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

答案:

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

Q191: 如何排查 Metaspace OOM?

答案:

bash 复制代码
# 查看元空间使用
jstat -gcmetacapacity <pid>

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

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

常见原因:

  1. CGLIB 动态代理生成大量类
  2. Groovy/JSP 动态编译
  3. 类加载器泄漏(Web 容器热部署)

解决:

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

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

答案:

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

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

答案:

bash 复制代码
# 低延迟参数模板
-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 问题?

答案:

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

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

答案:

bash 复制代码
# 开启 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 对服务延迟的影响?

答案:

  1. GC 日志分析:

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

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

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

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

答案:

bash 复制代码
# 设置目标停顿时间
-XX:MaxGCPauseMillis=200

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

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

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

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

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

答案 :

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

排查:

bash 复制代码
# 查看类的加载器
jcmd <pid> VM.classloader_stats

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

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

解决:

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

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

答案:

bash 复制代码
# 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 的性能基准测试?

答案:

  1. JMH(Java Microbenchmark Harness):

    java 复制代码
    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @OutputTimeUnit(TimeUnit.SECONDS)
    public void testMethod() {
        // 被测代码
    }
  2. 测试指标:

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

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

    • 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 开启

常用诊断命令速查

bash 复制代码
# 进程
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)。面试时结合线上排查经验,展示从原理到实践的完整能力。

相关推荐
Yuzhiyuxia4 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
考研保研资料分享7 小时前
2025公卫保研清华专硕:三校考核、英文读译与择校复盘
面试
Lichee2011 小时前
招行Java 面试知识点梳理:Spring 事务、锁、线程池、集合
java·面试
aiautotools11 小时前
Claude Code MCP 怎么配:命令、scope 与避坑
面试
soft_hub112 小时前
海外会议怎么用录音APP翻译会议内容
面试·职场和发展
考研保研资料分享12 小时前
2025生物信息跨保浙大:四校考核、文献读译与材料准备
面试
怕浪猫12 小时前
K3.1 直追 Astra,先补 13%
前端·javascript·面试
香蕉大香蕉(06敬畏市场)13 小时前
高频八股 · JVM(8)
java·jvm·面试·八股·大厂实习
CoderYanger13 小时前
Java EE 进阶:5. Spring IoC&DI(第二弹):Spring IoC
java·开发语言·spring boot·spring·面试·职场和发展·java-ee
Rain的Java大神之路14 小时前
面试被LeetCode 27(移除元素)问懵?别再用两层 for 循环了:一道“简单题”吃透双指针原地删除,附面试加分话术 + 11 个避坑清单
后端·leetcode·面试