目录
- 1. JVM 的组成
- 2. JMM 是什么
- 3. Java 垃圾回收算法比较
- 4. Java 线程池配置
- 5. execute 与 submit 对比
- 6. OOM 问题如何处理
- 7. RocketMQ、RabbitMQ、Kafka 对比
- 8. 面试回答模板
- 9. 最终记忆版
1. JVM 的组成
JVM 可以理解为 Java 程序运行时的虚拟计算机,主要由以下部分组成:
JVM
├── 类加载子系统 (ClassLoader Subsystem)
├── 运行时数据区 (Runtime Data Area)
├── 执行引擎 (Execution Engine)
├── 本地方法接口 (JNI)
└── 本地方法库 (Native Libraries)
1.1 类加载子系统
类加载子系统负责把 .class 字节码文件加载到 JVM 中。
类加载过程:
加载 (Loading)
│
▼
验证 (Verification)
│
▼
准备 (Preparation)
│
▼
解析 (Resolution)
│
▼
初始化 (Initialization)
| 阶段 | 说明 |
|---|---|
| 加载 | 读取 .class 文件,生成 Class 对象 |
| 验证 | 校验字节码是否合法 |
| 准备 | 给静态变量分配内存,并设置默认值 |
| 解析 | 将符号引用转换为直接引用 |
| 初始化 | 执行静态变量赋值和 static 代码块 |
示例:
public static int num = 10;
执行过程:
准备阶段:num = 0
初始化阶段:num = 10
1.2 运行时数据区
运行时数据区是 JVM 内存结构的核心。
运行时数据区
├── 线程共享
│ ├── 堆 (Heap)
│ └── 方法区 (Method Area) / 元空间 (Metaspace)
│
└── 线程私有
├── Java 虚拟机栈 (JVM Stack)
├── 本地方法栈 (Native Method Stack)
└── 程序计数器 (Program Counter Register)
1.2.1 堆 (Heap)
堆用于存放对象实例,是 GC 的主要区域。
User user = new User();
其中 new User() 创建出来的对象主要存放在堆中。
堆通常分为:
Heap
├── 新生代 (Young Generation)
│ ├── Eden
│ ├── Survivor From
│ └── Survivor To
│
└── 老年代 (Old Generation)
1.2.2 方法区 / 元空间
方法区用于存放类级别信息。
常见内容:
| 内容 |
|---|
| 类信息 |
| 常量 |
| 静态变量 |
| 运行时常量池 |
| JIT 编译后的代码 |
JDK 8 之后,永久代被移除,改为 Metaspace 元空间。
元空间使用的是本地内存,不再直接使用 JVM 堆内存。
1.2.3 Java 虚拟机栈
每个线程都有自己的虚拟机栈。每调用一个方法,都会创建一个栈帧。
栈帧包含:
| 内容 | 说明 |
|---|---|
| 局部变量表 | 存放方法参数和局部变量 |
| 操作数栈 | 方法计算过程中的临时数据 |
| 动态链接 | 指向运行时常量池中的方法引用 |
| 方法返回地址 | 方法执行完后返回的位置 |
常见异常:StackOverflowError,典型原因是递归过深。
public void test() {
test();
}
1.2.4 程序计数器
程序计数器用于记录当前线程执行到哪一条字节码指令。
| 特点 |
|---|
| 线程私有 |
| 占用内存很小 |
| 唯一不会出现 OOM 的 JVM 区域 |
1.2.5 本地方法栈
本地方法栈用于执行 Native 方法。
private native void start0();
1.3 执行引擎
执行引擎负责执行字节码。
| 组件 | 作用 |
|---|---|
| 解释器 | 逐行解释执行字节码 |
| JIT 编译器 | 将热点代码编译成本地机器码 |
| 垃圾回收器 (GC) | 回收无用对象 |
1.4 -Xms 与 -Xmx 为什么设置一样大
-Xms 是 JVM 启动时的初始堆大小,-Xmx 是最大堆大小。
生产环境通常建议将两者设置为相同值,例如:
-Xms4g -Xmx4g
好处
| 好处 | 说明 |
|---|---|
| 避免堆动态扩缩容的开销 | 堆扩容时需要申请内存并可能触发 Full GC,缩容时也需要回收,影响性能 |
| 减少 GC 停顿次数 | 堆大小稳定后,GC 策略和频率更加可预测,避免因扩容引发的意外 Full GC |
| 避免内存碎片 | 频繁扩缩容会加剧堆内存碎片化,固定大小有利于内存管理 |
| 防止因扩容失败导致的 OOM | 操作系统内存不足时,扩容可能失败,固定大小可提前发现问题 |
| 性能稳定可预测 | 压测和生产环境堆大小一致,GC 行为一致,性能更稳定 |
堆扩容的代价
当 JVM 需要扩容堆时,会经历以下过程:
对象分配失败
│
▼
触发 Minor GC
│
▼
仍无法分配?
├── 否 ──► 正常分配
│
└── 是
│
▼
尝试扩容堆
│
▼
扩容成功?
├── 是 ──► 可能触发 Full GC(整理内存)
│
└── 否 ──► OOM
面试回答模板
生产环境建议将
-Xms和-Xmx设置为相同大小,主要出于三方面考虑:
第一,避免 JVM 运行时动态扩缩容带来的性能开销,扩容过程可能伴随 Full GC,影响响应时间;
第二,保证 GC 行为的稳定性和可预测性,堆大小固定后 GC 策略不会因堆变化而波动;
第三,提前暴露内存问题,如果设置的值不够用,压测阶段就能发现,而不是线上因扩容失败导致 OOM。
2. JMM 是什么
JMM 全称是 Java Memory Model,Java 内存模型。
注意:JMM 不是 JVM 的内存区域划分,而是 Java 对多线程并发访问共享变量定义的一套规范。
2.1 JVM 内存结构和 JMM 的区别
| 对比项 | JVM 内存结构 | JMM |
|---|---|---|
| 关注点 | JVM 内存区域如何划分 | 多线程如何读写共享变量 |
| 解决问题 | 对象、栈帧、类信息如何存储 | 可见性、原子性、有序性 |
| 典型内容 | 堆、栈、方法区、程序计数器 | volatile、synchronized、happens-before |
2.2 JMM 解决的三大问题
2.2.1 可见性
一个线程修改了共享变量,其他线程能否立即看到。
问题示例:
boolean flag = true;
while (flag) {
// do something
}
如果另一个线程修改 flag = false;,当前线程不一定马上感知到。
解决:
private volatile boolean flag = true;
2.2.2 原子性
一个操作是否不可被中断。
示例:
count++;
这不是原子操作,实际包含三步:
1. 读取 count
2. 加 1
3. 写回 count
解决方式:
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
或者:
synchronized void incr() {
count++;
}
2.2.3 有序性
编译器和 CPU 可能会进行指令重排序。
int a = 1;
int b = 2;
在单线程下不影响结果,但在多线程下可能产生并发问题。
2.3 happens-before 规则
happens-before 用来判断一个操作对另一个操作是否可见。
| 规则 | 说明 |
|---|---|
| 程序顺序规则 | 一个线程内,前面的操作 happens-before 后面的操作 |
| 锁规则 | unlock happens-before 后续对同一把锁的 lock |
| volatile 规则 | volatile 写 happens-before 后续 volatile 读 |
| 线程启动规则 | Thread.start() happens-before 线程内操作 |
| 线程终止规则 | 线程内操作 happens-before Thread.join() |
| 传递性 | A → B,B → C,则 A → C |
2.4 volatile 的作用
| 能力 | 是否支持 |
|---|---|
| 可见性 | ✓ |
| 有序性 | ✓ |
| 原子性 | ✗ |
所以:
volatile int count = 0;
count++; // 仍然不是线程安全的
3. Java 垃圾回收算法比较
3.1 对象如何判断可以被回收
Java 主要使用 可达性分析算法:从 GC Roots 出发,如果一个对象没有任何引用链可达,则可以被回收。
常见 GC Roots:
| GC Roots 类型 |
|---|
| 虚拟机栈中引用的对象 |
| 方法区中静态变量引用的对象 |
| 方法区中常量引用的对象 |
| Native 方法引用的对象 |
| 被 synchronized 锁持有的对象 |
3.2 标记-清除算法 (Mark-Sweep)
标记存活对象 ──► 清除未标记对象
| 优点 | 缺点 |
|---|---|
| 实现简单 | 会产生内存碎片 |
| 不需要移动对象 | 清理效率不稳定 |
适合:早期老年代回收思路。
3.3 复制算法 (Copying)
存活对象复制到另一块内存 ──► 清空原来的区域
| 优点 | 缺点 |
|---|---|
| 没有内存碎片 | 浪费一部分内存空间 |
| 回收效率高 | 存活对象多时复制成本高 |
适合:新生代(对象大多朝生夕死,存活对象少,复制成本低)。
3.4 标记-整理算法 (Mark-Compact)
标记存活对象 ──► 存活对象向一端移动 ──► 清理边界外内存
| 优点 | 缺点 |
|---|---|
| 没有内存碎片 | 移动对象成本较高 |
| 适合存活对象多的区域 | 通常需要暂停用户线程 |
适合:老年代。
3.5 分代收集算法
核心思想:不同生命周期的对象,使用不同的 GC 算法。
| 区域 | 特点 | 常用算法 |
|---|---|---|
| 新生代 | 对象存活率低 | 复制算法 |
| 老年代 | 对象存活率高 | 标记-清除 / 标记-整理 |
| 元空间 | 类元数据 | 类卸载 |
3.6 常见垃圾回收器比较
| 垃圾回收器 | 特点 | 适用场景 |
|---|---|---|
| Serial | 单线程,简单 | 客户端、小内存 |
| Parallel Scavenge | 吞吐量优先 | 后台计算、大批量任务 |
| CMS | 低停顿,老年代并发回收 | JDK 8 常见,但容易碎片化 |
| G1 | Region 分区,兼顾吞吐和低停顿 | JDK 9+ 默认,服务端常用 |
| ZGC | 超低延迟,支持大堆 | 大内存、低延迟系统 |
| Shenandoah | 超低停顿,并发整理 | 低延迟服务 |
3.7 G1 的核心特点
G1 将堆划分为多个 Region:
Heap
├── Region 1
├── Region 2
├── Region 3
└── ...
特点:
| 特点 |
|---|
| 支持可预测停顿时间 |
| 支持大堆内存 |
| 优先回收垃圾最多的 Region |
| 减少内存碎片 |
| 适合服务端应用 |
常用参数:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xms4g -Xmx4g
4. Java 线程池配置
Java 线程池核心类:ThreadPoolExecutor
构造方法核心参数:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
4.1 核心参数说明
| 参数 | 说明 |
|---|---|
| corePoolSize | 核心线程数 |
| maximumPoolSize | 最大线程数 |
| keepAliveTime | 非核心线程空闲存活时间 |
| unit | 时间单位 |
| workQueue | 任务队列 |
| threadFactory | 线程工厂 |
| handler | 拒绝策略 |
4.2 线程池执行流程
提交任务
│
▼
核心线程数未满?
├── 是 ──► 创建核心线程执行
│
└── 否
│
▼
队列未满?
├── 是 ──► 放入队列
│
└── 否
│
▼
最大线程数未满?
├── 是 ──► 创建非核心线程执行
│
└── 否 ──► 执行拒绝策略
4.3 线程数如何配置
CPU 密集型任务
| 场景 |
|---|
| 加密解密 |
| 图片处理 |
| 大量计算 |
| 复杂规则计算 |
推荐:
线程数 = CPU 核心数 或 CPU 核心数 + 1
示例(8 核 CPU):
corePoolSize = 8;
maximumPoolSize = 9;
IO 密集型任务
| 场景 |
|---|
| 调用第三方接口 |
| 查询数据库 |
| 访问 Redis |
| 文件上传下载 |
| MQ 消费处理 |
推荐公式:
线程数 = CPU 核心数 × (1 + IO等待时间 / CPU计算时间)
简化估算:
线程数 = CPU 核心数 × 2 ~ 4
示例(8 核 CPU):
corePoolSize = 16;
maximumPoolSize = 32;
最终配置应结合压测、CPU 使用率、队列长度、响应时间综合调整。
4.4 队列如何选择
| 队列 | 特点 | 场景 |
|---|---|---|
| ArrayBlockingQueue | 有界队列 | 推荐,防止 OOM |
| LinkedBlockingQueue | 可近似无界 | 不建议直接使用无界队列 |
| SynchronousQueue | 不存储任务,直接交给线程 | 高并发短任务 |
| PriorityBlockingQueue | 支持优先级 | 优先级任务 |
| DelayQueue | 延迟执行 | 延迟任务 |
生产建议:必须使用有界队列。
不建议:
Executors.newFixedThreadPool(10);
原因:底层使用无界队列,任务堆积可能导致 OOM。
4.5 拒绝策略
| 策略 | 说明 |
|---|---|
| AbortPolicy | 直接抛异常(默认策略) |
| CallerRunsPolicy | 由提交任务的线程执行 |
| DiscardPolicy | 直接丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 |
生产中常用:
new ThreadPoolExecutor.CallerRunsPolicy()
作用:对调用方形成反压,避免任务无限堆积。
4.6 生产级线程池示例
@Configuration
public class ThreadPoolConfig {
@Bean("bizExecutor")
public ThreadPoolExecutor bizExecutor() {
int cpu = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpu * 2,
cpu * 4,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactory() {
private final AtomicInteger index = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("biz-pool-" + index.getAndIncrement());
thread.setUncaughtExceptionHandler((t, e) ->
log.error("线程池执行异常, thread={}", t.getName(), e)
);
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
4.7 线程池配置原则
| 原则 | 说明 |
|---|---|
| 不使用 Executors 默认工厂 | 避免无界队列或无限线程 |
| 线程数根据任务类型配置 | CPU 密集型和 IO 密集型不同 |
| 队列必须有界 | 防止任务无限堆积 |
| 线程必须命名 | 方便日志排查 |
| 配置拒绝策略 | 防止系统被打爆 |
| 增加监控 | 监控 activeCount、queueSize、completedTaskCount |
| 异常要捕获 | 防止任务静默失败 |
5. execute 与 submit 对比
execute() 和 submit() 都可以向线程池提交任务,但它们的能力不同。
5.1 核心对比
| 对比项 | execute() | submit() |
|---|---|---|
| 所属接口 | Executor | ExecutorService |
| 是否有返回值 | 无 | 有,返回 Future |
| 支持 Runnable | ✓ | ✓ |
| 支持 Callable | ✗ | ✓ |
| 能否获取任务结果 | 不能 | 可以通过 Future.get() 获取 |
| 异常表现 | 异常直接抛到 UncaughtExceptionHandler | 异常封装进 Future,调用 get() 时抛出 |
| 适合场景 | 只执行任务,不关心结果 | 需要结果、异常感知、任务取消 |
5.2 execute 支持 Callable 吗
不支持。
方法签名:
void execute(Runnable command);
所以 execute() 只能接收 Runnable。
错误示例:
executor.execute(new Callable<String>() {
@Override
public String call() {
return "success";
}
});
原因:Callable 不是 Runnable,编译不通过。
正确用法:
executor.execute(() -> {
System.out.println("执行 Runnable 任务");
});
5.3 submit 支持 Runnable 吗
支持。submit() 支持三种形式:
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
方式一:提交 Runnable,没有业务返回值
Future<?> future = executor.submit(() -> {
System.out.println("Runnable task");
});
Object result = future.get();
System.out.println(result); // null
方式二:提交 Runnable,并指定固定返回值
Future<String> future = executor.submit(() -> {
System.out.println("Runnable task");
}, "success");
String result = future.get();
System.out.println(result); // success
注意:
"success"不是任务内部计算出来的结果,而是提交任务时指定的固定值。
方式三:提交 Callable,有真实返回值
Future<String> future = executor.submit(() -> {
return "callable result";
});
String result = future.get();
System.out.println(result); // callable result
5.4 Runnable 和 Callable 对比
| 对比项 | Runnable | Callable |
|---|---|---|
| 方法 | run() | call() |
| 返回值 | 无返回值 | 有返回值 |
| 异常 | 不能直接抛 checked exception | 可以抛 checked exception |
| 常用提交方式 | execute() / submit() | submit() |
接口定义:
@FunctionalInterface
public interface Runnable {
void run();
}
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
5.5 异常处理区别
execute 异常表现
executor.execute(() -> {
int i = 1 / 0;
});
异常会直接抛出,线程会终止,通常能在日志中看到。
submit 异常表现
Future<?> future = executor.submit(() -> {
int i = 1 / 0;
});
如果不调用 future.get(),异常可能不会明显暴露。
正确处理:
try {
future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
cause.printStackTrace();
}
submit()会把任务异常包装成ExecutionException。
5.6 submit 的底层逻辑
submit() 内部会将任务封装成 FutureTask,再调用 execute() 执行。
大致逻辑:
RunnableFuture<T> ftask = new FutureTask<>(callable);
execute(ftask);
return ftask;
因为 FutureTask 实现了 Runnable,所以最终仍然可以交给线程池执行。
6. OOM 问题如何处理
OOM 全称:OutOfMemoryError,表示 JVM 内存不足。
6.1 常见 OOM 类型
| OOM 类型 | 常见原因 |
|---|---|
| Java heap space | 堆内存不足,对象太多 |
| GC overhead limit exceeded | GC 频繁但回收效果差 |
| Metaspace | 类太多,动态代理太多 |
| Direct buffer memory | 直接内存不足 |
| unable to create new native thread | 线程太多 |
| Requested array size exceeds VM limit | 数组过大 |
6.2 排查 OOM 标准流程
① 确认 OOM 类型
│
▼
② 保留现场
│
▼
③ 导出 heap dump
│
▼
④ 分析 dump 文件
│
▼
⑤ 找到大对象或泄漏对象
│
▼
⑥ 分析对象引用链
│
▼
⑦ 定位具体代码
│
▼
⑧ 修复代码或调整参数
│
▼
⑨ 压测验证
6.3 生产建议 JVM 参数
JDK 9+:
-Xms4g -Xmx4g
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
-XX:+ExitOnOutOfMemoryError
-XX:+UseG1GC
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100M
JDK 8:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log
6.4 常见 OOM 场景和解决方案
集合无限增长
问题代码:
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
解决方案:
| 方案 |
|---|
| 限制集合大小 |
| 分页处理 |
| 批量处理后及时清理 |
| 使用队列削峰 |
| 避免全量加载 |
查询数据一次性加载太多
问题代码:
List<Order> orders = orderMapper.selectList(null);
如果订单有几百万条,可能直接打爆堆。
解决方式:
Page<Order> page = new Page<>(pageNo, pageSize);
orderMapper.selectPage(page, queryWrapper);
或者使用游标、分片、批处理。
线程池无界队列导致 OOM
问题代码:
Executors.newFixedThreadPool(10);
原因:底层使用无界 LinkedBlockingQueue,任务堆积后可能 OOM。
解决方式:
new ThreadPoolExecutor(
16, 32,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy()
);
Metaspace OOM
常见原因:
| 原因 |
|---|
| 动态生成类太多 |
| CGLIB 代理过多 |
| Groovy 动态脚本 |
| 频繁热部署 |
| ClassLoader 泄漏 |
参数控制:
-XX:MaxMetaspaceSize=512m
注意:不能只加内存,还要排查类加载泄漏。
Direct Memory OOM
常见于:
| 场景 |
|---|
| Netty |
| NIO |
| ByteBuffer.allocateDirect |
| 文件传输 |
| RocketMQ / Kafka 客户端 |
参数控制:
-XX:MaxDirectMemorySize=1g
同时要排查直接内存是否及时释放。
unable to create new native thread
常见原因:
| 原因 |
|---|
| 线程创建太多 |
| 线程池配置不合理 |
| 操作系统线程数限制 |
| 容器内存限制过小 |
解决方案:
| 方案 |
|---|
| 减少线程数 |
| 使用线程池复用 |
| 排查是否死循环创建线程 |
调整 Linux ulimit |
| 检查容器资源限制 |
6.5 常用排查命令
| 操作 | 命令 |
|---|---|
| 查看 Java 进程 | jps -l |
| 查看 JVM 参数 | jcmd <pid> VM.flags |
| 查看堆信息 | jmap -heap <pid> |
| 导出 dump | jmap -dump:format=b,file=/tmp/heap.hprof <pid> |
| 查看线程 | jstack <pid> > thread.txt |
| 查看 GC 情况 | jstat -gcutil <pid> 1000 10 |
6.6 常用分析工具
| 工具 | 作用 |
|---|---|
| MAT | 分析 heap dump |
| VisualVM | 可视化分析 JVM |
| Arthas | 在线诊断 |
| JProfiler | 商业性能分析 |
| GCViewer | 分析 GC 日志 |
7. RocketMQ、RabbitMQ、Kafka 对比
7.1 一句话总结
| MQ | 定位 |
|---|---|
| RabbitMQ | 传统消息队列,功能丰富,路由灵活 |
| RocketMQ | 金融级业务消息,事务消息、延迟消息强 |
| Kafka | 高吞吐分布式日志平台,适合大数据和流处理 |
7.2 核心对比表
| 对比项 | RabbitMQ | RocketMQ | Kafka |
|---|---|---|---|
| 开发语言 | Erlang | Java | Scala / Java |
| 核心模型 | Exchange + Queue | Topic + Queue | Topic + Partition |
| 吞吐量 | 中等 | 高 | 很高 |
| 延迟 | 低 | 较低 | 较低 |
| 消息顺序 | 支持 | 支持较好 | 分区内有序 |
| 延迟消息 | 插件或 TTL + 死信 | 原生支持 | 不原生,需额外设计 |
| 事务消息 | 支持但较弱 | 原生强支持 | 支持事务,偏流处理 |
| 消息堆积能力 | 一般 | 强 | 很强 |
| 路由能力 | 很强 | 一般 | 一般 |
| 运维复杂度 | 中等 | 中等偏高 | 较高 |
| 适合场景 | 业务解耦、复杂路由 | 订单、支付、交易 | 日志、埋点、大数据、流处理 |
7.3 RabbitMQ
核心架构:
Producer ──► Exchange ──► Queue ──► Consumer
Exchange 类型:
| 类型 | 说明 |
|---|---|
| Direct | 精确匹配 routing key |
| Topic | 通配符匹配 |
| Fanout | 广播 |
| Headers | 根据 header 匹配 |
| 优点 | 缺点 |
|---|---|
| 路由模型强大 | 大量消息堆积时性能下降明显 |
| 延迟低 | 吞吐不如 Kafka |
| 功能成熟 | Erlang 技术栈对 Java 团队运维门槛较高 |
| 社区生态好 | — |
| 支持确认机制 | — |
适合场景:
| 场景 |
|---|
| 普通业务异步解耦 |
| 邮件发送 |
| 短信发送 |
| 订单状态通知 |
| 复杂路由场景 |
7.4 RocketMQ
核心架构:
Producer ──► NameServer ──► Broker ──► Consumer
核心概念:
| 概念 | 说明 |
|---|---|
| Topic | 消息主题 |
| MessageQueue | Topic 下的队列 |
| Producer | 生产者 |
| Consumer | 消费者 |
| Broker | 消息存储节点 |
| NameServer | 注册中心 |
| 优点 | 缺点 |
|---|---|
| 支持事务消息 | 生态不如 Kafka 广 |
| 支持延迟消息 | 运维复杂度高于 RabbitMQ |
| 支持顺序消息 | 需理解 Broker、CommitLog、ConsumeQueue 等机制 |
| 消息堆积能力强 | — |
| 适合电商、支付、交易场景 | — |
| Java 技术栈友好 | — |
适合场景:
| 场景 |
|---|
| 订单超时关闭 |
| 支付结果通知 |
| 交易状态流转 |
| 分布式事务最终一致性 |
| 延迟任务 |
| 顺序消费 |
7.5 Kafka
核心架构:
Producer ──► Topic ──► Partition ──► Consumer Group
Kafka 的核心是分区日志:
Partition 0: msg1 → msg2 → msg3
Partition 1: msg4 → msg5 → msg6
| 优点 | 缺点 |
|---|---|
| 吞吐量极高 | 单条消息路由能力不如 RabbitMQ |
| 消息堆积能力极强 | 延迟消息不是强项 |
| 非常适合日志和流处理 | 业务事务消息不如 RocketMQ |
| 分区扩展能力强 | 运维复杂度较高 |
| 生态丰富(Flink、Spark、ClickHouse) | — |
适合场景:
| 场景 |
|---|
| 日志采集 |
| 用户行为埋点 |
| 大数据实时计算 |
| 流式处理 |
| 数据同步 |
| MySQL Binlog 消费链路 |
7.6 三者如何选择
| 场景 | 推荐 |
|---|---|
| 普通业务解耦 | RabbitMQ |
| 邮件、短信、通知 | RabbitMQ |
| 订单、支付、库存 | RocketMQ |
| 分布式事务最终一致性 | RocketMQ |
| 延迟任务、顺序消息 | RocketMQ |
| 日志、埋点、大数据 | Kafka |
| 实时计算、流处理 | Kafka |
| Binlog 数据同步 | Kafka |
8. 面试回答模板
8.1 JVM 组成
JVM 主要由类加载子系统、运行时数据区、执行引擎、本地方法接口和本地方法库组成。
其中运行时数据区分为线程共享和线程私有两部分。线程共享区域包括堆和方法区,线程私有区域包括虚拟机栈、本地方法栈和程序计数器。堆是 GC 的主要区域,方法区在 JDK 8 之后由元空间实现,虚拟机栈存储方法调用产生的栈帧。
8.2 JMM
JMM 是 Java 内存模型,不是 JVM 的堆、栈这些内存结构,而是 Java 针对多线程访问共享变量定义的一套规范。
它主要解决可见性、原子性和有序性问题。volatile可以保证可见性和有序性,但不能保证复合操作的原子性;synchronized可以保证原子性、可见性和有序性。JMM 还定义了happens-before规则,用于判断一个操作对另一个操作是否可见。
8.3 GC 算法
Java 判断对象是否可回收主要使用可达性分析算法,从 GC Roots 出发,没有引用链可达的对象就可以被回收。
常见垃圾回收算法有标记-清除、复制、标记-整理和分代收集。新生代对象存活率低,适合复制算法;老年代对象存活率高,适合标记-清除或标记-整理。服务端常用 G1,它通过 Region 分区管理堆,可以优先回收垃圾最多的区域,并支持可预测停顿时间。
8.4 线程池配置
线程池核心参数包括核心线程数、最大线程数、空闲线程存活时间、任务队列、线程工厂和拒绝策略。
线程池执行任务时,先创建核心线程;核心线程满了以后放入队列;队列满了再创建非核心线程;达到最大线程数后执行拒绝策略。生产环境不建议使用Executors默认线程池,因为可能存在无界队列或无限创建线程的问题。CPU 密集型任务线程数可以设置为 CPU 核心数或核心数加一,IO 密集型任务可以设置为 CPU 核心数的 2 到 4 倍,最终应结合压测和监控调整。
8.5 execute 和 submit
execute()是Executor接口的方法,只能提交Runnable,没有返回值,也不能直接获取任务执行结果。
submit()是ExecutorService的方法,可以提交Runnable,也可以提交Callable,并且会返回Future,可以通过Future.get()获取结果或捕获异常。
execute()不支持直接提交Callable;submit()支持Runnable,提交Runnable时Future.get()默认返回null,也可以通过submit(Runnable task, T result)指定固定返回值。
8.6 OOM 排查
OOM 需要先判断是哪类内存溢出,例如
Java heap space、Metaspace、Direct buffer memory或unable to create new native thread。
生产环境应开启HeapDumpOnOutOfMemoryError并保留 GC 日志。排查时先看错误类型,再分析 heap dump,找出占用内存最多的对象和引用链。常见原因包括集合无限增长、一次性查询大量数据、线程池无界队列堆积、动态类过多、直接内存未释放等。解决时不能只加内存,还要定位代码中的对象泄漏或资源释放问题。
8.7 MQ 对比
RabbitMQ、RocketMQ、Kafka 的定位不同。
RabbitMQ 路由能力强,适合普通业务异步解耦和复杂路由;RocketMQ 对交易场景支持更好,原生支持事务消息、延迟消息和顺序消息,适合订单、支付、库存等场景;Kafka 本质上是分布式日志系统,吞吐量和消息堆积能力非常强,适合日志采集、用户行为埋点、大数据和流式处理。实际选型时,普通业务通知可以选 RabbitMQ,电商交易链路可以选 RocketMQ,日志和大数据链路可以选 Kafka。
9. 最终记忆版
JVM
├── 类加载、运行时数据区、执行引擎、JNI、本地库
├── 运行时数据区
│ ├── 线程共享:堆、方法区 / 元空间
│ └── 线程私有:虚拟机栈、本地方法栈、程序计数器
JMM
├── Java 内存模型,解决多线程下可见性、原子性、有序性问题
├── volatile:保证可见性和有序性,不保证原子性
└── synchronized:三者都能保证
GC
├── 判断对象是否回收:可达性分析
├── 算法:标记清除、复制、标记整理、分代收集
├── 新生代 → 复制算法,老年代 → 标记整理
└── 常用回收器:G1、ZGC、Parallel、CMS
线程池
├── 核心参数:corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler
├── CPU 密集型:CPU 核心数 + 1
├── IO 密集型:CPU 核心数 × 2 ~ 4
└── 生产必须用有界队列,避免 Executors 默认线程池
execute / submit
├── execute:只支持 Runnable,没有返回值
├── submit:支持 Runnable 和 Callable,返回 Future
└── submit 的异常会封装到 Future,调用 get() 时抛出
OOM
├── 先看类型 → 保留现场 → 导出 dump → 分析引用链
└── 常见原因:集合过大、全量查询、无界队列、线程过多、Metaspace 泄漏、DirectMemory 泄漏
MQ
├── RabbitMQ:路由强,适合业务解耦
├── RocketMQ:事务消息、延迟消息强,适合订单支付
└── Kafka:吞吐高、堆积强,适合日志和大数据