Day 009 — JVM 完整体系 + 调优实战

📅 2026-07-23 | 🏷️ Java · 后端方向 | ⏱️ 建议 5h | 🎯 JVM 是 Java 面试的"试金石"——深度和广度直接决定评级


📌 今日知识地图

JVM 面试全景
│
├── 模块一:运行时数据区(内存模型)
│   ├── 堆(Young/Old) / 元空间 / 虚拟机栈 / 本地方法栈 / 程序计数器
│   ├── 对象创建全流程 & 内存分配(TLAB)
│   └── 对象访问定位(句柄池 vs 直接指针)
│
├── 模块二:垃圾回收(GC)
│   ├── 判断算法:引用计数 / 可达性分析(GC Roots)
│   ├── 四种引用:强/软/弱/虚 + ReferenceQueue
│   ├── GC 算法:标记-清除 / 标记-复制 / 标记-整理
│   └── 分代收集理论
│
├── 模块三:垃圾回收器演进
│   ├── Serial / ParNew / Parallel Scavenge(年轻代)
│   ├── CMS(老年代,并发标记清除)
│   ├── G1(Region + Remember Set + SATB)
│   └── ZGC(染色指针 + 并发整理,亚毫秒停顿)
│
├── 模块四:类加载机制
│   ├── 类加载的 7 个阶段
│   ├── 双亲委派模型 & 破坏方式(SPI/Tomcat/热部署)
│   └── 类加载器:Bootstrap / Extension / Application / 自定义
│
├── 模块五:JIT 编译 & 调优实战
│   ├── JIT:C1/C2 / 逃逸分析 / 标量替换 / 栈上分配
│   ├── 常用 GC 参数 & 日志分析
│   └── OOM 排查:jmap/MAT/jstack/Arthas
│
└── 面试题精选(10 道 + 公司标签)

模块一:运行时数据区

1.1 完整内存布局

JVM 运行时数据区

┌─────────────────────────────────────────────┐
│                   堆 (Heap)                   │
│  ┌─────────────────────────────────────────┐ │
│  │           新生代 (Young Gen)              │ │
│  │  ┌──────┐ ┌──────┐ ┌──────────────┐    │ │
│  │  │ Eden │ │ S0   │ │ S1 (Survivor)│    │ │
│  │  │ 8/10 │ │ 1/10 │ │ 1/10         │    │ │
│  │  └──────┘ └──────┘ └──────────────┘    │ │
│  └─────────────────────────────────────────┘ │
│  ┌─────────────────────────────────────────┐ │
│  │           老年代 (Old Gen)               │ │
│  └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────┤
│              元空间 (Metaspace)               │
│              (JDK 8 替代永久代)               │
│         存放类元信息、方法、常量池              │
│         使用本地内存,默认无上限               │
├─────────────────────────────────────────────┤
│  线程私有区域(每个线程一份,随线程创建/销毁)  │
│  ┌──────────┐ ┌──────┐ ┌──────────────┐    │
│  │虚拟机栈   │ │程序计│ │本地方法栈     │    │
│  │(栈帧:    │ │数器  │ │(Native方法)  │    │
│  │ 局部变量表│ │(记录 │ │              │    │
│  │ 操作数栈  │ │执行位│ │              │    │
│  │ 动态链接  │ │置)   │ │              │    │
│  │ 返回地址) │ │      │ │              │    │
│  └──────────┘ └──────┘ └──────────────┘    │
└─────────────────────────────────────────────┘
区域线程存储内容异常
共享对象实例、数组OOM: Java heap space
元空间 (1.8+)共享类元信息、运行时常量池OOM: Metaspace
虚拟机栈私有栈帧(局部变量表/操作数栈/动态链接/返回地址)StackOverflowError / OOM
本地方法栈私有Native 方法的栈帧同虚拟机栈
程序计数器私有当前线程执行字节码的行号无异常(唯一无 OOM 的区域)
直接内存NIO 的 DirectByteBufferOOM(不受 -Xmx 限制!)

1.2 对象创建全流程(面试手写题!)

一个 new Person() 经历了什么:

1. 类加载检查
   → 检查常量池中是否有 Person 的符号引用 → 是否已加载/解析/初始化
   → 没有则先执行类加载

2. 分配内存
   → 计算对象大小 → 从堆中划分空间
   → 指针碰撞(Bump the Pointer):内存规整时,指针后移
   → 空闲列表(Free List):内存不规整时,从列表中找
   → 并发安全:CAS + TLAB(Thread Local Allocation Buffer)

3. 初始化零值
   → 将分配的内存空间初始化为零值(保证实例字段不赋初值也能用)

4. 设置对象头(Object Header)
   → Mark Word:哈希码、GC 分代年龄、锁状态标志、偏向线程 ID
   → 类型指针:指向方法区中类的元数据

5. 执行 <init> 方法
   → 按代码中赋值顺序初始化实例字段
   → 执行构造器中的代码
// 对象头结构(64 位 JVM)
// Mark Word (64 bits):
//   - 无锁状态:unused(25) | hashcode(31) | unused(1) | age(4) | biased_lock(1) | lock(2)
//   - 偏向锁:  thread_id(54) | epoch(2) | unused(1) | age(4) | biased_lock(1) | lock(2)
//   - 轻量级锁:ptr_to_lock_record(62) | lock(2)
//   - 重量级锁:ptr_to_monitor(62) | lock(2)
//   - GC 标记:  forward_ptr(62) | lock(2)

// 类型指针 (Klass Pointer):默认 64 bits,开启 -XX:+UseCompressedClassPointers 后 32 bits

1.3 对象访问定位

句柄池访问:
  reference → 句柄池 → [实例数据指针 | 类型数据指针]
                       → 堆中的实例数据
                       → 方法区中的类型数据
  优点:对象移动时只需改句柄池,reference 不变
  缺点:多一次指针跳转

直接指针访问(HotSpot 采用):
  reference → 对象 → [对象头 | 实例数据] + 类型指针 → 方法区类型数据
  优点:一次指针跳转,快
  缺点:对象移动时需要更新 reference(GC 时处理)

1.4 栈帧结构

一个方法调用 = 一个栈帧入栈,方法返回 = 栈帧出栈

栈帧结构:
┌──────────────────┐
│   局部变量表       │ ← 方法参数 + 局部变量,基本类型存值,引用存指针
│  (Local Vars)    │   槽位可复用(slot),32 位一个槽,long/double 占两个
├──────────────────┤
│   操作数栈         │ ← JVM 基于栈的指令集,每次 push/pop 操作
│  (Operand Stack) │
├──────────────────┤
│   动态链接         │ ← 指向运行时常量池中该方法的符号引用
│  (Dynamic Link)  │
├──────────────────┤
│   返回地址         │ ← 调用者的 PC 值(方法正常返回 / 异常返回)
│  (Return Address)│
└──────────────────┘

模块二:垃圾回收(GC)

2.1 判断对象存活

// ═══════════════════════════════════════
// 引用计数法
// ═══════════════════════════════════════
// 每个对象维护引用计数,为 0 则回收
// 缺点:解决不了循环引用(A→B, B→A,计数都不为 0)
// 早期 Python / Redis 用引用计数 + 标记清除补充

// ═══════════════════════════════════════
// 可达性分析(Java/JVM 采用)
// ═══════════════════════════════════════
// 从 GC Roots 出发,沿引用链标记可达对象,不可达的回收

// GC Roots 包括:
// ① 虚拟机栈中引用的对象(局部变量表中的引用)
// ② 方法区中静态属性引用的对象
// ③ 方法区中常量引用的对象
// ④ 本地方法栈中 JNI 引用的对象
// ⑤ 被 synchronized 持有的对象
// ⑥ JVM 内部的引用(Class 对象、常驻异常对象等)

2.2 四种引用

引用类型回收时机用途
强引用 (Strong)永不回收(除非引用断开)Object o = new Object()
软引用 (Soft)内存不够时回收(OOM 之前)图片缓存、网页缓存
弱引用 (Weak)下一次 GC 必定回收ThreadLocal 的 Entry、WeakHashMap
虚引用 (Phantom)任何时候都可能回收对象被回收前收到通知(管理直接内存)
// 软引用:内存不够才回收
SoftReference<byte[]> softRef = new SoftReference<>(new byte[100 * 1024 * 1024]);
byte[] data = softRef.get();  // 内存充足 → 返回数据;内存不足 → 返回 null

// 弱引用:GC 必回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc();
Object obj = weakRef.get();  // null — GC 后必为 null

// 虚引用:get() 永远返回 null,需要配合 ReferenceQueue 使用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);
// 对象被回收时,虚引用被加入 queue → 收到通知 → 清理资源

2.3 GC 算法

算法过程优点缺点适用
标记-清除标记可达→清除不可达简单碎片多老年代(CMS 的基础)
标记-复制分两块,存活对象从一块复制到另一块,清空原块无碎片浪费一半空间新生代
标记-整理标记可达→存活对象移到一端→清理边界外无碎片耗时(移动对象)老年代

2.4 分代收集理论

分代假设(两个):
  1. 弱分代假说:绝大多数对象朝生夕死 → 新生代高频 GC(Minor GC)
  2. 强分代假说:熬过越多次 GC 的对象越难死 → 老年代低频 GC(Major/Full GC)

新生代分配与晋升:
  新对象 → Eden
  Eden 满了 → Minor GC → 存活对象 → S0
  下次 Minor GC → Eden + S0 存活对象 → S1(S0 和 S1 交替使用)
  每熬过一次 Minor GC → 年龄 +1 → 年龄达到阈值(默认 15)→ 晋升老年代

跨代引用怎么办?
  → 老年代引用新生代对象
  → Remembered Set:老年代维护一个"卡表"(Card Table)
  → 新生代 GC 时只需扫描"脏卡"对应的老年代区域,不需要扫整个老年代

模块三:垃圾回收器演进

3.1 七种回收器速览

新生代回收器:             老年代回收器:
  Serial        ───→  Serial Old(单线程,标记-整理)
  ParNew        ───→  CMS(并发标记清除)
  Parallel Scavenge ─→  Parallel Old(吞吐量优先)

全区域回收器:
  G1(Region 分代 + 并发整理)
  ZGC(染色指针 + 全并发,亚毫秒)
  Shenandoah(全并发,无分代)

连线规则(经典组合):
  Serial + Serial Old          ← 单线程,Client 模式
  ParNew + CMS                 ← 经典组合,低延迟
  Parallel Scavenge + Parallel Old  ← 吞吐量优先
  G1                           ← JDK 9+ 默认
  ZGC                          ← JDK 15+ 生产可用,大内存低延迟

3.2 各回收器核心特点

CMS(Concurrent Mark Sweep)— 低延迟经典

CMS 四个阶段:
  1. 初始标记(STW,极短):只标记 GC Roots 直接引用的对象
  2. 并发标记(无 STW):沿着引用链标记,与用户线程并发
  3. 重新标记(STW,较短):修正并发标记期间的变动(三色标记 + SATB)
  4. 并发清除(无 STW):清除未标记的对象

CMS 三大问题(面试必问!):
  ① 并发时用户线程还在产生垃圾 → "浮动垃圾" → 需要预留空间
  ② 标记-清除 → 碎片 → 碎片太多导致 Concurrent Mode Failure → 退化为 Serial Old
  ③ 对 CPU 敏感(并发阶段占用线程资源)

G1(Garbage First)— JDK 9+ 默认

G1 核心概念:
  Region:把堆划分为 2048 个大小相等的 Region(每个 1~32MB)
  每个 Region 可以是 Eden / Survivor / Old / Humongous

G1 的混合回收(Mixed GC):
  不只是回收新生代,还回收部分老年代 Region
  "Garbage First" = 优先回收垃圾最多的 Region

Remembered Set(RSet):
  每个 Region 维护 RSet → 记录"谁引用了我"
  避免了全堆扫描

SATB(Snapshot-At-The-Beginning):
  在并发标记开始时拍快照 → 标记期间新产生的对象默认存活
G1 GC 周期:

Young GC(只回收新生代 Region):
  → STW,复制存活对象到 Survivor/Old Region
  → 常规操作

Mixed GC(回收新生代 + 部分老年代):
  1. 初始标记(STW,跟 Young GC 一起做)
  2. 根区域扫描(并发)
  3. 并发标记(无 STW)
  4. 重新标记(STW)
  5. 清理(STW,筛选回收价值最高的 Region)
  6. 复制(STW,将选定 Region 的存活对象移到空闲 Region)

Full GC(万不得已):
  → Mixed GC 跟不上分配速度 → STW 的标记-整理 → 慢!
  → 调优目标:避免 Full GC

ZGC — 亚毫秒级停顿

ZGC 核心创新:染色指针(Colored Pointers)

在 64 位指针中嵌入元数据(前 4 位):
  - Finalizable:对象只能被 Finalizer 访问
  - Remapped:指向对象的最新地址
  - Marked 0/1:标记位

技术特点:
  → 并发整理:移动对象时用"读屏障"自愈指针
  → 全阶段并发:初始标记和重新标记也是并发的(仅极短 STW)
  → STW < 1ms(不管堆多大)
  → JDK 15 生产可用,JDK 21 支持分代 ZGC

3.3 回收器选型指南

场景推荐回收器理由
堆 < 4G,低延迟ParNew + CMS经典方案,但 CMS 已废弃(JDK 14)
堆 4G ~ 32G,平衡G1JDK 9+ 默认,最通用的选择
堆 > 32G,极低延迟ZGC亚毫秒停顿,大堆首选
吞吐量优先Parallel Scavenge + Parallel Old后台计算任务
单核 / 小内存Serial + Serial OldClient 模式

模块四:类加载机制

4.1 类加载的 7 个阶段

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

① 加载:通过全限定名获取二进制字节流 → 转为方法区数据结构 → 生成 Class 对象
② 验证:文件格式验证 / 元数据验证 / 字节码验证 / 符号引用验证
③ 准备:为静态变量分配内存并赋零值(不包含代码中的赋值)
       例外:static final 常量在准备阶段就赋代码中的值
④ 解析:将常量池中的符号引用替换为直接引用
⑤ 初始化:执行类构造器 <clinit>() 方法
         包含:静态变量赋值语句 + static {} 代码块
         顺序:按代码出现顺序,父类先于子类
// 准备 vs 初始化的区别
class Demo {
    static int a = 10;         // 准备阶段:a = 0;初始化阶段:a = 10
    static final int b = 20;   // 准备阶段:b = 20(常量,编译期就确定了)
    static { /* 初始化代码 */ }
}

4.2 双亲委派模型

双亲委派:一个类加载器收到类加载请求时,先委托给父加载器,父加载器加载不了才自己加载。

目的:
  → 保证 Java 核心类的安全:不管谁加载,String 始终是 Bootstrap ClassLoader 的那个 String
  → 保证同一个类不被重复加载

三层类加载器:
  Bootstrap ClassLoader(启动类加载器)
    ↑ 加载 <JAVA_HOME>/lib 下的核心类(rt.jar, modules)
    ↑ 用 C++ 实现,Java 中返回 null
  Extension / Platform ClassLoader(扩展/平台类加载器)
    ↑ 加载 <JAVA_HOME>/lib/ext 或 java.ext.dirs
  Application ClassLoader(应用类加载器)
    ↑ 加载 classpath 下的类
  自定义 ClassLoader

4.3 破坏双亲委派的三种情况

// ═══════════════════════════════════════
// 破坏方式一:SPI(Service Provider Interface)
// ═══════════════════════════════════════
// 问题:核心类(如 java.sql.DriverManager)由 Bootstrap 加载
//       但它的实现类(如 com.mysql.jdbc.Driver)在 classpath 中
//       Bootstrap 加载不到 → 需要子加载器来加载
// 解决:线程上下文类加载器(Thread Context ClassLoader)
//       DriverManager 通过 TCCL 获取 App ClassLoader 来加载实现类

// ═══════════════════════════════════════
// 破坏方式二:Tomcat 类加载
// ═══════════════════════════════════════
// 需求:① 不同 Web 应用隔离(各用各的 lib)
//       ② 共享公共库(Servlet API 等)
//       ③ 支持热部署(替换应用而不用重启 Tomcat)
// Tomcat 方案:每个 Web 应用有独立的 WebAppClassLoader
//   → 优先自己加载 → 自己加载不到才委托给父加载器

// ═══════════════════════════════════════
// 破坏方式三:热部署 / OSGi
// ═══════════════════════════════════════
// 每个模块有自己的 ClassLoader → 卸载模块时直接丢弃其 ClassLoader

模块五:JIT 编译 & 调优实战

5.1 JIT 编译器

// JIT (Just-In-Time) 编译器:将热点代码编译为本地机器码

// 两种编译器:
// C1(Client Compiler):编译快,优化少 → 适合 GUI 应用
// C2(Server Compiler):编译慢,优化激进 → 适合长时间运行的服务端

// 分层编译(Tiered Compilation,JDK 8+ 默认开启):
// Level 0:解释执行
// Level 1:C1 编译,无 profiling
// Level 2:C1 编译,带简单 profiling
// Level 3:C1 编译,带完整 profiling
// Level 4:C2 编译,基于 profiling 数据做激进优化

// 热点代码检测:
// 方法调用计数器:方法被调用超过阈值 → JIT 编译
// 回边计数器:循环执行次数超过阈值 → OSR (On Stack Replacement)

5.2 逃逸分析

// 逃逸分析:分析对象的作用范围
// 对象不逃逸出方法 → 可以栈上分配、标量替换、锁消除

public String createString() {
    StringBuilder sb = new StringBuilder();  // sb 不逃逸出方法
    sb.append("hello");
    return sb.toString();
    // 优化:sb 可以直接在栈上分配(不占堆内存),甚至标量替换(拆成字段在寄存器中操作)
}

// 三种优化:
// ① 栈上分配:对象在栈帧中分配,方法结束自动释放,无需 GC
// ② 标量替换:将对象拆解为其成员字段,直接在 CPU 寄存器/栈上操作
// ③ 同步消除:不逃逸的对象上的锁操作可以被消除

5.3 常用 GC 参数

# ═══════════════════════════════════════
# 堆大小
# ═══════════════════════════════════════
-Xms2g              # 初始堆大小
-Xmx4g              # 最大堆大小(建议跟 -Xms 相同,避免堆伸缩)
-Xmn1g              # 新生代大小
-XX:NewRatio=2      # 老年代/新生代比例 = 2
-XX:SurvivorRatio=8 # Eden/Survivor 比例 = 8

# ═══════════════════════════════════════
# 元空间
# ═══════════════════════════════════════
-XX:MetaspaceSize=128m       # 元空间初始大小
-XX:MaxMetaspaceSize=256m    # 元空间最大大小

# ═══════════════════════════════════════
# GC 选择
# ═══════════════════════════════════════
-XX:+UseG1GC                  # 使用 G1
-XX:+UseZGC                   # 使用 ZGC(JDK 15+)
-XX:MaxGCPauseMillis=200      # G1 目标停顿时间(ms)

# ═══════════════════════════════════════
# GC 日志(JDK 9+ 统一日志格式)
# ═══════════════════════════════════════
-Xlog:gc*:file=gc.log:time,tags,level:filecount=10,filesize=100M

# ═══════════════════════════════════════
# 内存溢出 dump
# ═══════════════════════════════════════
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

# ═══════════════════════════════════════
# 其他常用
# ═══════════════════════════════════════
-XX:+PrintGCDetails           # 打印 GC 详情(JDK 8)
-XX:+UseCompressedOops        # 压缩普通对象指针
-XX:+UseCompressedClassPointers # 压缩类指针

5.4 OOM 排查实战

# ═══════════════════════════════════════
# 第一步:拿到 heap dump
# ═══════════════════════════════════════
# 方式一:启动时配置自动 dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof

# 方式二:手动 dump
jmap -dump:live,format=b,file=dump.hprof <pid>

# ═══════════════════════════════════════
# 第二步:MAT 分析
# ═══════════════════════════════════════
# 1. 打开 hprof 文件 → Leak Suspects Report
# 2. 看 Dominator Tree:哪个类占最多内存
# 3. 看 GC Roots 路径:这个对象为什么没被回收
# 4. 常见结论:
#    - HashMap 无限增长 → 内存泄漏
#    - ThreadLocal 没 remove → 内存泄漏
#    - 大对象(byte[])太多 → 合理还是异常
#    - 类加载器太多 → Metaspace OOM

# ═══════════════════════════════════════
# 第三步:线程排查
# ═══════════════════════════════════════
jstack <pid>                    # 查看线程栈
jstack <pid> | grep "BLOCKED"   # 找死锁
top -H -p <pid>                 # 找 CPU 最高的线程

# printf '%x\n' <tid> → 转十六进制 → 在 jstack 中搜索 nid=0x<tid>

# ═══════════════════════════════════════
# 第四步:Arthas 在线诊断(神器!)
# ═══════════════════════════════════════
dashboard              # 实时监控
thread                 # 线程状态
jad com.example.Foo    # 反编译类
watch com.example.Foo bar '{params, returnObj}'  # 观测方法调用
trace com.example.Foo bar  # 方法调用路径 + 耗时

5.5 四大 OOM 场景

OOM 类型信息常见原因排查
Java heap space堆内存不足内存泄漏/大对象/配置太小jmap + MAT
Metaspace元空间不足动态生成大量类/配置太小增大 MaxMetaspaceSize
GC overhead limit exceededGC 时间超过 98% 但回收不到 2%堆太小/内存泄漏增大堆或排查泄漏
Direct buffer memory直接内存不足NIO 未释放 ByteBuffer排查 DirectByteBuffer

面试题精选(10 道)

Q1. JVM 运行时数据区有哪些?(字节/阿里/美团 最高频)

标准回答

线程共享:堆(对象实例)、元空间(类元信息、常量池,JDK 8 替代永久代)。

线程私有:虚拟机栈(栈帧包含局部变量表/操作数栈/动态链接/返回地址)、本地方法栈(Native 方法)、程序计数器(记录当前执行指令地址,唯一不抛 OOM 的区域)。

直接内存不在 JVM 内存模型内,但受 -XX:MaxDirectMemorySize 控制,NIO 使用。

Q2. 对象创建的全过程?(字节/阿里 高频)

标准回答

① 类加载检查 → ② 分配内存(指针碰撞/空闲列表,CAS+TLAB 保证并发安全)→ ③ 初始化零值 → ④ 设置对象头(Mark Word + 类型指针)→ ⑤ 执行 <init> 方法(实例字段初始化 + 构造器)。

关键细节:TLAB 为每个线程在 Eden 区预留一小块内存,分配时无需 CAS → 减少竞争。

Q3. 什么是 GC Roots?(阿里/腾讯)

标准回答

可达性分析的起点,包括:虚拟机栈中局部变量表引用的对象、方法区静态变量引用的对象、常量引用的对象、JNI 引用的对象、synchronized 持有的对象、JVM 内部引用(Class 对象等)。从这些根出发遍历引用链,不可达的对象判定为可回收。

Q4. 四种引用的区别和使用场景?(字节/美团)

标准回答

强引用永不被回收。软引用在 OOM 前回收(适合缓存)。弱引用在一次 GC 后必回收(ThreadLocal 的 Entry 用弱引用持有 key,防止 key 无法回收)。虚引用 get() 永远返回 null,配合 ReferenceQueue 在对象回收后收到通知(管理直接内存的释放,Cleaner 类)。

Q5. CMS 和 G1 的区别?(阿里/字节 高频)

标准回答

CMS 是分代回收器(新生代 ParNew + 老年代 CMS),G1 是全区域回收器(Region 分代)。CMS 用标记-清除(有碎片),G1 用标记-复制-整理(无碎片)。CMS 的并发标记用增量更新,G1 用 SATB。CMS 只回收老年代,G1 做 Mixed GC(回收部分老年代 Region)。G1 可预测停顿(-XX:MaxGCPauseMillis),CMS 不可预测。G1 适合 4G+ 堆,CMS 适合小堆低延迟。

Q6. G1 的 Mixed GC 和 Full GC 的区别?(字节/阿里)

标准回答

Mixed GC 回收新生代 + 部分老年代 Region(根据暂停时间目标选择回收价值最高的 Region),大部分阶段并发执行,STW 时间可控。Full GC 是单线程的标记-整理(STW),当 Mixed GC 跟不上分配速度时触发,停顿时间长,调优目标是避免 Full GC。

Q7. 类加载的双亲委派模型?为什么要破坏它?(阿里/腾讯 高频)

标准回答

一个类加载器收到加载请求后先委托给父加载器,父加载器加载不了才自己加载。保证 Java 核心类的安全(同一个 String 类不会出现多个版本)。

三种破坏:① SPI 机制——核心类(Bootstrap 加载)需要调用应用层的实现类,通过线程上下文类加载器(TCCL)回调;② Tomcat——每个 Web 应用独立 ClassLoader,优先自己加载而非委托父类,实现应用隔离和热部署;③ OSGi 热部署——模块化加载,卸载模块时直接丢弃 ClassLoader。

Q8. ZGC 为什么能做到亚毫秒停顿?(腾讯/字节)

标准回答

ZGC 的核心技术是染色指针(Colored Pointers)和读屏障。在 64 位指针中嵌入 GC 状态信息(Remapped/Marked0/Marked1),并发移动对象时通过读屏障自愈指针——访问对象时如果发现指针状态是旧地址,读屏障自动修正为新地址。所有 GC 阶段(标记、整理、重映射)都和用户线程并发,STW 只在极短的根扫描阶段,因此停顿不受堆大小影响。

Q9. 逃逸分析是什么?做了哪些优化?(阿里/美团)

标准回答

逃逸分析判断对象的作用范围是否超出方法/线程。不逃逸的对象可以做:① 栈上分配(对象在栈帧中分配,方法结束自动释放);② 标量替换(将对象拆为成员字段分散存储,避免分配对象);③ 同步消除(不逃逸对象上的锁操作可以消除)。JDK 8+ 默认开启逃逸分析,可以通过 -XX:+DoEscapeAnalysis 控制。

Q10. 如何排查 OOM?用过什么工具?(美团/字节)

标准回答

四步法:① 获取 heap dump(-XX:+HeapDumpOnOutOfMemoryError 或 jmap);② MAT 分析——看 Dominator Tree(谁占最多)、GC Roots 路径(为什么没回收);③ jstack 看线程栈(是否有死锁、大量线程 BLOCKED);④ Arthas 在线诊断——实时 dashboard 看内存/GC/线程,trace 追踪具体方法调用。

常见原因:HashMap 无限增长(没有设置过期/上限)、ThreadLocal 没 remove(线程池复用导致)、大对象(byte[])积累、类加载器泄漏(Metaspace OOM)。


📊 今日知识图谱

JVM 完整体系 DAY 9
│
├── 运行时数据区
│   ├── 共享:堆(Young+Old) + 元空间(类信息/常量池)
│   ├── 私有:栈(栈帧) + 本地方法栈 + 程序计数器
│   └── 对象创建:类检查→分配内存(CAS+TLAB)→零值→对象头→init
│
├── 垃圾回收
│   ├── 判断:可达性分析(GC Roots)
│   ├── 引用:强(不回收)/软(OOM前)/弱(GC必收)/虚(通知)
│   ├── 算法:标记-清除(碎片)/复制(无碎片)/整理(无碎片+耗时)
│   └── 分代:新生代(Eden+S0/S1复制) + 老年代 + 卡表
│
├── 回收器演进
│   ├── Serial(单线程) → ParNew(多线程) → Parallel(吞吐量)
│   ├── CMS(并发清除,碎片,退化为SerialOld,JDK14废弃)
│   ├── G1(Region,RSet,SATB,MixedGC,预测停顿,JDK9+默认)
│   └── ZGC(染色指针,读屏障,全并发,亚毫秒停顿,JDK15+)
│
├── 类加载
│   ├── 7阶段:加载→验证→准备→解析→初始化→使用→卸载
│   ├── 双亲委派:自底向上委托,自顶向下加载
│   └── 破坏:SPI(TCCL) / Tomcat(隔离+热部署) / OSGi
│
└── 调优实战
    ├── JIT:C1/C2分层编译 / 逃逸分析(栈上分配/标量替换/锁消除)
    ├── 参数:-Xms/Xmx/Xmn/UseG1GC/MaxGCPauseMillis
    └── 排查:jmap(heap dump) → MAT → jstack → Arthas

🔜 明日预告

Day 10 — Java 并发编程完整体系

  • 线程生命周期 + synchronized 底层(对象头/锁升级)
  • volatile(可见性/禁止重排/不保证原子性)
  • AQS 完整拆解(state + CLH 队列 + acquire/release)
  • ReentrantLock(公平/非公平)+ Condition
  • 线程池 7 参数 + 4 种拒绝策略 + 执行流程
  • ThreadLocal(ThreadLocalMap/WeakReference/内存泄漏)
  • JUC 工具:CountDownLatch/CyclicBarrier/Semaphore/CompletableFuture
  • ConcurrentHashMap 1.8 源码深度解读
  • 10 道并发高频真题

💡 速通心法:JVM 面试三条主线——① 内存(运行时数据区每块放什么、OOM 怎么查);② GC(从 Serial 到 ZGC 的演进逻辑、G1 的 Region+RSet+SATB);③ 类加载(双亲委派 + 三种破坏场景)。这三条线能串起来讲 30 分钟不卡壳,JVM 面试就稳了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程星辰海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值