大厂真题解析
定位: 真实面试题的全链路拆解——每一道题都展示如何串联 JVM 的不同知识点 使用方式: 先自己回答,再对比参考回答。关注"原理链"——每一问如何从 A 连到 B 再到 C
知识地图:JVM 知识体系全景
┌───────────────────────────────────────────────────────────────────────┐
│ JVM 知识体系全景 │
│ │
│ ┌──────────┐ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
│ │ 01 字节码 │→ │ 02 类加载 │→ │ 03 运行时 │→ │ 04 对象分配与 │ │
│ │ .class │ │ 加载/验证 │ │ 数据区 │ │ 内存布局 │ │
│ │ 结构 │ │ 双亲委派 │ │ 堆/栈/方法区│ │ TLAB/MarkWord │ │
│ │ javap │ │ 自定义CL │ │ 直接内存 │ │ 逃逸分析 │ │
│ └──────────┘ └────────────┘ └────────────┘ └────────┬─────────┘ │
│ │ │ │ │ │
│ │ │ │ ▼ │
│ │ │ │ ┌──────────────────┐ │
│ │ │ └─────────→│ 05 GC 机制 │ │
│ │ │ │ 可达性分析 │ │
│ │ │ │ 收集器 ZGC/代 │ │
│ │ │ └────────┬─────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────────────────────────────────────┐ │
│ └────────→│ 06 JVM 调优与排查 │ │
│ │ OOM/CPU/FullGC排查 + 工具 (jmat/jstack) │ │
│ └────────────────────┬────────────────────┬────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌────────────────────────┐ │
│ │ 07 JIT 执行引擎 │ │ 08 JMM 与并发 │ │
│ │ C1/C2/分层编译 │ │ volatile/synchronized │ │
│ │ 内联/逃逸分析 │ │ happens-before/锁升级 │ │
│ └──────────────────┘ └────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 09 前沿与新特性 │ │
│ │ ZGC 亚毫秒停顿 / 虚拟线程 M:N / Valhalla 值类型│ │
│ └──────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
面试最高频的路径: new对象 → 堆分配 → GC回收 → 调优
面试最难回答路径: JMM 线程通信 → JIT 指令重排 → 锁升级真题 1:"从 new 一个对象开始,讲一下 JVM 内部发生了什么"
面试公司: 阿里、字节跳动、美团(高频)
考察点: 知识串联能力。能否从类加载→内存分配→对象布局→初始化→GC 整个链路讲清楚
参考回答(PRINCIPLE CHAIN):
1. 类加载检查
new Object() → 字节码指令 → 检查该类是否已加载/解析/初始化
如未加载:启动类加载过程(双亲委派 → 加载 → 连接 → 初始化)
→ 详阅 [[02.1-类加载生命周期]]
2. 分配内存
类加载完成后 → 确定对象所需内存大小
指针碰撞(规整堆)或 空闲列表(不规整堆)
优先 TLAB 分配(线程本地缓冲区,免同步)
如 TLAB 不足 → Eden 区分配
→ 详阅 [[04.1-对象创建过程]]、[[04.4-分配策略(栈上分配与TLAB与大对象)]]
3. 内存空间清零
分配后内存填充零值(保证字段可直接使用而不报错)
注意:TLAB 分配时已经零值初始化
4. 设置对象头
MarkWord: 锁状态(初始无锁)、GC 分代年龄(0)、identity hashcode(懒计算)
KlassPointer: 指向类元数据的指针(用于方法调用时的类型确定)
如果数组,还有数组长度
→ 详阅 [[04.2-对象内存布局(MarkWord与Klass与实例数据)]]
5. 执行 <init> 方法
栈帧压入 → 调用父类构造 → 初始化字段 → 执行构造体
对象引用赋值(注意:这里可能发生"对象未完全初始化"的并发问题)
6. 后续使用与 GC
对象进入使用阶段 → 被引用 → 收集到 GC Roots 可达性分析
通过 GC 年龄达到阈值 → 从新生代晋升到老年代
最终被回收 → finalize() → 空间复用
→ 详阅 [[05.1-对象存活判定算法]]追问预判:
问: 对象分配时堆不规整,用空闲列表还是指针碰撞取决于什么?
- 答: GC 收集器是否带整理功能(Serial/ParNew 带复制整理 → 指指针碰撞;CMS 标记清除 → 空间不规整 → 空闲列表)
问: TLAB 满了之后怎么分配?
- 答: 直接在 Eden 区分配(需同步)。如果 Eden 也不够 → Minor GC(Allocation Failure)。如果超过 PretenureSizeThreshold → 直接进老年代
问: volatile 在对象创建中有什么作用?
- 答: 单例 DCL 模式中,instance = new Singleton() 非原子——分配+初始化+赋值可能重排 → 其他线程拿到未初始化的对象。加 volatile 禁止重排
真题 2:"说下你对 JVM 内存模型的理解"
面试公司: 字节跳动、腾讯、快手(鉴别"是否真懂"的经典题)
考察点: 区分"运行时数据区(内存结构)"和"JMM(并发语义)"两个概念
参考回答(PRINCIPLE CHAIN):
首先需要澄清:JVM "内存模型"有两个含义,面试官通常希望听到两者都覆盖:
一、运行时数据区(Runtime Data Area)—— 内存的"物理"划分
堆(Heap):所有线程共享,存对象实例,GC 管理
虚拟机栈(Stack):线程私有,存栈帧(局部变量表、操作数栈)
方法区(Metaspace):类信息、常量、静态变量
程序计数器:线程私有,指向当前执行的字节码行号
本地方法栈:native 方法
关键:堆是所有线程共享的——这是 JMM 需要存在的原因
→ 详阅 [[03.4-Java堆]]、[[03.2-Java虚拟机栈]]、[[03.5-方法区与元空间]]
二、JMM(Java Memory Model)—— 线程间通信的契约
解决的问题:CPU 缓存 + 指令重排导致的多线程内存操作不可见
核心规则:happens-before 原则
- volatile 变量的写 happens-before 读
- synchronized 解锁 happens-before 加锁
- Thread.start() 之前的操作 happens-before 新线程看到
...
保证了三种性质:
- 原子性(Atomicity):synchronized、CAS、锁
- 可见性(Visibility):volatile、synchronized、final
- 有序性(Ordering):volatile(内存屏障)、happens-before
注意区别:运行时数据区 = 内存结构(静态的),JMM = 并发规则(动态的)
→ 详阅 [[08.1-JMM与happens-before]]追问预判:
问: 既然堆是共享的,为什么还有可见性问题?
- 答: 线程在 CPU 上运行,从主内存(RAM)读数据到缓存(L1/L2)后再操作——不是每次都从堆读。volatile 强制从主内存读/写
问: happens-before 和内存屏障的关系?
- 答: happens-before 是 JMM 的抽象语义,内存屏障是底层实现。JVM 根据 happens-before 规则插入屏障指令,CPU 根据屏障禁止重排
真题 3:"GC 调优做过吗?怎么做的?"
面试公司: 阿里、字节跳动、京东(最高频实战题)
考察点: 是否真的经历过线上 GC 问题,还是只背参数
参考回答(PRINCIPLE CHAIN):
GC 调优是一个系统性的过程,不是一上来就调参数。
第一步:确定目标
不是什么场景都需要 P99 < 10ms。先明确业务要求:
- 批处理/离线计算 → 吞吐量优先(目标:GC 时间 < 总时间的 1-5%)
- 在线交易/API 网关 → 延迟优先(目标:P99 GC < 10ms 甚至 < 2ms)
- 流媒体/实时系统 → 最坏情况停顿(目标:任何 GC 都不超过 50ms)
第二步:收集现状
jstat -gcutil pid 5000 50 → 看 Young GC 频率、Full GC 频率
GC 日志(-Xlog:gc*)→ 每个 GC 事件的耗时和堆变化
堆使用趋势(JVisualVM / Grafana + Prometheus + JMX Exporter)
→ 详阅 [[06.2-常用工具(jps与jstat与jmap与jstack与jinfo)]]
第三步:分析问题模式
模式 A:Young GC 过于频繁(每秒多次)
原因:Eden 太小 / 对象创建速率太大
行动:调大 -Xmn 或检查是否需要减少对象创建
模式 B:Full GC 频繁
原因:老年代空间被占满(内存泄漏或堆太小)/ Metaspace 扩容
行动:jmap dump → MAT 找 GC Root 链 → 修复/增大堆
→ 详阅 [[06.6-FullGC频繁排查方法论]]
模式 C:单次 GC 停顿过长
原因:堆太大 + G1 重定位慢 / CMS 并发失败
行动:换 ZGC(JDK 17+)或调整 G1 的 -XX:MaxGCPauseMillis
→ 详阅 [[05.5-GC调优参数速查]]
第四步:验证与监控
调整后观察至少一周(确保不出现偶发性 Full GC)
关键指标:GC 频率下降、停顿减少、应用 P99 延迟降低
生产常用配置模板:
java -Xms8g -Xmx8g \
-XX:+UseZGC \
-Xlog:gc*:file=gc.log:time,level,tags:filecount=5,filesize=100m \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/app/dumps/追问预判:
问: 堆设置 8GB,老年代 6GB,YGC 一次 100ms,这个正常吗?
- 答: 取决于收集器。G1 下 YGC 100ms 在 8GB 堆上偏高(通常在 20-50ms)。检查是否 Mixed GC 或巨型对象占用了大量 Region
问: 怎么判断是"内存泄漏"还是"堆不够大"?
- 答: 看 GC 后的堆用量基线。连续几次 Full GC 后堆基数不降 → 泄漏;降回基线 → 堆不够
真题 4:"CMS 和 G1 的区别?什么时候用哪个?"
面试公司: 美团、拼多多、滴滴(中高频选型题)
考察点: 对收集器本质差异的理解,以及场景判断
参考回答(PRINCIPLE CHAIN):
核心区别在于设计哲学:
1. 堆结构
CMS:分代连续空间(新生代 Eden/S0/S1 + 老年代连续空间)
G1:分区 Region(1-32MB 的独立区域,逻辑分代但物理不连续)
2. 回收算法
CMS:新生代(标记-复制)+ 老年代(标记-清除)
G1:新生代(标记-复制)+ 整个堆(标记-整理)
3. 关键机制
CMS:增量更新 + 并发清除。缺陷是标记-清除产生碎片,并发失败需 STW
G1:SATB(快照) + RSet(记录跨代引用)。可预测停顿,无碎片
4. STW 控制
CMS:无法精确控制——停顿时间取决于堆大小
G1:通过 -XX:MaxGCPauseMillis 设定目标(默认 200ms),选择"性价比最高"的 Region 集合
选型建议:
- JDK 8 老项目,堆 <8GB,低延迟:可用 CMS(但已废弃,尽快迁移)
- JDK 9+,4-64GB 堆:G1 默认,优先选
- >64GB 或 P50 < 5ms 要求:建议 ZGC
→ 详阅 [[05.3-垃圾收集器详解(Serial到ZGC)]]追问预判:
问: G1 的 RSet 会不会很大?
- 答: 会。大堆 + 多跨代引用 → RSet 可能占 5-20% 堆空间。ZGC 没有 RSet(彩色指针替代),这是 ZGC 的优势之一
问: CMS 被废弃后,为什么很多公司还在用?
- 答: 大部分是因为 JDK 8 未升级 + 对迁移风险的不确定性。CMS 在 JDK 14 已正式移除
真题 5:"volatile 和 synchronized 的区别?底层实现?"
面试公司: 腾讯、京东、字节(极其高频)
考察点: JMM 深度理解 + 锁升级机制
参考回答(PRINCIPLE CHAIN):
一、语义层面
volatile:
可见性(写刷主存,读从主存取)
有序性(禁止指令重排 → 插入内存屏障)
不保证原子性(count++ 不安全)
synchronized:
可见性(解锁释放到主存)
有序性(同一时刻只有一个线程执行)
原子性(互斥执行)
→ 详阅 [[08.1-JMM与happens-before]]
二、底层实现
volatile:
写操作:store barrier → write ← store barrier
读操作:load barrier → read ← load barrier
JMM 层面保证 LoadLoad/StoreStore 等屏障禁止重排
synchronized:
字节码:monitorenter / monitorexit
MarkWord 锁状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁升级是单向的(降级只在 STW 的 GC 时发生)
→ 详阅 [[04.2-对象内存布局(MarkWord与Klass与实例数据)]]
三、使用选择
单个共享变量可见性 → volatile
复合操作原子性 + 临界区保护 → synchronized / Lock
追求性能并且是短操作 → 偏向锁 / 轻量级锁
高竞争 → ReentrantLock(避免重量级锁的 park/unpark 开销)追问预判:
问: JDK 15+ 为什么默认关闭了偏向锁?
- 答: 偏向锁的撤销需要全局 STW 安全点(因为需要检查持有偏向锁的线程是否还活着)。在 JVM 内部同步(如 class 加载)中偏向锁撤销是性能热点。JDK 15+ 默认关闭偏向锁,获得性能提升
问: volatile 数组是否保证元素可见性?
- 答: 不保证。volatile int[] arr — arr 引用是 volatile,但 arr[0] 的读写不是 volatile。需要用 AtomicIntegerArray 或 Unsafe 的 loadFence/storeFence
真题 6:"说下锁升级的过程"
面试公司: 阿里、快手、拼多多(JDK 8 时代高频题,JDK 15+ 有调整)
考察点: MarkWord 的状态变化 + 每种锁的运作机制
参考回答(PRINCIPLE CHAIN):
Java 锁的四种状态(从无锁到重量级):
1. 无锁(No Lock)
MarkWord 低 2 位 = 01 无锁状态
存储:identity hashcode(懒计算)、GC 年龄
2. 偏向锁(Biased Locking)[JDK 15+ 默认关闭]
MarkWord 存获得锁的线程 ID
场景:单线程反复获取同一把锁(如 Vector/Hashtable)
获取:CAS 设置线程 ID → 成功则直接进入
撤销:遇到其他线程竞争 → 在全局安全点撤销 → 升级为轻量锁
问题:撤销需要 STW(即使是短暂的)→ 高并发时性能反而不好
3. 轻量级锁(Lightweight Locking)
MarkWord 指向线程栈中的 Lock Record
获取:CAS 尝试替换 MarkWord 为指向 Lock Record 的指针
场景:少量线程交替获取锁(不阻塞)
失败:自旋(循环 CAS)→ 自旋一定次数仍失败 → 升级
4. 重量级锁(Heavyweight Locking / Monitor)
MarkWord 指向 Mutex(OS 互斥量)
获取失败 → 进入等待队列(park/unpark → OS 线程挂起/恢复)
场景:多线程高烈度竞争
→ 详阅 [[04.2-对象内存布局(MarkWord与Klass与实例数据)]]
锁升级状态图(MarkWord 低 3 位):
01 (无锁) ─── CAS 设 ThreadID ──→ 01 (偏向锁)
01 (偏向锁) ── 其他线程竞争 ──→ 00 (轻量锁)
00 (轻量锁) ── CAS 失败+自旋超时 → 10 (重量锁)
10 (重量锁) ─────────→ 01 (无锁) 释放锁后回到无锁追问预判:
问: 锁能降级吗?
- 答: 重量级锁到轻量级锁的降级不可用。但偏向锁到无锁的降级在 GC 的 STW 安全点中可以发生(因为可以安全的检查线程状态)
问: Lock Record 在哪里?存什么?
- 答: 在获取锁的线程的栈帧中。存两部分:Displaced MarkWord(原 MarkWord 的副本)和 owner 指针(指向 Object 的 MarkWord)
真题 7:"类的生命周期?双亲委派模型?怎么打破?"
面试公司: 阿里、华为、字节(中高频)
考察点: 类加载全链路 + 实际应用场景
参考回答(PRINCIPLE CHAIN):
一、类的生命周期(7 个阶段)
加载 (Loading) → 验证 (Verification) → 准备 (Preparation)
→ 解析 (Resolution) → 初始化 (Initialization) → 使用 (Using)
→ 卸载 (Unloading)
关键知识点:
准备:静态变量赋零值(不是代码值),final 常量直接赋值
解析:符号引用 → 直接引用(可在初始化之后发生——动态绑定)
初始化:执行 <clinit>() 方法(按赋值语句顺序)
→ 详阅 [[02.1-类加载生命周期]]
二、双亲委派模型
Bootstrap CL ← Extension/Platform CL ← Application CL
工作流程:
1. App CL 收到 loadClass 请求
2. 询问 Extension CL 能否加载(向上委派)
3. Extension CL 询问 Bootstrap CL
4. Bootstrap CL 在自己的路径寻找 → 找不到 → 向下返回
5. Extension CL 寻找 → 找不到 → 向下返回
6. App CL 寻找 → 找到 → 返回 Class 对象
→ 详阅 [[02.2-双亲委派模型与打破]]
三、打破双亲委派
方法:重写 loadClass()(不调 super.loadClass())
场景:
1. SPI(JDBC):Bootstrap CL 加载 DriverManager → 需要加载第三方驱动
→ 用 Thread Context ClassLoader(通常是 App CL)反转委派
2. Tomcat:每个 Web 应用有自己的 WebappClassLoader
→ 优先加载 WEB-INF/lib 的类(如果找不到再委派给父加载器)
→ 实现应用间类隔离 + 不同版本的库共存
3. OSGi:每个 Bundle 一个 ClassLoader → 类加载路径变成图(不是树)
→ 详阅 [[02.4-Tomcat与Spring类加载分析]]、[[02.5-OSGI与SPI模块化]]追问预判:
问: 两个 Web 应用都依赖 Spring,Tomcat 怎么处理?
- 答: Spring jar 放在 Tomcat 的 shared 目录 → Shared ClassLoader 加载 → 多个 Webapp CL 共享。如果有不同版本的 Spring → 分别放在各自 WEB-INF/lib → 各自的 Webapp CL 优先加载
问: 自己写 ClassLoader 要注意什么?
- 答: 重写 findClass() 而不是 loadClass()(遵循委派)。已加载的类用 defineClass() 转化为 Class 对象。注意缓存(避免重复 defineClass)
真题 8:"你遇到过什么 OOM?怎么排查的?"
面试公司: 所有大厂(经典实战题——有没有真正处理过线上 P0 故障)
考察点: 排查方法论 + 工具熟练度 + 沟通能力
参考回答(PRINCIPLE CHAIN):
我遇到过一个典型案例:
【现象】订单服务每隔 2 小时出现一次 Full GC,持续 5 秒以上,导致请求超时报警
【排查过程】
Step 1: 确认 OOM 类型(不是 OOM 崩溃,而是频繁 Full GC 导致的慢响应)
jstat -gcutil pid 2000 50
发现 Full GC 每 15 分钟一次,每次耗时 4-7 秒
老年代使用率在 Full GC 后只从 98% 降到 60% → 怀疑泄漏
Step 2: 获取堆快照
jmap -dump:format=b,file=heap.hprof pid
(如无法 jmap,在启动参数加 -XX:+HeapDumpOnOutOfMemoryError)
Step 3: MAT 分析
Dominator Tree → 发现最大的对象是 ArrayList,持有大量 Session 对象
GC Root Chain → SessionManager → ConcurrentHashMap → Session → 各种数据
原因:Session 过期未被清除(配置的过期时间 8 小时 / remove 方法未被调用)
→ 详阅 [[06.4-OOM排查方法论]]
Step 4: 临时措施
手动调用 remove 逻辑 + 重启服务恢复
Step 5: 根本解决
Session 用 Redis 替代(避免 JVM 内存不足)
增加健康检测:当堆 > 80% 时告警
→ 详阅 [[06.7-大厂实战案例库]]追问预判:
问: 如果不能用 jmap(生产环境没权限),怎么排查?
- 答: 启动时预置 -XX:+HeapDumpOnOutOfMemoryError。或者用 Arthas heapdump 命令。或者启用 JMX + Prometheus 监控堆趋势
问: OOM 有几种类型?
- 答: Java heap space(堆满)、Metaspace(加载类太多)、Direct buffer memory(NIO)、unable to create new native thread(线程太多)、GC overhead limit exceeded(98% 时间花在 GC 上)
真题 9:"ZGC 了解吗?和 G1 的区别?"
面试公司: 字节、阿里、美团(JDK 21 时代的高频前沿题)
考察点: 对新特性的理解 + 场景选型
参考回答(PRINCIPLE CHAIN):
ZGC 是 JDK 11 引入(JDK 15 正式、JDK 21 分代正式)的全并发 GC,
核心目标:任何堆大小下 STW < 1ms。
ZGC 的核心技术:
1. 彩色指针(Colored Pointers)
64 位指针,其中 4 位存储颜色信息
M0/M1(交替标记位,奇数/偶数 GC 周期)、Rmtd(已重映射)、Fin(可终结)
指针就是 metadata → 读对象状态 = 位运算(不是查表)
没有传统 GC 的 RSet/记忆集
2. 负载屏障(Load Barrier)
每次读对象字段时检查指针颜色
如果颜色指示"需要处理"(如对象已移动)→ 慢速路径修正指针
修正成本极低(一次 CAS 更新引用)
3. 全并发
标记 / 重定位 / 重映射 全部并发
只有根扫描(STW <0.1ms)和阶段结束(STW <0.1ms)需要暂停
与 G1 的核心对比:
| 特性 | G1 | ZGC |
|------|-----|------|
| 堆结构 | Region | Region(但无分代直到 JDK 21)|
| 标记 | 并发(SATB)| 并发(彩色指针)|
| 重定位 | STW | 并发(负载屏障修正引用)|
| 重映射 | 不单独做 | 懒(随下次 GC 的负载屏障自动完成)|
| 记忆集 | RSet | 无(彩色指针替代)|
| Pause | ~10-50ms | <1ms |
| 堆范围 | 1MB-64GB | 8MB-16TB |
→ 详阅 [[09.1-ZGC原理]]
JDK 21 分代 ZGC:将堆分为年轻代 + 老年代(不分代时每次标记整个堆)→ 吞吐量提升 10-20%追问预判:
问: ZGC 的彩色指针怎么处理 4TB 以上的地址空间?
- 答: ZGC 使用多重映射(Multi-Mapping),同一物理内存映射到不同的虚拟地址。不同颜色 = 不同的虚拟地址映射到同一物理页。这样修改指针颜色 = 修改虚拟地址 = 指向同一物理地址的不同视图
问: ZGC 有什么代价?
- 答: 额外的 CPU 开销(所有 load 经过 barrier),内存开销稍高(转发表),小堆(<4GB)上用 G1 更划算
真题 10:"虚拟线程是什么?什么场景用?"
面试公司: 字节、阿里、腾讯(JDK 21 正式后极高频)
考察点: 新特性理解 + 场景判断 + Pinning 问题的认知
参考回答(PRINCIPLE CHAIN):
虚拟线程是 Project Loom(JDK 21 正式)的核心产出,
解决了 Java 传统平台线程 1:1 映射 OS 线程的扩展性问题。
【原理】
虚拟线程是 M:N 映射。M 个虚拟线程运行在 N 个平台线程(载波)上。
创建:~1μs(传统 ~1ms),不需要 OS 线程——栈存在堆上(~1KB,不是 ~1MB)
挂起/恢复:用户态(Continuation:保存栈帧到堆对象),不需要 OS 上下文切换
→ 详阅 [[09.2-虚拟线程原理]]
【工作方式】
虚拟线程运行时:
1. 挂到 ForkJoinPool 的一个载波线程上执行
2. 遇到阻塞(socket.read / JDBC 查询)→ JVM 挂起虚拟线程
3. 载波线程回到池中 → 可以执行另一个虚拟线程
4. I/O 完成后 → 虚拟线程恢复执行(可能在另一个载波上)
【适用场景】
✅ 适用:I/O 密集型
- HTTP 服务器(Tomcat + 虚拟线程 → 每个请求一个虚拟线程)
- 数据库查询(大量并发查询,DB driver 支持阻塞 API)
- 消息队列消费(Kafka 消费者)
- 网关/代理(转发请求)
❌ 不适用:
- CPU 密集型(数学计算、视频编码、加密解密)
- 高竞争锁(synchronized 大量争用 → pinning + 载波无法释放)
- 纯计算并行(用 platform thread + parallel stream 更好)
【Pinning 问题】
虚拟线程在 synchronized 块内阻塞 → 载波线程无法被释放 →
其他虚拟线程不能用这个载波 → 并发能力下降
解决方案:用 ReentrantLock 替换 synchronized
→ 详阅 [[09.2-虚拟线程原理]]追问预判:
问: 虚拟线程和 Go 的 goroutine 有什么区别?
- 答: 本质相似(用户态线程)。差异:goroutine 是 Go 语言内置,与 channel 等特性深度集成;Java 虚拟线程在 JVM 层兼容现有的 Java 生态——所有现有框架(Spring、Netty、JDBC)无需大规模改动就能使用
问: 用了虚拟线程,还需要响应式编程(WebFlux)吗?
- 答: 大部分场景不再需要。WebFlux 的核心价值是"用少数线程处理大量并发 I/O"——这正是虚拟线程的强项。但 WebFlux 在背压(Backpressure)处理上仍有优势,数据流处理场景 WebFlux/RxJava 仍有价值
📚 相关链接
- **面试高频100题** — 各分类高频题汇总
- **大厂实战案例库** — 更多真实案例
- **JVM文库设计文档** — 知识库设计全景
准备建议: 这 10 道题覆盖了 JVM 面试 90% 的知识点。建议每道题用自己的话完整讲一遍(录音 + 复盘) 时间分配: 每道题准备 3-5 分钟的完整回答。G1/ZGC 和虚拟线程是增量时间投入回报最高的两个方向