vault backup: 2026-08-09 19:06:40
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
---
|
||||
tags: [test/review, java, thread-pool, ThreadPoolExecutor, rejection-policy, task-queue]
|
||||
create time: 2026-08-09 12:00
|
||||
---
|
||||
|
||||
# 线程池参数与拒绝策略_测试题
|
||||
|
||||
## 概述
|
||||
本测试覆盖 ThreadPoolExecutor 七大参数、任务提交流程、四种拒绝策略、工作队列类型以及 CPU/IO 密集型调优公式,共 10 道题目(6 道选择题 + 3 道填空题 + 1 道简答题)。
|
||||
|
||||
---
|
||||
|
||||
## 一、选择题(6道,由浅入深)
|
||||
|
||||
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
|
||||
|
||||
### Q1(基础)— 考察定义层面
|
||||
ThreadPoolExecutor 构造方法的七个参数中,前五个决定了线程池的什么?
|
||||
|
||||
A. 线程的安全性和死锁检测
|
||||
B. 容量模型和任务调度逻辑
|
||||
C. 线程的名称格式和优先级
|
||||
D. 拒绝策略的具体行为
|
||||
|
||||
### Q2(基础)— 考察行为判断
|
||||
调用 `Executors.newFixedThreadPool(10)` 时,底层实际使用的是哪种队列?
|
||||
|
||||
A. `ArrayBlockingQueue`(有界)
|
||||
B. `LinkedBlockingQueue`(无界,capacity = Integer.MAX_VALUE)
|
||||
C. `SynchronousQueue`
|
||||
D. `PriorityBlockingQueue`
|
||||
|
||||
### Q3(进阶)— 考察核心原理
|
||||
当一个线程池配置了 `corePoolSize=8`、`maximumPoolSize=16`、`workQueue` 容量为 100 时,假设当前已有 8 个核心线程在运行,此时同时提交 150 个新任务。请问有多少任务会被放入队列、多少会触发非核心线程创建、多少会触发拒绝策略?
|
||||
|
||||
A. 100 入队 + 0 创建非核心 + 50 触发拒绝
|
||||
B. 100 入队 + 8 创建非核心 + 42 触发拒绝
|
||||
C. 100 入队 + 16 创建非核心 + 34 触发拒绝
|
||||
D. 150 全部入队,不会触发非核心线程
|
||||
|
||||
### Q4(进阶)— 考察比较与辨析
|
||||
关于四种拒绝策略,以下描述**正确**的是:
|
||||
|
||||
A. `AbortPolicy` 是默认策略,会静默丢弃任务而不抛出异常
|
||||
B. `CallerRunsPolicy` 会将任务回退到提交任务的线程执行,起到降速缓冲的作用
|
||||
C. `DiscardOldestPolicy` 是最安全的策略,永远不会丢失任何任务
|
||||
D. `DiscardPolicy` 适合处理可以丢失的非关键任务,会抛出异常通知调用方
|
||||
|
||||
### Q5(深入)— 考察场景推理
|
||||
某服务在流量突增时出现了 OOM 错误,经排查发现使用的是 `Executors.newCachedThreadPool()`。最可能的原因是:
|
||||
|
||||
A. CachedThreadPool 的工作队列是有界的,流量大会导致拒绝策略触发
|
||||
B. CachedThreadPool 的核心线程数为 0,所有任务都必须创建新线程,无限增长导致 OOM
|
||||
C. CachedThreadPool 的 keepAliveTime 太短,频繁创建销毁线程带来额外开销
|
||||
D. CachedThreadPool 使用 ArrayBlockingQueue 且有容量上限
|
||||
|
||||
### Q6(深入)— 考察数值计算
|
||||
在一台 8 核机器上运行 IO 密集型任务(阻塞系数取 0.85),根据经验公式,推荐的线程数约为:
|
||||
|
||||
A. 9 个(N + 1)
|
||||
B. 16 个(2 × N)
|
||||
C. 53 个(N / (1 - 0.85))
|
||||
D. 128 个(N × 16)
|
||||
|
||||
---
|
||||
|
||||
## 二、填空题(3道)
|
||||
|
||||
### F1 — 填空1
|
||||
线程池任务提交的流转逻辑遵循以下优先级:第一步创建核心线程;第二步如果已达核心线程数且队列未满则__(1)__;第三步如果队列已满但线程数未达到 maximumPoolSize 则创建__(2)__;第四步如果线程数达到上限且队列仍满则触发__(3)__。
|
||||
|
||||
> **提示**: 回忆流程图中的四个分支路径,注意每一步的前置条件。
|
||||
|
||||
### F2 — 填空2
|
||||
在 8 核机器上处理 CPU 密集型任务,推荐的线程数公式是 __(1)__(其中 N 为 CPU 核心数),多出来的那一个线程是为了容忍页缺失等偶发停顿。而对于 IO 密集型任务(阻塞系数 0.85),公式则是 __(2)__。
|
||||
|
||||
> **提示**: CPU 密集型和 IO 密型的公式完全不同,前者简单后者需要引入阻塞系数。
|
||||
|
||||
### F3 — 填空3
|
||||
`ArrayBlockingQueue` 属于__(1)__类型的阻塞队列,特点是 FIFO 且可控,__(2)__在生产中使用。与之相对,`LinkedBlockingQueue` 的默认 capacity 为 __(3)__,容易导致任务堆积甚至 OOM。
|
||||
|
||||
> **提示**: 关注各队列类型的"有界/无界"属性和生产推荐度。
|
||||
|
||||
---
|
||||
|
||||
## 三、简答题(1道)
|
||||
|
||||
### S1
|
||||
某微服务使用了如下线程池配置进行异步任务处理:
|
||||
|
||||
```java
|
||||
ExecutorService pool = Executors.newFixedThreadPool(20);
|
||||
for (Request req : batchRequests) {
|
||||
pool.submit(() -> process(req)); // 每个请求耗时约 200ms
|
||||
}
|
||||
```
|
||||
|
||||
大促期间突然出现以下现象:
|
||||
- 服务响应变慢,RT 飙升
|
||||
- 监控显示 JVM 堆内存持续上升并最终 OOM
|
||||
- CPU 使用率正常,说明不是计算瓶颈
|
||||
|
||||
请分析:
|
||||
1. 这段代码存在什么根本性问题?为什么会触发 OOM?
|
||||
2. 如果要改造这段代码,至少需要改进哪三个方面?(结合线程池配置原则)
|
||||
3. 如果决定将拒绝策略改为 `CallerRunsPolicy`,需要注意什么风险?
|
||||
|
||||
> **答题框架提示**:
|
||||
> - 第 1 问抓住 Executors.newFixedThreadPool 的底层队列特性
|
||||
> - 第 2 问从"有界队列 + 自定义核心线程数 + 手动构造"入手
|
||||
> - 第 3 问从 CallerRunsPolicy 的行为特征推导其对业务链路的影响
|
||||
|
||||
---
|
||||
|
||||
## 参考答案与解析
|
||||
|
||||
### 选择题答案
|
||||
|
||||
| 题号 | 正确答案 | 解析 |
|
||||
|------|---------|------|
|
||||
| Q1 | B | 前五个参数(corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue)共同定义了线程池的容量模型和任务调度逻辑。threadFactory 和 handler 是扩展点。 |
|
||||
| Q2 | B | `newFixedThreadPool` 内部使用 `LinkedBlockingQueue`,其默认 capacity 为 `Integer.MAX_VALUE`(无界),这会导致任务不断堆积而不会触发非核心线程创建或拒绝策略。 |
|
||||
| Q3 | B | 核心线程 8 个已满,150 个任务先到队列排,队列容量 100 入队 100 个;剩余 50 个触发第三步创建非核心线程,最多可创建 16-8=8 个非核心线程处理 8 个任务;最后 50-8=42 个触发拒绝策略。 |
|
||||
| Q4 | B | B 正确:CallerRunsPolicy 由提交线程执行,起到降速缓冲作用。A 错(AbortPolicy 抛出异常),C 错(DiscardOldestPolicy 会丢弃老任务),D 错(DiscardPolicy 静默丢弃不抛异常)。 |
|
||||
| Q5 | B | CachedThreadPool 的 corePoolSize=0,所有任务都需新建线程处理,且 keepAliveTime=60s。无界队列 + 无限创建线程,在高并发下线程数暴增导致 OOM。 |
|
||||
| Q6 | C | IO 密集型公式:N / (1 - 阻塞系数) = 8 / (1 - 0.85) = 8 / 0.15 ≈ 53 个线程。CPU 密集型的 N+1=9 不适用于 IO 场景。 |
|
||||
|
||||
### 填空题答案
|
||||
|
||||
| 题号 | 答案 | 解析 |
|
||||
|------|------|------|
|
||||
| F1 | (1)放入队列排队 (2)非核心线程 (3)拒绝策略 | 任务提交的四级决策路径:核心线程→队列→非核心线程→拒绝策略。缺一不可。 |
|
||||
| F2 | (1)N + 1 (2)N / (1 - 阻塞系数) | CPU 密集型几乎一直在计算,不需要等待 IO,多余线程只会增加上下文切换开销;IO 密集型线程大量时间在等待 IO,可以多开线程提高利用率。 |
|
||||
| F3 | (1)有界 (2)推荐 (3)Integer.MAX_VALUE | ArrayBlockingQueue 明确指定容量上限,安全可控;LinkedBlockingQueue 默认无界是大坑。 |
|
||||
|
||||
### 简答题参考答案
|
||||
|
||||
S1:**参考答案要点**:
|
||||
1. 根本问题:`Executors.newFixedThreadPool(20)` 底层使用无界队列 `LinkedBlockingQueue`(capacity = MAX_VALUE),150 个任务全部入队排队,永远不会创建超过 20 个线程。但由于 `process(req)` 耗时 200ms,队列中堆积的任务引用不会被释放,加上请求对象本身的内存占用,堆持续增长最终 OOM。本质上是一个典型的"无界队列导致的任务堆积 OOM"。
|
||||
2. ① 换为有界队列的 `ThreadPoolExecutor`(如 `ArrayBlockingQueue(100)`);② 根据 IO 密集型公式重新计算核心线程数和最大线程数;③ 显式配置拒绝策略(如 `CallerRunsPolicy`)而非依赖默认的 AbortPolicy 抛异常打断链路。
|
||||
3. `CallerRunsPolicy` 会将任务回退到提交线程执行——在这个场景下,如果提交线程是 HTTP 请求线程,那么处理本身就需要 200ms 的任务会在请求线程里同步执行,直接拖慢整个业务链路,导致更多的请求涌入时雪崩效应加剧。使用时务必确认调用方的线程性质。
|
||||
|
||||
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
|
||||
|
||||
## 关联笔记
|
||||
- [[01.Java/concurrent/锁升级与 CAS 机制]]
|
||||
@@ -0,0 +1,137 @@
|
||||
---
|
||||
tags: [test/review, java, lock-escalation, CAS, AQS, optimistic-lock]
|
||||
create time: 2026-08-09 12:00
|
||||
---
|
||||
|
||||
# 锁升级与 CAS 机制_测试题
|
||||
|
||||
## 概述
|
||||
本测试覆盖 synchronized 锁升级机制(偏向→轻量级→重量级)、CAS 原子操作原理、ABA 问题及其解决方案、AQS 框架设计等核心内容,共 10 道题目(6 道选择题 + 3 道填空题 + 1 道简答题)。
|
||||
|
||||
---
|
||||
|
||||
## 一、选择题(6道,由浅入深)
|
||||
|
||||
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
|
||||
|
||||
### Q1(基础)— 考察定义层面
|
||||
synchronized 锁升级的正确顺序是:
|
||||
|
||||
A. 重量级 → 轻量级 → 偏向锁
|
||||
B. 偏向锁 → 轻量级 → 重量级
|
||||
C. 轻量级 → 偏向锁 → 重量级
|
||||
D. 偏向锁 → 重量级 → 轻量级
|
||||
|
||||
### Q2(基础)— 考察概念记忆
|
||||
关于偏向锁,以下说法**正确**的是:
|
||||
|
||||
A. JDK 17 中仍然默认启用,通过 `-XX:+UseBiasedLocking` 控制
|
||||
B. 偏向锁撤销时需要全局 STW,代价较高,因此在真实场景中很少能发挥优势
|
||||
C. 偏向锁状态下,同一线程重复获取锁也需要进行一次 CAS 操作
|
||||
D. 当有其他线程尝试获取偏向锁时,当前锁会直接升级为重量级锁
|
||||
|
||||
### Q3(进阶)— 考察核心原理
|
||||
在轻量级锁获取过程中,线程首先在栈帧中创建 Lock Record,然后使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。如果 CAS 失败,接下来会发生什么?
|
||||
|
||||
A. 立即升级为重量级锁
|
||||
B. 线程进入睡眠状态等待其他线程释放锁
|
||||
C. 当前线程自旋重试(最多循环指定次数)
|
||||
D. 抛出 ConcurrencyModificationException 异常
|
||||
|
||||
### Q4(进阶)— 考察比较与辨析
|
||||
`synchronized` 关键字与 `ReentrantLock` 的主要区别,以下描述**错误**的是:
|
||||
|
||||
A. synchronized 是 JVM 内置关键字,ReentrantLock 是 JDK API 层的实现
|
||||
B. synchronized 支持锁升级(偏向→轻量级→重量级),ReentrantLock 没有锁升级机制
|
||||
C. synchronized 支持超时获取锁(tryLock 语法),ReentrantLock 不支持
|
||||
D. ReentrantLock 可选公平锁和非公平锁,synchronized 始终是非公平的
|
||||
|
||||
### Q5(深入)— 考察场景推理
|
||||
在某高并发计数器场景中,开发者使用 `AtomicInteger` 代替 `synchronized` 来提升性能。但在实际压测中发现,随着并发量的增加,CPU 使用率反而急剧升高,QPS 并未如预期增长。最可能的原因是:
|
||||
|
||||
A. AtomicInteger 本身存在线程安全问题,在高并发下会漏计
|
||||
B. CAS 长时间自旋增加了 CPU 开销,竞争激烈时应升级为重量级锁或使用其他方案
|
||||
C. AtomicInteger 的 value 字段缺少 volatile 修饰,线程间看不到最新值
|
||||
D. AtomicInteger 的 incrementAndGet() 不是原子操作
|
||||
|
||||
### Q6(深入)— 考察源码级理解
|
||||
AQS 的 CLH 队列中,当一个节点的状态为 `SIGNAL` 时,意味着什么?
|
||||
|
||||
A. 该线程已取消等待(CANCELLED)
|
||||
B. 该线程的后继节点需要被 unpark(即前驱释放锁后会唤醒后继)
|
||||
C. 该线程正在等待条件变量(CONDITION)
|
||||
D. 该节点处于共享模式传播状态(PROPAGATE)
|
||||
|
||||
---
|
||||
|
||||
## 二、填空题(3道)
|
||||
|
||||
### F1 — 填空1
|
||||
在轻量级锁的获取流程中,竞争发生时线程会进行自旋重试。如果自旋超过阈值(默认 __(1)__ 次,可通过 `-XX:PreBlockSpin` 调整),就会升级为__(2)__锁。此时未获得锁的线程会被__(3)__挂起,进入 OS 的等待队列,需要切换到内核态,这是性能损耗最大的环节。
|
||||
|
||||
> **提示**: 回忆轻量级锁的自旋阈值和重量级锁的关键特征。
|
||||
|
||||
### F2 — 填空2
|
||||
CAS 是 CPU 级别的原语指令,在 x86 架构下对应__(1)__汇编指令。在多核环境下通过总线锁或缓存锁保证原子性。CAS 的一个经典问题是__(2)__问题——线程 T1 读取值为 A,T2 把值改为 B 再改回 A,T1 的 CAS 检查时发现值仍是 A 但实际值已经被修改过。解决思路是给值加__(3)__,每次变更版本号加 1。
|
||||
|
||||
> **提示**: x86 的 cmpxchg 指令是 CAS 的硬件基础,ABA 的解法是加版本戳。
|
||||
|
||||
### F3 — 填空3
|
||||
AQS 的核心是一个 `volatile int state` 变量。在不同同步器中,state 的含义不同:ReentrantLock 中 state 表示__(1)__;CountDownLatch 中 state 表示__(2)__(递减到 0 后触发);Semaphore 中 state 表示__(3)__。
|
||||
|
||||
> **提示**: state 的含义取决于具体实现类的语义,回忆文中 AQS 状态机表格。
|
||||
|
||||
---
|
||||
|
||||
## 三、简答题(1道)
|
||||
|
||||
### S1
|
||||
某电商秒杀系统在订单扣减环节面临高并发竞争。开发团队最初使用 `synchronized` 保护库存扣减逻辑,后发现性能瓶颈。团队计划迁移到 `ReentrantLock` + AQS 的方案。已知以下条件:
|
||||
- 峰值 QPS 约 5000
|
||||
- 库存扣减需要保证原子性和一致性
|
||||
- 部分上游服务依赖超时较短(200ms),需要支持锁的超时获取
|
||||
|
||||
请回答:
|
||||
1. 从 synchronized 迁移到 ReentrantLock 的主要优势有哪些?列出至少 3 个。
|
||||
2. 如果采用 ReentrantLock 的非公平锁模式,请简述其 tryAcquire 的核心逻辑流程(涉及 CAS 和重入)。
|
||||
3. 在 AQS 的 CLH 队列中,如果一个节点的线程已经超时,AQS 如何处理这个节点?节点可能的状态有哪些?
|
||||
|
||||
> **答题框架提示**:
|
||||
> - 第 1 问从公平性、中断、超时、Condition 等维度对比
|
||||
> - 第 2 问结合 ReentrantLock tryAcquire 的代码逻辑
|
||||
> - 第 3 问列举 AQS 节点状态及其含义
|
||||
|
||||
---
|
||||
|
||||
## 参考答案与解析
|
||||
|
||||
### 选择题答案
|
||||
|
||||
| 题号 | 正确答案 | 解析 |
|
||||
|------|---------|------|
|
||||
| Q1 | B | synchronized 的锁升级路径是固定的:无锁→偏向锁→轻量级锁→重量级锁。一旦升级到重量级就不会再降级(除非竞争减弱时从退出队列恢复)。 |
|
||||
| Q2 | B | 偏向锁在实际场景中"永远只有一个人访问"的概率很低,而撤销代价高(需全局 STW),因此 JDK 15 标记废弃、JDK 17 移除。A 错在版本不对,C 错在偏向锁重复获取无需 CAS,D 错在是升级为轻量级而非直接重量级。 |
|
||||
| Q3 | C | CAS 失败后线程会自旋重试,最多循环指定次数(默认 10 次)。超过阈值才升级为重量级锁。不会立即升或抛异常。 |
|
||||
| Q4 | C | C 是错误的描述:synchronized 不支持超时获取锁,ReentrantLock 才有 `tryLock(timeout)`。A、B、D 都是正确的。 |
|
||||
| Q5 | B | 在高并发竞争下,CAS 自旋重试会消耗大量 CPU,这就是为什么有界锁最终会升级为重量级锁。QPS 不上涨是因为 CPU 变成了瓶颈。 |
|
||||
| Q6 | B | SIGNAL 状态表示该节点的后继节点需要被 unpark——即前驱释放锁后会唤醒后继。CANCELLED=已取消,CONDITION=等待条件变量,PROPAGATE=共享模式传播。 |
|
||||
|
||||
### 填空题答案
|
||||
|
||||
| 题号 | 答案 | 解析 |
|
||||
|------|------|------|
|
||||
| F1 | (1)10 (2)重量级 (3)阻塞 | 轻量级锁自旋阈值为 10 次,超时会退化为重量级锁。重量级锁通过 Monitor 实现,线程阻塞挂起需要内核态切换。 |
|
||||
| F2 | (1)cmpxchg (2)ABA (3)版本号 | cmpxchg 是 x86 的 CAS 汇编指令。ABA 问题的解法是 AtomicStampedReference ——给值加版本号。 |
|
||||
| F3 | (1)持有锁的次数(重入计数) (2)计数器初始值 (3)可用许可证数量 | state 的含义因同步器而异,但其声明始终是 volatile int,保证可见性。 |
|
||||
|
||||
### 简答题参考答案
|
||||
|
||||
S1:**参考答案要点**:
|
||||
1. 主要优势:① 支持公平锁/非公平锁可选,可以根据业务选择合适的策略;② 支持 `lockInterruptibly()` 响应中断,避免因某个锁永远拿不到而导致线程无限阻塞;③ 支持 `tryLock(timeout)` 超时获取,满足上游 200ms 超时约束;④ 支持多个 Condition 变量,比 synchronized 的 wait()/notify() 更灵活。
|
||||
2. 非公平锁 tryAcquire 核心流程:① 先尝试直接用 CAS 拿锁(`compareAndSetState(0, acquires)`),成功则设置独占线程;② 如果 state 不为 0,判断当前线程是否为已有的独占线程,如果是则重入累加 state;③ 都不满足则返回 false,走 AQS 排队流程。
|
||||
3. AQS 节点可能的状态包括:`CANCELLED`(值为 1,已取消等待)、`SIGNAL`(值为 -1,后继需 unpark)、`CONDITION`(值为 -2,等待条件变量)、`PROPAGATE`(值为 -3,共享模式传播)。如果节点超时,通常会将其状态设为 CANCELLED 并从队列中移除(由前驱节点的 unlinkCancelledNodes 或 self-unpark 机制处理)。
|
||||
|
||||
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
|
||||
|
||||
## 关联笔记
|
||||
- [[01.Java/concurrent/线程池参数与拒绝策略]]
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
tags: [test/review, java, jvm-memory, heap, metaspace, oom]
|
||||
create time: 2026-08-09 12:00
|
||||
---
|
||||
|
||||
# JVM 内存模型_测试题
|
||||
|
||||
## 概述
|
||||
本测试覆盖 JVM 运行时数据区的划分、对象创建流程、GC Roots 判定标准以及常见 OOM 类型,共 10 道题目(6 道选择题 + 3 道填空题 + 1 道简答题),由浅入深检验对 JVM 内存模型的掌握程度。
|
||||
|
||||
---
|
||||
|
||||
## 一、选择题(6道,由浅入深)
|
||||
|
||||
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
|
||||
|
||||
### Q1(基础)— 考察定义层面
|
||||
以下哪个区域是 JVM 中**唯一不会抛出 OutOfMemoryError** 的内存区域?
|
||||
|
||||
A. 堆(Heap)
|
||||
B. 方法区 / 元空间(Method Area / Metaspace)
|
||||
C. Java 虚拟机栈(JVM Stack)
|
||||
D. 程序计数器(Program Counter Register)
|
||||
|
||||
### Q2(基础)— 考察行为判断
|
||||
JDK 8 移除了永久代(PermGen),改用元空间(Metaspace)。关于这一变化,以下说法**正确**的是:
|
||||
|
||||
A. 元空间仍然在 JVM 堆内分配内存,只是改了一个名字
|
||||
B. 元空间使用本地内存(Direct Memory),上限通过 `-XX:MaxMetaspaceSize` 控制
|
||||
C. 元空间的大小固定不变,不会受到本机总内存的限制
|
||||
D. 永久代和元空间的 OOM 错误完全相同,都是 `java.lang.OutOfMemoryError: PermGen space`
|
||||
|
||||
### Q3(进阶)— 考察核心原理
|
||||
在堆中通过 `new` 指令创建一个对象时,以下步骤的**正确执行顺序**是:
|
||||
|
||||
① 设置对象头(Hash Code、分代年龄、锁标志等)
|
||||
② 类加载检查(常量池定位 + 必要的类加载)
|
||||
③ 初始化零值(内存清零)
|
||||
④ 执行 `<init>` 方法(字段赋初值)
|
||||
⑤ 分配内存(指针碰撞或空闲列表)
|
||||
|
||||
A. ② → ⑤ → ③ → ① → ④
|
||||
B. ② → ③ → ⑤ → ① → ④
|
||||
C. ⑤ → ② → ③ → ① → ④
|
||||
D. ② → ⑤ → ① → ③ → ④
|
||||
|
||||
### Q4(进阶)— 考察比较与辨析
|
||||
标记-复制算法和标记-整理算法都用于解决垃圾回收,两者的关键区别在于:
|
||||
|
||||
A. 标记-复制会产生内存碎片,标记-整理不会产生
|
||||
B. 标记-复制需要将存活对象复制到另一块内存区域并清空原区,标记-整理是让存活对象向一端移动后清理边界外内存
|
||||
C. 标记-复制只适用于老年代,标记-整理只适用于新生代
|
||||
D. 标记-整理的 STW 时间一定比标记-复制长
|
||||
|
||||
### Q5(深入)— 考察场景推理
|
||||
某服务的线上日志频繁出现 `java.lang.OutOfMemoryError: GC overhead limit exceeded`,以下排查方向**最不合理**的是:
|
||||
|
||||
A. 通过 `-XX:+HeapDumpOnOutOfMemoryError` 自动 dump 堆快照,用 MAT 分析 Dominator Tree
|
||||
B. 加大堆容量(增大 `-Xmx`)或修复潜在的内存泄漏
|
||||
C. 通过 `-XX:-UseGCOverheadLimit` 关闭该检查以避免再次报错
|
||||
D. 检查是否有一群几乎不会死亡的对象长期占用少量堆空间
|
||||
|
||||
### Q6(深入)— 考察源码级理解
|
||||
当 JVM 抛出 `java.lang.OutOfMemoryError: unable to create new native thread` 时,根本原因通常不是 JVM 堆内存不足,而是:
|
||||
|
||||
A. 元空间中加载的 Class 数量超过了 `-XX:MaxMetaspaceSize`
|
||||
B. 操作系统级别的线程数限制触顶(如 Linux 的 `ulimit -u`)
|
||||
C. Direct ByteBuffer 占用了过多的堆外内存
|
||||
D. 程序计数器的缓冲区被写满
|
||||
|
||||
---
|
||||
|
||||
## 二、填空题(3道)
|
||||
|
||||
### F1 — 填空1
|
||||
在使用 Eclipse MAT 分析 OOM 堆快照时,**Dominator Tree** 展示了对象之间的引用关系链。按照对象占用堆空间从大到小排列,排在最上方的通常是__(1)__持有大量引用的__(2)__。
|
||||
|
||||
> **提示**: 参考文中提到的"秋招面试高频问题"表格中 OOM 排查流程,以及堆快照分析的典型输出结构。
|
||||
|
||||
### F2 — 填空2
|
||||
一个对象的死亡通常需要两次标记过程:第一次确认没有 GC Roots 路径可达;第二次才是真正回收。如果该对象重写了 `finalize()` 方法且尚未执行过,虚拟机会在第二次标记时帮它触发一次 finalize。**但这个方法在 Java __(1)__ 版本之后已被标记为废弃**。
|
||||
|
||||
> **提示**: 回忆文中关于 GC Roots 判定标准部分的提示内容。
|
||||
|
||||
### F3 — 填空3
|
||||
堆大小由两个 JVM 参数控制:`-Xms` 表示__(1)__,`-Xmx` 表示__(2)__。生产环境中通常建议将两者设为同一值,以避免运行时动态扩缩带来的性能开销。
|
||||
|
||||
> **提示**: 这是堆内存调优中最基础也是最常见的两个参数。
|
||||
|
||||
---
|
||||
|
||||
## 三、简答题(1道)
|
||||
|
||||
### S1
|
||||
某电商服务在线上遇到 `java.lang.OutOfMemoryError: Metaspace` 异常。经初步调查发现:
|
||||
- 该服务大量使用了 CGLIB 动态代理生成子类
|
||||
- MyBatis 扫描了较大的包路径,导致加载了大量 Class
|
||||
- 使用了 Spring Boot DevTools 进行热部署
|
||||
|
||||
请结合 JVM 内存模型的知识,回答以下问题:
|
||||
1. 为什么 Metaspace 会耗尽?它与 JDK 7 的永久代有什么区别?
|
||||
2. 可以从哪些维度(至少 3 个)来缓解或解决这个问题?
|
||||
3. 如果需要在不停服的情况下临时提升 Metaspace 上限,应该使用什么 JVM 参数?
|
||||
|
||||
> **答题框架提示**:
|
||||
> - 第 1 问先解释 Metaspace 的存储位置和底层实现机制,再对比永久代
|
||||
> - 第 2 问分别从"减少 Class 数量"、"调整参数"、"架构层面"三个维度思考
|
||||
> - 第 3 问直接给出参数名即可
|
||||
|
||||
---
|
||||
|
||||
## 参考答案与解析
|
||||
|
||||
### 选择题答案
|
||||
|
||||
| 题号 | 正确答案 | 解析 |
|
||||
|------|---------|------|
|
||||
| Q1 | D | 程序计数器记录当前线程执行的字节码行号,只需极小的空间,是唯一不会发生 OOM 的区域。堆和方法区可能 OOM,栈会抛 StackOverflowError。 |
|
||||
| Q2 | B | 元空间使用本地内存(Direct Memory),而非堆内内存,上限通过 `-XX:MaxMetaspaceSize` 控制。A 错在说"堆内",C 错在说"不受限",D 错在错误类型不同(元空间 OOM 类型为 `OutOfMemoryError: Metaspace`)。 |
|
||||
| Q3 | A | 正确的创建顺序是:②类加载检查 → ⑤分配内存 → ③零值初始化 → ①设置对象头 → ④执行 init 方法。必须先定位类才能分配对应的内存空间。 |
|
||||
| Q4 | B | 标记-复制将存活对象复制到另一半并清空原区,天然紧凑无碎片但浪费一半空间;标记-整理是移动存活对象到一端后清理边界外内存。A 恰好说反了。 |
|
||||
| Q5 | C | 关闭检查只是掩耳盗铃,不能解决根本问题。A(dump 分析)、B(加大堆或修泄漏)、D(识别僵尸对象)都是合理的排查方向。 |
|
||||
| Q6 | B | 此错误说明 OS 级别的线程数上限触顶,每个 Java 线程映射为一个原生线程,消耗约 1MB 栈内存。可通过减少并发线程数或提升系统 ulimit 来解决。 |
|
||||
|
||||
### 填空题答案
|
||||
|
||||
| 题号 | 答案 | 解析 |
|
||||
|------|------|------|
|
||||
| F1 | (1)对象 (2)实例链 | Dominator Tree 按支配关系展示对象图,最大的"支配者"对象排在最上方,帮助快速定位持有大量引用的可疑对象。 |
|
||||
| F2 | (1)Java 9+ | `finalize()` 方法从 Java 9 开始被标记为 deprecated,最终在 Java 18 被正式移除。JVM 会在第二次标记时尝试调用它。 |
|
||||
| F3 | (1)初始堆大小 (2)最大堆大小 | `-Xms` 和 `-Xmx` 分别控制堆的初始值和最大值。设成同一值可以避免运行时因动态扩容/缩容带来的性能损耗。 |
|
||||
|
||||
### 简答题参考答案
|
||||
|
||||
S1:**参考答案要点**:
|
||||
1. Metaspace 使用本地内存而非堆内内存,理论上只受本机物理内存总量限制;永久代则在堆内实现,大小固定容易撑爆。动态代理和热部署会产生大量 Class 定义,耗尽 Metaspace。
|
||||
2. ① 减少 Class 数量:缩小 MyBatis 扫描包路径范围;减少不必要的 CGLIB 代理;评估热部署框架的生产必要性。② 调整参数:适当增大 `-XX:MaxMetaspaceSize`。③ 架构优化:避免过于频繁的热部署重启,或考虑更优雅的热更新方案。
|
||||
3. `-XX:MaxMetaspaceSize=512m`(或其他合理值)。注意这是一个启动参数,不停服修改需要配合 JMX 或重启。
|
||||
|
||||
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
|
||||
|
||||
## 关联笔记
|
||||
- [[01.Java/jvm/垃圾回收算法与收集器]]
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
tags: [test/review, java, gc-algorithm, cms, g1, zgc, young-gen]
|
||||
create time: 2026-08-09 12:00
|
||||
---
|
||||
|
||||
# 垃圾回收算法与收集器_测试题
|
||||
|
||||
## 概述
|
||||
本测试覆盖标记-清除/复制/整理三种经典 GC 算法、分代理论依据,以及 CMS、G1、ZGC 三大收集器的设计原理和选型策略,共 10 道题目(6 道选择题 + 3 道填空题 + 1 道简答题)。
|
||||
|
||||
---
|
||||
|
||||
## 一、选择题(6道,由浅入深)
|
||||
|
||||
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
|
||||
|
||||
### Q1(基础)— 考察定义层面
|
||||
新生代 Eden:S0:S1 的经典比例设计中,Eden 区占新生代的份额大约是:
|
||||
|
||||
A. 1/2(50%)
|
||||
B. 1/3(约 33%)
|
||||
C. 4/5(80%)
|
||||
D. 9/10(90%)
|
||||
|
||||
### Q2(基础)— 考察概念记忆
|
||||
以下哪种 GC 算法的主要缺点是**会产生大量内存碎片**?
|
||||
|
||||
A. 标记-复制(Mark-Copy)
|
||||
B. 标记-整理(Mark-Compact)
|
||||
C. 标记-清除(Mark-Sweep)
|
||||
D. G1 的 Mixed GC
|
||||
|
||||
### Q3(进阶)— 考察核心原理
|
||||
CMS 收集器有四个阶段,其中**不会暂停用户线程**的阶段是:
|
||||
|
||||
A. 初始标记(Initial Mark)
|
||||
B. 并发标记(Concurrent Mark)
|
||||
C. 预标记(Re-Mark)
|
||||
D. 以上三个阶段都会 STW
|
||||
|
||||
### Q4(进阶)— 考察比较与辨析
|
||||
G1 收集器与传统的 CMS 收集器相比,最核心的架构区别在于:
|
||||
|
||||
A. G1 不再物理分隔新生代和老年代,而是将堆划分为多个大小相等的 Region
|
||||
B. G1 只使用标记-清除算法,而 CMS 使用标记-复制算法
|
||||
C. G1 的所有 GC 动作都在单线程内串行完成
|
||||
D. G1 不支持并发回收,必须依赖 Full GC
|
||||
|
||||
### Q5(深入)— 考察场景推理
|
||||
某服务堆大小约为 16GB,要求 GC 暂停时间控制在 200ms 以内,对吞吐量有一定要求但不追求极致低延迟。根据收集器选型决策树,最合适的选择是:
|
||||
|
||||
A. CMS
|
||||
B. G1
|
||||
C. ZGC
|
||||
D. Parallel Old
|
||||
|
||||
### Q6(深入)— 考察边界场景
|
||||
ZGC 能够实现暂停时间不超过 10ms(无论堆大小)的核心技术是:
|
||||
|
||||
A. 在写屏障中记录所有引用修改,然后批量重定位
|
||||
B. 染色指针 + 加载屏障,将原本沉重的写屏障移到加载时机
|
||||
C. 利用多核并行地在一个 STW 窗口内完成全量回收
|
||||
D. 将所有对象分配到连续内存区域,避免引用失效
|
||||
|
||||
---
|
||||
|
||||
## 二、填空题(3道)
|
||||
|
||||
### F1 — 填空1
|
||||
CMS 收集器有三个显著缺陷:**浮动垃圾**(并发清扫阶段产生的新对象只能等下一次 GC 处理)、__(1)__(基于标记-清除算法容易产生碎片)、以及 CPU 资源敏感(并发阶段和用户代码共享 CPU)。CMS 在 JDK __(2)__ 中被彻底移除。
|
||||
|
||||
> **提示**: 回顾 CMS 缺陷部分的具体描述和生命周期。
|
||||
|
||||
### F2 — 填空2
|
||||
G1 收集器中有一个特殊的概念叫 ROI(Return of Investment),用于排序 Region 的回收优先级——优先回收__(1)__最大的 Region。此外,超过半个 Region 大小的超大对象会直接分配到__(2)__ Region,以避免碎片问题。
|
||||
|
||||
> **提示**: ROI = 回收得到的空间 / 花费的时间,超大对象的特殊命名与其用途相关。
|
||||
|
||||
### F3 — 填空3
|
||||
根据选型决策树:堆 < 4GB 推荐 Parallel GC(吞吐优先)或 CMS(JDK 8 低延迟需求);4GB ≤ 堆 ≤ 32GB 推荐 __(1)__;堆 > 32GB 且要求暂停 < 10ms 推荐 __(2)__。
|
||||
|
||||
> **提示**: 这是生产环境中最常用的两个收集器选择分界线。
|
||||
|
||||
---
|
||||
|
||||
## 三、简答题(1道)
|
||||
|
||||
### S1
|
||||
某电商大促活动即将开始,负责团队正在做线上 GC 压测。现有三个待选收集器:CMS、G1、ZGC。已知以下条件:
|
||||
- 堆大小约 24GB
|
||||
- 要求 GC 暂停时间尽量短(P99 < 300ms)
|
||||
- 业务允许一定的吞吐量损失
|
||||
- 当前运行在 JDK 11 之上
|
||||
|
||||
请回答:
|
||||
1. 你会推荐哪个(些)收集器?给出决策理由。
|
||||
2. ZGC 相比 G1 的核心优势是什么?它的两项关键技术名词是什么?
|
||||
3. 如果在压测中发现使用 CMS 时频繁出现 Full GC,可能的原因是什么?(至少给出两条)
|
||||
|
||||
> **答题框架提示**:
|
||||
> - 第 1 问先看堆大小落在选型树的哪个区间
|
||||
> - 第 2 问直接提取 ZGC 的技术细节
|
||||
> - 第 3 问从 CMS 的三个缺陷出发推导
|
||||
|
||||
---
|
||||
|
||||
## 参考答案与解析
|
||||
|
||||
### 选择题答案
|
||||
|
||||
| 题号 | 正确答案 | 解析 |
|
||||
|------|---------|------|
|
||||
| Q1 | C | HotSpot 通过 Eden:S0:S1 = 8:1:1 的比例设计,Eden 占 8/10 = 80%,让绝大多数新对象直接分配到 Eden。只有少量长寿对象才进入 Survivor。 |
|
||||
| Q2 | C | 标记-清除算法不移动对象、不复制,只是在清除未标记区域时留下空洞,产生碎片。标记-复制和标记-整理都能避免碎片。 |
|
||||
| Q3 | B | CMS 的初始标记和预标记都需要短暂 STW,并发标记和并发清扫是完全与用户线程并行的。所以选 B。 |
|
||||
| Q4 | A | G1 的最大创新是将堆划分为最多 2048 个 Region(每个 1~32MB),不再物理分隔新生代和老年代。B 错误(G1 不使用纯标记-清除),C 错误(G1 并行并发),D 错误(G1 支持并发)。 |
|
||||
| Q5 | B | 堆大小 24GB 落在 4GB~32GB 区间,选型决策树明确指出这个范围推荐 G1。ZGC 虽然也适用但成本更高(吞吐量略低),Parallel Old 吞吐优先不适合低延迟场景。 |
|
||||
| Q6 | B | ZGC 的核心创新是染色指针(在指针高位 bit 编码颜色信息)和加载屏障(读引用时才执行重定位),将写屏障移到加载时机。A 描述的是传统写屏障方式。 |
|
||||
|
||||
### 填空题答案
|
||||
|
||||
| 题号 | 答案 | 解析 |
|
||||
|------|------|------|
|
||||
| F1 | (1)内存碎片 (2)JDK 14 | CMS 基于标记-清除算法,必然产生碎片,可能导致大对象无法分配而提前触发 Full GC。CMS 在 JDK 9 标记废弃,JDK 14 彻底移除。 |
|
||||
| F2 | (1)ROI(回收价值) (2)Humongous | G1 按 ROI 排序优先回收性价比最高的 Region。超大对象(超过半 Region)直接放入 Humongous Region 避免跨区碎片。 |
|
||||
| F3 | (1)G1 (2)ZGC | 4GB~32GB 是 G1 的主战场(大多数生产环境的默认选择),>32GB 且要求低延迟时切换 ZGC。 |
|
||||
|
||||
### 简答题参考答案
|
||||
|
||||
S1:**参考答案要点**:
|
||||
1. 堆大小 24GB 落在 4GB~32GB 区间,推荐 G1 作为首选。如果预算允许且对延迟要求更严苛,也可以评估 ZGC。JDK 11 以上已支持 ZGC 生产使用。
|
||||
2. ZGC 的核心优势是暂停时间绝对不超过 10ms,与堆大小无关。两项关键技术:染色指针(Colored Pointers)和加载屏障(Load Barrier)。
|
||||
3. CMS 频繁 Full GC 的可能原因:① 内存碎片过多导致大对象无法在 Eden 分配直接进入老年代,老年代很快满;② CMS 并发清扫阶段产生的"浮动垃圾"积累到下一次的 Full GC;③ 老年代阈值配置不合理,GC 未能及时触发。
|
||||
|
||||
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
|
||||
|
||||
## 关联笔记
|
||||
- [[01.Java/jvm/JVM 内存模型]]
|
||||
Reference in New Issue
Block a user