JVM(Java 虚拟机)与线上排障
正式学习入口
本模块已经按知识图谱编号拆分为可独立复习的精通级分册。建议先建立运行时、类和对象三种生命周期,再学习收集器、日志参数和事故排查;旧版正文继续保留在本页下方,用于兼容既有链接和快速检索。
- 00-知识图谱与复习路线
- 01-运行时内存与对象创建
- 02-类文件、类加载与执行引擎
- 03-GC(垃圾回收)基础算法与对象生命周期
- 04-垃圾收集器与版本演进
- 05-内存、日志、参数与容器化
- 06-线上排障与事故复盘
- 07-项目案例与面试话术
- 08-综合面试题库
兼容保留的旧版正文
1. 简历关联点
简历中写到“熟悉 JVM(Java 虚拟机)底层原理和 GC(垃圾回收)常用算法,有 OOM(内存溢出)问题排查经验、CPU(中央处理器)飙高排查及生产事故处理经验”。面试官会重点追问:
- JVM(Java 虚拟机)内存区域分别存什么。
- JVM(Java 虚拟机)生命周期从启动到退出经历哪些阶段。
- 对象如何创建、分配、晋升、回收。
- 类生命周期和对象生命周期分别是什么。
- Minor GC(年轻代垃圾回收)、Major(老年代)GC(垃圾回收)、Full GC(完全垃圾回收)区别。
- OOM(内存溢出)如何定位,是堆、元空间、直接内存还是线程栈。
- CPU(中央处理器)飙高如何定位到具体线程和代码。
- 线上事故如何止血、定位、复盘。
2. 面试主线
回答 JVM(Java 虚拟机)问题时,建议按“JVM(Java 虚拟机)生命周期 -> 运行时内存 -> 类生命周期 -> 对象生命周期 -> GC(垃圾回收) -> 监控指标 -> 排障工具 -> 项目经验”展开。
- JVM(Java 虚拟机)生命周期从启动、初始化、执行 main(主方法)、运行期管理,到正常退出或异常终止。
- JVM(Java 虚拟机)把运行时数据划分为线程私有区和线程共享区。
- 类生命周期经历加载、验证、准备、解析、初始化、使用、卸载。
- 对象主要分配在堆,经过 Eden(伊甸园区)、Survivor(幸存者区)、Old(老年代)流转。
- GC(垃圾回收)通过可达性分析判断对象是否存活,再用复制、标记清除、标记整理等算法回收。
- 线上排障先看现象和指标,再用 jstack(线程栈工具)、jmap(内存映射工具)、jstat(虚拟机统计工具)、Arthas(Java 诊断工具)定位。
- 项目经验要讲“现象、止血、定位、根因、修复、复盘”,不能只说“调大内存”。
3. 基础知识
3.1 JVM(Java 虚拟机)运行时内存结构
flowchart TD
JVM["JVM(Java 虚拟机)运行时数据区"] --> PC["程序计数器\n线程私有"]
JVM --> STACK["虚拟机栈\n线程私有"]
JVM --> NATIVE["本地方法栈\n线程私有"]
JVM --> HEAP["堆\n线程共享"]
JVM --> META["方法区/元空间\n线程共享"]
HEAP --> YOUNG["年轻代"]
YOUNG --> EDEN["Eden(伊甸园区)"]
YOUNG --> S0["Survivor0(幸存者区0)"]
YOUNG --> S1["Survivor1(幸存者区1)"]
HEAP --> OLD["Old(老年代)"]
META --> CLASS["类元数据"]
META --> CONST["运行时常量池"]| 区域 | 是否线程私有 | 存储内容 | 常见异常 |
|---|---|---|---|
| 程序计数器 | 是 | 当前线程执行的字节码位置 | 几乎不会 OOM(内存溢出) |
| 虚拟机栈 | 是 | 栈帧、局部变量表、操作数栈 | StackOverflowError(栈溢出错误) |
| 本地方法栈 | 是 | Native(本地)方法调用 | StackOverflowError(栈溢出错误) |
| 堆 | 否 | 对象实例、数组 | OutOfMemoryError(内存溢出错误) |
| 元空间 | 否 | 类元数据、方法元信息 | Metaspace(元空间)OOM(内存溢出) |
| 直接内存 | 否 | NIO(新输入输出)直接缓冲区 | Direct buffer memory(直接缓冲内存)OOM(内存溢出) |
热门面试题
问题(基础题):JVM(Java 虚拟机)的堆、栈和元空间分别存什么?
- 考点:线程私有与共享、对象、栈帧、类元数据。
- 回答思路:堆放对象实例,栈按线程和方法保存执行现场,元空间保存类元数据,再补充直接内存不在堆内。
- 详细答案:堆是线程共享区域,主要保存对象和数组,是 GC(垃圾回收)重点管理空间;每个 Java(编程语言)线程拥有自己的虚拟机栈,方法每次调用都会创建栈帧,里面有局部变量表、操作数栈、动态链接和返回信息;元空间使用本地内存保存类、方法和常量池等元数据。程序计数器记录当前线程执行位置,直接内存则常被 NIO(新输入输出)缓冲区使用。不同区域都可能耗尽,但异常类型和排查证据不同。
- 进阶追问:局部变量引用的对象放在栈还是堆?
- 进阶回答:局部变量表通常保存引用值,普通对象实例通常在堆中;JIT(即时编译)通过逃逸分析可能进行标量替换,使对象不再以完整堆对象存在,但不能把它简单背成“所有局部对象都在栈上”。
问题(原理题):一次方法调用在内存中如何执行?
- 考点:栈帧、局部变量表、操作数栈、方法返回。
- 回答思路:调用时压入栈帧,字节码在局部变量表和操作数栈间运算,返回时弹出。
- 详细答案:线程调用方法时创建并压入栈帧,参数和局部变量进入局部变量表;加载、计算和调用字节码把数据压入或弹出操作数栈;动态链接把符号方法引用关联到实际目标;正常返回或抛出异常时恢复调用方现场并弹出栈帧。递归层次过深会持续创建栈帧,最终出现 StackOverflowError(栈溢出错误);线程过多则每个线程栈一起消耗本地内存。
- 进阶追问:为什么把线程栈设置得很大也有风险?
- 进阶回答:单线程可容纳更深调用,但在固定容器内存下,每个线程预留更多栈空间会降低可创建线程数,可能更早出现无法创建本地线程或被容器 OOM(内存溢出) Killer(终止器)终止。应先修复异常递归并控制线程池,再按调用深度调整。
问题(项目追问题):容器明明给了 2 GB(吉字节),为什么堆只配 1.2 GB(吉字节)仍可能被杀?
- 考点:堆外开销、线程栈、元空间、直接内存和容器限制。
- 回答思路:容器限制的是进程总内存,不能只计算最大堆。
- 详细答案:进程总占用还包括 Metaspace(元空间)、Code Cache(代码缓存)、线程栈、DirectBuffer(直接缓冲区)、JNI(Java 本地接口)内存、GC(垃圾回收)结构和本地库。若线程很多、HTTP(超文本传输协议)客户端重复创建直接缓冲或类加载器泄漏,即使堆未达到上限,RSS(常驻内存集)也可能超过容器限制而被操作系统终止,并且来不及生成 Java(编程语言)堆 OOM(内存溢出)转储。
- 进阶追问:如何区分 JVM(Java 虚拟机)OOM(内存溢出)和容器 OOM(内存溢出)?
- 进阶回答:前者通常有 OutOfMemoryError(内存溢出错误)、GC(垃圾回收)日志和可能的 heap dump(堆转储);后者常表现为进程退出码 137、容器事件显示 OOMKilled(容器内存杀死),应用日志突然中断。要同时看容器工作集、进程本地内存和堆指标。
3.2 JVM(Java 虚拟机)生命周期
JVM(Java 虚拟机)生命周期不是只包含“运行 Java(编程语言)代码”。从操作系统启动 Java(编程语言)进程开始,到 JVM(Java 虚拟机)退出结束,中间会经历启动、初始化、运行、关闭四个大阶段。
flowchart TD
A["操作系统启动 Java(编程语言)进程"] --> B["创建 JVM(Java 虚拟机)实例"]
B --> C["初始化运行时环境\n堆/栈/方法区/线程系统"]
C --> D["加载启动类和主类"]
D --> E["执行 main(主方法)"]
E --> F["运行期:类加载/解释执行/JIT(即时编译)/GC(垃圾回收)"]
F --> G{"是否满足退出条件?"}
G -->|否| F
G -->|是| H["执行 Shutdown Hook(关闭钩子)"]
H --> I["释放资源并退出 JVM(Java 虚拟机)进程"]JVM(Java 虚拟机)生命周期阶段拆解:
| 阶段 | 发生什么 | 面试关注 |
|---|---|---|
| 启动阶段 | 操作系统创建 Java(编程语言)进程,加载 JVM(Java 虚拟机)动态库,创建 JVM(Java 虚拟机)实例 | JVM(Java 虚拟机)是进程内运行时,不是单独的操作系统进程管理器 |
| 初始化阶段 | 初始化堆、栈、方法区、类加载器、GC(垃圾回收)线程、JIT(即时编译)线程 | 内存参数和 GC(垃圾回收)参数在此阶段生效 |
| 主类加载阶段 | Bootstrap-ClassLoader(启动类加载器)、Platform-ClassLoader(平台类加载器)、Application-ClassLoader(应用类加载器)协作加载主类 | 双亲委派和类路径问题 |
| 执行阶段 | 调用 main(主方法),字节码解释执行,热点代码触发 JIT(即时编译),对象分配和 GC(垃圾回收)持续发生 | 性能优化、JIT(即时编译)、GC(垃圾回收)、线程状态 |
| 关闭阶段 | 非 daemon(守护线程)结束、System.exit(系统退出)、收到 SIGTERM(终止信号)等触发关闭 | Shutdown Hook(关闭钩子)、资源释放、优雅停机 |
| 强制终止 | kill -9(强杀信号)、Runtime.halt(强制停止运行时)、JVM(Java 虚拟机)崩溃 | Shutdown Hook(关闭钩子)可能不执行 |
JVM(Java 虚拟机)退出条件:
- main(主方法)执行结束,并且所有 non-daemon thread(非守护线程)都结束。
- 代码主动调用 System.exit(系统退出)。
- 操作系统发送 SIGTERM(终止信号)并触发正常关闭流程。
- JVM(Java 虚拟机)发生 fatal error(致命错误),例如本地内存崩溃。
- 操作系统发送 kill -9(强杀信号),此时 JVM(Java 虚拟机)没有机会执行 Shutdown Hook(关闭钩子)。
daemon thread(守护线程)和 non-daemon thread(非守护线程)区别:
| 线程类型 | 是否阻止 JVM(Java 虚拟机)退出 | 典型场景 | 风险 |
|---|---|---|---|
| non-daemon thread(非守护线程) | 会阻止 | 业务线程、线程池工作线程 | 未关闭线程池会导致应用无法退出 |
| daemon thread(守护线程) | 不会阻止 | GC(垃圾回收)线程、监控辅助线程 | JVM(Java 虚拟机)退出时可能被直接终止 |
Shutdown Hook(关闭钩子)适合做优雅停机,比如关闭线程池、停止消费 MQ(消息队列)、刷出内存队列、释放文件句柄。但 Shutdown Hook(关闭钩子)不能写太重的逻辑,也不能假设它一定执行。
sequenceDiagram
participant OS as OS(操作系统)
participant JVM as JVM(Java 虚拟机)
participant APP as App(应用)
participant HOOK as Shutdown Hook(关闭钩子)
OS->>JVM: SIGTERM(终止信号)
JVM->>APP: 标记进入 shutdown(关闭)流程
JVM->>HOOK: 并发执行已注册 Hook(钩子)
HOOK->>HOOK: 停止接收新任务/关闭线程池/释放资源
HOOK-->>JVM: Hook(钩子)执行完成
JVM->>OS: 进程退出热门面试题
问题(基础题):main(主方法)结束后 JVM(Java 虚拟机)一定退出吗?
- 考点:守护线程与非守护线程、退出条件。
- 回答思路:JVM(Java 虚拟机)是否退出取决于是否仍有存活的 non-daemon thread(非守护线程)。
- 详细答案:main(主方法)线程结束只代表一个非守护线程结束。如果业务线程池、定时任务或未关闭客户端仍有 non-daemon thread(非守护线程),JVM(Java 虚拟机)会继续运行;仅剩 daemon thread(守护线程)时可以退出,守护线程不会被保证完成清理。因此应用必须显式关闭线程池,不能通过全部设成守护线程掩盖资源管理问题。
- 进阶追问:为什么线程池常导致应用停止卡住?
- 进阶回答:核心工作线程通常是非守护线程,并可能一直等待队列任务。关闭时要先停止接收、调用 shutdown(平滑关闭)、等待有界时间,超时后再取消可中断任务,同时持久化未完成任务状态。
问题(原理题):JVM(Java 虚拟机)从启动到退出经历哪些关键阶段?
- 考点:运行时初始化、类加载、解释与编译、关闭流程。
- 回答思路:按进程启动、运行时初始化、主类加载、执行期和关闭期回答。
- 详细答案:操作系统创建进程并加载 JVM(Java 虚拟机)运行库;运行时初始化内存区域、线程系统、类加载器、GC(垃圾回收)和 JIT(即时编译)基础设施;加载并初始化主类后执行 main(主方法)。运行期持续发生类加载、字节码解释、热点编译、对象分配和回收。正常退出或收到 SIGTERM(终止信号)时进入关闭流程并执行已注册 Shutdown Hook(关闭钩子);kill -9(强杀信号)或进程崩溃不会保证这一步。
- 进阶追问:Shutdown Hook(关闭钩子)之间有固定顺序吗?
- 进阶回答:不能依赖注册先后形成严格业务顺序,多个钩子可能并发执行。需要顺序的资源释放应由一个统一生命周期管理器编排,并设置超时,不能在钩子之间建立隐式依赖。
问题(项目追问题):支付或 Runner(执行器)任务如何应对 JVM(Java 虚拟机)随时退出?
- 考点:优雅停机不可靠边界、状态持久化、租约和幂等。
- 回答思路:正常停机尽量排空,强制退出依靠可恢复状态和幂等兜底。
- 详细答案:收到终止信号后先从注册中心摘流量,停止拉取新任务,等待在途任务到安全检查点,释放租约并关闭资源。但由于强杀和节点故障不可避免,任务领取必须有到期租约,关键步骤持久化状态和检查点,副作用以任务号或支付事件号幂等;其他实例在租约过期后恢复执行。关闭钩子是减少中断概率的优化,不是数据可靠性的唯一保证。
- 进阶追问:任务执行成功但来不及更新状态就退出怎么办?
- 进阶回答:恢复实例可能再次执行,所以实际副作用必须幂等,例如数据库唯一键、状态机条件更新或第三方 Idempotency Key(幂等键);重试时先查询已有结果,再决定是否补做。
3.3 类生命周期
类生命周期比“类加载过程”更完整,包含加载、链接、初始化、使用、卸载。
flowchart LR
A["Loading(加载)"] --> B["Linking(链接)"]
B --> B1["Verification(验证)"]
B1 --> B2["Preparation(准备)"]
B2 --> B3["Resolution(解析)"]
B3 --> C["Initialization(初始化)"]
C --> D["Using(使用)"]
D --> E["Unloading(卸载)"]| 阶段 | 作用 | 例子 |
|---|---|---|
| Loading(加载) | 读取 Class(类元数据)字节码,生成 Class(类元数据)对象 | 从 classpath(类路径)、jar(归档包)、网络加载 |
| Verification(验证) | 验证字节码安全性 | 防止非法字节码破坏 JVM(Java 虚拟机) |
| Preparation(准备) | 给 static(静态关键字)变量分配内存并设置默认值 | static int a = 1 此时先是 0 |
| Resolution(解析) | 符号引用转直接引用 | 方法、字段、类引用解析 |
| Initialization(初始化) | 执行 <clinit> 类初始化方法 | static(静态关键字)变量赋值和 static-block(静态代码块) |
| Using(使用) | 对象创建、方法调用、字段访问 | 业务运行期 |
| Unloading(卸载) | 类加载器可回收,类元数据释放 | 热部署、动态代理、脚本引擎 |
类初始化触发条件:
- new(创建对象)一个类实例。
- 读取或设置类的 static(静态关键字)字段,final(最终关键字)编译期常量除外。
- 调用类的 static(静态关键字)方法。
- 反射调用类。
- 初始化子类前,先初始化父类。
- JVM(Java 虚拟机)启动时初始化 main(主方法)所在类。
类卸载必须同时满足:
- 该类所有实例都不可达。
- 加载该类的 ClassLoader(类加载器)不可达。
- 该类对应的 Class(类元数据)对象不可达。
线上如果频繁动态生成类,但 ClassLoader(类加载器)被静态集合或线程上下文引用持有,就可能导致 Metaspace(元空间)OOM(内存溢出)。
热门面试题
问题(基础题):类的加载、链接和初始化有什么区别?
- 考点:类生命周期阶段边界。
- 回答思路:加载生成类元数据,链接验证并准备引用,初始化执行静态赋值和静态代码块。
- 详细答案:Loading(加载)读取字节码并在 JVM(Java 虚拟机)中建立类元数据和 Class(类元数据)对象;Linking(链接)包含 Verification(验证)字节码、Preparation(准备)静态字段默认值和 Resolution(解析)符号引用;Initialization(初始化)执行类初始化方法,按代码顺序完成 static(静态关键字)显式赋值和 static-block(静态代码块)。类被加载不代表已经初始化,编译期常量读取等场景可能不触发初始化。
- 进阶追问:
static int a = 1在准备阶段是什么值? - 进阶回答:普通静态字段在 Preparation(准备)阶段先获得默认值 0,在 Initialization(初始化)阶段执行赋值后变成 1;编译期常量可能通过常量属性在准备阶段得到值,面试回答要说明例外。
问题(原理题):父类和子类的静态代码、实例字段、构造器按什么顺序执行?
- 考点:类初始化先后与对象构造链。
- 回答思路:先初始化父类再子类;创建对象时先父类实例初始化和构造,再子类。
- 详细答案:首次主动使用子类时,先完成父类的类初始化,再按子类静态字段和静态代码块的源码顺序执行。创建实例时对象内存先有默认值,然后进入构造链:父类实例字段初始化与实例代码块、父类构造器,再到子类实例字段初始化与实例代码块、子类构造器。构造期间调用可覆盖方法存在风险,因为子类字段可能尚未初始化。
- 进阶追问:多个线程同时触发同一个类初始化会执行多次吗?
- 进阶回答:JVM(Java 虚拟机)保证同一类初始化过程被同步控制,只有一个线程执行类初始化方法,其他线程等待;如果初始化失败,该类可能进入错误状态,后续使用抛出相关初始化错误,而不是反复安全重试。
问题(项目追问题):为什么热部署或动态代理可能导致 Metaspace(元空间)OOM(内存溢出)?
- 考点:类卸载条件、ClassLoader(类加载器)泄漏。
- 回答思路:类元数据只有在其类加载器和相关实例都不可达时才可卸载。
- 详细答案:每轮热部署创建新的 ClassLoader(类加载器)并加载一批类;如果旧类加载器仍被 static(静态关键字)集合、ThreadLocal(线程本地变量)、线程上下文类加载器、驱动注册或监听器引用,整批类元数据都无法卸载。动态生成代理类数量无界也会持续消耗元空间。排查时查看类加载数量、卸载趋势和 class histogram(类直方图),并分析旧类加载器引用链。
- 进阶追问:只调大 MaxMetaspaceSize(最大元空间大小)能解决吗?
- 进阶回答:只能推迟故障。若类加载数量持续上升且卸载很少,应注销监听器、清理 ThreadLocal(线程本地变量)、关闭自建线程并修复类加载器引用;动态类生成还要做缓存和数量上限。
3.4 对象生命周期
对象创建大致经过:
- 类加载检查:检查 Class(类元数据)是否已加载、解析、初始化。
- 分配内存:指针碰撞或空闲列表。
- 初始化零值:保证对象字段有默认值。
- 设置对象头:记录 Mark Word(标记字)、类型指针、数组长度。
- 执行构造方法:执行
<init>初始化业务字段。
对象完整生命周期:
flowchart TD
A["类已加载并初始化"] --> B["分配对象内存"]
B --> C["字段零值初始化"]
C --> D["设置对象头\nMark Word(标记字)/类型指针"]
D --> E["执行 <init>(构造方法)"]
E --> F["对象被引用并使用"]
F --> G{"是否仍被 GC Roots(垃圾回收根)可达?"}
G -->|是| F
G -->|否| H["进入可回收状态"]
H --> I["GC(垃圾回收)回收内存"]对象生命周期常见面试点:
- 对象不一定都在堆上分配,JIT(即时编译)可能通过逃逸分析做栈上分配或标量替换。
- 对象从“不可达”到“真正回收”不是立刻发生,要等 GC(垃圾回收)。
- finalize(终结方法)机制不可靠且已不推荐使用,不能依赖它释放核心资源。
- 对象如果被 ThreadLocal(线程本地变量)、static(静态关键字)集合、缓存、未关闭线程引用,就可能长期不可回收。
热门面试题
问题(基础题):new(创建对象)一个对象时 JVM(Java 虚拟机)做了什么?
- 考点:类检查、内存分配、零值、对象头和构造器。
- 回答思路:按类是否初始化、分配空间、清零、设置对象头、执行构造方法回答。
- 详细答案:先检查目标类是否已经加载、链接和初始化;随后在堆中通过指针碰撞或空闲列表分配空间,并把实例字段初始化为零值;设置 Mark Word(标记字)、类型指针和数组长度等对象头信息;最后执行构造方法,其中会先调用父类构造并按顺序给字段赋业务值。构造方法执行前对象已有默认值,但尚未成为完整业务对象。
- 进阶追问:构造过程中把 this(当前对象引用)发布给其他线程有什么风险?
- 进阶回答:其他线程可能在构造完成前看到对象,读取到默认值或不完整状态,称为 this(当前对象引用) escape(逸出)。不要在构造器中注册监听器、启动线程或写入全局集合;先完整构造,再通过安全发布交付。
问题(原理题):多线程同时创建对象,堆分配如何避免竞争?
- 考点:TLAB(线程本地分配缓冲区)、指针碰撞和慢路径。
- 回答思路:大多数小对象在线程私有缓冲区快速分配,缓冲不足再进入共享协调路径。
- 详细答案:JVM(Java 虚拟机)通常给每个线程划分 TLAB(线程本地分配缓冲区),线程只移动自己的分配指针,无需每个对象都争用全局锁。TLAB(线程本地分配缓冲区)不足或对象过大时进入慢路径,从共享堆区域通过 CAS(比较并交换)或其他同步方式分配。快速分配并不代表对象在栈上,TLAB(线程本地分配缓冲区)仍属于堆。
- 进阶追问:大对象为什么容易带来问题?
- 进阶回答:大对象可能绕过普通年轻代路径,直接占用连续老年代或 Humongous(大对象区域),分配和回收成本高,短时间大量出现会造成碎片、并发标记提前和疏散失败。文件与报表应流式处理,避免巨型字节数组。
问题(项目追问题):ThreadLocal(线程本地变量)为什么会让任务对象长期存活?
- 考点:线程生命周期、ThreadLocalMap(线程本地映射)、弱键强值。
- 回答思路:线程池线程长期存在,键可能消失但值仍被线程内部映射强引用。
- 详细答案:每个线程持有 ThreadLocalMap(线程本地映射),ThreadLocal(线程本地变量)键是弱引用,但值通常是强引用。键被回收后,值不会立刻自动消失,只有后续访问触发清理或线程结束才可能释放;线程池线程长期不结束,订单上下文、字节数组等就可能一直存活。必须在 finally(最终清理)代码块中 remove(移除),并避免存放大对象。
- 进阶追问:为什么把 key(键)设计成弱引用仍不能彻底避免泄漏?
- 进阶回答:弱引用只允许 ThreadLocal(线程本地变量)对象本身回收,ThreadLocalMap(线程本地映射)的条目值仍由线程强引用。没有访问触发清理时,陈旧条目会伴随线程长期存在,所以主动 remove(移除)才是确定性做法。
3.5 类加载过程
类加载流程:
flowchart LR
A["Loading(加载)"] --> B["Verification(验证)"]
B --> C["Preparation(准备)"]
C --> D["Resolution(解析)"]
D --> E["Initialization(初始化)"]
B -.-> F["Linking(链接)"]
C -.-> F
D -.-> F双亲委派模型:
flowchart TD
A["应用类加载器"] -->|向上委派| B["平台类加载器"]
B -->|向上委派| C["启动类加载器"]
C -->|找不到再向下| B
B -->|找不到再向下| A双亲委派的价值是避免核心类被重复加载或恶意替换,比如避免自定义 java.lang.String 破坏 Java(编程语言)基础类库。
热门面试题
问题(基础题):双亲委派模型解决什么问题?
- 考点:类唯一性、核心类保护、加载顺序。
- 回答思路:类加载请求先交给父加载器,父找不到再由当前加载器处理。
- 详细答案:应用类加载器收到请求后先委派给父级,最终由启动类加载器优先尝试核心类;上级找不到时才逐级向下加载。这避免同一加载器体系重复定义核心类型,也防止应用自带同名
java.lang.String替换基础类。类身份由“全限定名加定义它的 ClassLoader(类加载器)”共同决定,仅类名相同不保证可互相转换。 - 进阶追问:双亲委派是不是绝对不能打破?
- 进阶回答:不是。SPI(服务提供者接口)、模块化容器和热部署需要父级代码发现子级实现,可能使用线程上下文类加载器或自定义加载顺序。但打破后必须管理版本隔离、类冲突和卸载,否则容易出现类型转换异常和元空间泄漏。
问题(原理题):为什么同名类会出现 ClassCastException(类型转换异常)?
- 考点:类身份与 ClassLoader(类加载器)隔离。
- 回答思路:同名只是文本相同,不同定义类加载器会产生不同运行时类型。
- 详细答案:JVM(Java 虚拟机)判断类型时同时看全限定名和定义类加载器。插件甲和插件乙各自加载一份
Order,即使字节码完全相同,运行时也视为两个类型;把甲的对象强转为乙的类型就会失败。共享接口应由共同父加载器加载,插件只加载实现,跨边界数据最好使用稳定接口或序列化结构。 - 进阶追问:如何排查类冲突?
- 进阶回答:打印目标类的来源位置和 ClassLoader(类加载器)层级,检查依赖树中重复版本;使用类加载日志观察实际由谁加载。不要只删除一个归档包碰运气,应明确哪一层拥有共享类。
问题(项目追问题):Spring Boot(快速开发框架)应用出现 NoClassDefFoundError(找不到类定义错误)怎么排查?
- 考点:编译期与运行期类路径差异、初始化失败和版本冲突。
- 回答思路:先区分类从未找到、类初始化失败和方法版本不兼容。
- 详细答案:检查完整异常链和缺失类名,确认打包产物是否包含依赖、依赖作用域是否错误、运行环境是否加载旧版本;若前面先出现 ExceptionInInitializerError(类初始化异常),后续 NoClassDefFoundError(找不到类定义错误)可能是同一类初始化失败后的结果。若是 NoSuchMethodError(找不到方法错误),通常是编译与运行依赖版本不一致。通过依赖树、归档包内容和类加载来源定位。
- 进阶追问:如何防止上线后才发现?
- 进阶回答:在与生产一致的 Java(编程语言)版本和打包方式下做启动烟测,执行关键自动配置和渠道客户端初始化;锁定依赖版本并在持续集成中检查冲突,避免本地开发类路径掩盖产物缺失。
4. 底层原理
4.1 对象存活判断
主流 JVM(Java 虚拟机)使用可达性分析,从 GC-Roots(垃圾回收根)出发,能被引用链到达的对象就是存活对象。
常见 GC-Roots(垃圾回收根):
- 虚拟机栈中的局部变量引用。
- 方法区中静态变量引用。
- 方法区中常量引用。
- JNI(Java 本地接口)引用。
- synchronized(同步锁)持有的对象。
可达性分析把堆中的对象关系看成一张有向图。GC-Roots(垃圾回收根)是图搜索起点,从这些起点沿引用能够访问到的对象都会被标记为存活;无法到达的对象才进入回收候选集。
flowchart TD
R1["线程栈局部变量<br/>GC Roots(垃圾回收根)"] --> A["订单任务 A"]
R2["static(静态关键字)缓存<br/>GC Roots(垃圾回收根)"] --> B["缓存容器 B"]
A --> C["订单列表 C"]
B --> D["配置对象 D"]
X["临时字节数组 X"] --> Y["包装对象 Y"]
C --> Z["明细对象 Z"]
subgraph 可回收候选
X
Y
end上图中,即使 X 和 Y 互相引用,只要没有任何 GC-Roots(垃圾回收根)能够到达它们,整个环仍然可以回收。这也是 Java(编程语言)虚拟机不采用单纯引用计数作为主要对象存活算法的重要原因:引用计数很难自然识别循环引用。
热门面试题
问题(基础题):JVM(Java 虚拟机)如何判断一个对象已经死亡?
- 考点:可达性分析、GC-Roots(垃圾回收根)、引用链。
- 回答思路:先说明从 GC-Roots(垃圾回收根)做图搜索,再说明不可达只是进入回收候选,最后补充引用类型和终结机制边界。
- 详细答案:主流 JVM(Java 虚拟机)使用可达性分析。回收器在安全点获得能够作为起点的引用集合,包括线程栈局部变量、静态字段、常量、JNI(Java 本地接口)引用和被监视器持有的对象,然后沿对象字段向下扫描并标记所有可达对象。未被标记的对象与运行中程序没有可见引用关系,因此可作为回收候选。不可达不等于立刻释放:具体何时清理取决于回收器阶段,弱引用等特殊引用还会经过引用处理。finalize(终结方法)机制已不推荐依赖,因为执行时机不确定、会增加回收成本,还可能造成对象意外复活。
- 进阶追问:两个对象互相引用,为什么仍然可以被回收?
- 进阶回答:可达性分析关心的是“能否从 GC-Roots(垃圾回收根)到达”,不是对象内部引用数量。对象
A引用B、B引用A,但如果外部根到它们的路径已经断开,这个闭环整体仍不可达,因此可以一起回收;单纯引用计数则会看到二者计数都不为零,容易误判存活。
问题(原理题):GC-Roots(垃圾回收根)为什么不能把整个堆随意扫描一遍就结束?
- 考点:一致性快照、安全点、并发标记。
- 回答思路:对象图在业务线程运行时持续变化,必须解决扫描期间引用被新增、删除或移动的问题。
- 详细答案:如果应用线程一边修改引用,一边让回收线程无约束地扫描,回收器可能漏标仍在使用的对象,造成严重内存错误。传统做法会在 Stop The World(停顿世界)阶段暂停应用线程,建立一致的根集合;现代并发回收器只在关键阶段短暂停顿,之后通过写屏障、记忆集或染色指针等机制记录并发修改,保证标记正确。所谓“并发回收”并不代表完全没有停顿,只是把耗时工作尽量与应用线程并行。
- 进阶追问:可达对象很多时,最先表现出的线上问题是什么?
- 进阶回答:存活对象越多,标记、复制或整理需要处理的数据越多,停顿和回收线程 CPU(中央处理器)消耗会增加,回收后释放空间却可能很少。监控上会看到回收频率升高、停顿变长、Old(老年代)占用回落不明显。应结合 GC(垃圾回收)日志和 heap dump(堆转储)查看最大支配对象、引用链及缓存或队列是否无界增长,而不是只调大堆。
问题(项目追问题):异步导出任务结束后对象为什么还可能无法回收?
- 考点:意外 GC-Roots(垃圾回收根)、线程池、静态集合、ThreadLocal(线程本地变量)。
- 回答思路:对象是否“业务上不用”不重要,关键是引用链是否真正断开。
- 详细答案:常见原因包括任务对象仍留在线程池队列中,结果列表被 static(静态关键字)缓存引用,ThreadLocal(线程本地变量)在线程复用后未清理,回调监听器没有注销,或者失败任务被无限重试列表持有。排查时先生成 heap dump(堆转储),从大对象沿 Path-to-GC-Roots(到垃圾回收根的路径)反查是谁持有它,再与线程池、缓存和任务状态对应。修复通常是分页流式处理、任务完成后清理引用、在 finally(最终清理)代码块中移除 ThreadLocal(线程本地变量)、给缓存和重试队列设置上限。
- 进阶追问:为什么手工调用 System.gc(系统垃圾回收建议)不是可靠修复?
- 进阶回答:System.gc(系统垃圾回收建议)只向 JVM(Java 虚拟机)提出执行完全回收的建议,不能切断仍存在的强引用,也不保证立即执行。即使执行,也可能带来较长 Stop The World(停顿世界),但泄漏对象仍会存活。正确修复是找到并断开错误引用链,同时限制数据规模和并发度。
4.2 GC(垃圾回收)算法
| 算法 | 原理 | 优点 | 缺点 | 适合区域 |
|---|---|---|---|---|
| 标记清除 | 标记存活对象,清除未标记对象 | 简单 | 内存碎片多 | 老年代 |
| 标记整理 | 标记后移动存活对象到一端 | 减少碎片 | 移动成本高 | 老年代 |
| 复制算法 | 存活对象复制到另一块空间 | 快,无碎片 | 浪费空间 | 年轻代 |
| 分代收集 | 按对象年龄分区采用不同算法 | 符合大多数对象朝生夕死 | 参数复杂 | 堆整体 |
三种基础算法的核心差异可以用同一块内存中的对象变化表示:
flowchart TD
S["回收前: A 存活 | X 垃圾 | B 存活 | Y 垃圾"] --> M1["标记清除: A | 空洞 | B | 空洞"]
S --> M2["标记整理: A | B | 连续空闲空间"]
S --> M3["复制算法: 将 A、B 复制到另一块连续空间"]
M1 --> R1["清理快,但产生内存碎片"]
M2 --> R2["无碎片,但需要移动并修正引用"]
M3 --> R3["分配简单,但需要预留复制空间"]算法选择由“对象存活率”和“停顿目标”决定。年轻代大多数对象很快死亡,复制少量存活对象成本低;老年代存活率通常较高,如果仍复制全部存活对象,复制量和备用空间成本都很高,因此更适合标记清除或标记整理。实际收集器往往组合使用这些思想,而不是整台 JVM(Java 虚拟机)只使用一种算法。
热门面试题
问题(基础题):标记清除、标记整理和复制算法有什么区别?
- 考点:空间碎片、对象移动、存活率、空间预留。
- 回答思路:从“是否移动对象、是否产生碎片、需要多少额外空间、适合什么存活率”四个维度对比。
- 详细答案:标记清除先标记存活对象,再把未标记区域加入空闲列表,速度直接但会留下不连续空洞,大对象分配可能因为找不到连续空间而失败。标记整理在标记后把存活对象向一端移动,并修正所有引用,得到连续空闲空间,代价是移动和更新引用。复制算法把存活对象复制到另一块区域并顺序排列,分配只需移动指针,特别适合存活对象少的年轻代,但需要保留目标空间。分代收集则根据不同年龄区域的存活率选择不同策略。
- 进阶追问:为什么年轻代通常使用复制算法?
- 进阶回答:大多数新对象存活时间很短,一次 Minor GC(年轻代垃圾回收)后只需复制少量存活对象,成本与存活对象数量相关,而不是与整个区域大小相关。复制完成后得到连续空间,可使用指针碰撞快速分配。若年轻代存活率长期很高,复制量和晋升量会增加,说明对象生命周期或年轻代大小与负载不匹配。
问题(原理题):GC(垃圾回收)为什么会导致 Stop The World(停顿世界)?
- 考点:对象图一致性、对象移动、并发标记边界。
- 回答思路:先解释必须获得一致根集合,再解释移动对象时引用更新,最后说明并发收集器只能缩短而不能完全消除停顿。
- 详细答案:应用线程持续创建对象和修改引用,回收器若在没有协调的情况下判断可达性,可能把仍在使用的对象漏标。初始标记通常需要短暂停顿以扫描直接关联的根;复制或整理阶段移动对象时,还要保证应用线程不会继续使用旧地址。G1(垃圾优先回收器)等收集器把并发标记放到应用运行期间,并通过写屏障记录引用变化,但根扫描、重新标记和对象疏散仍有停顿阶段。ZGC(低延迟垃圾回收器)通过染色指针和读屏障把更多重定位工作并发化,但也存在极短的停顿阶段。
- 进阶追问:停顿时间只由堆大小决定吗?
- 进阶回答:不是。根集合大小、存活对象数量、跨区域引用、对象复制速度、回收线程数量、内存带宽和系统 CPU(中央处理器)竞争都会影响停顿。一个 8 GB(吉字节)堆若存活对象很少,可能比 4 GB(吉字节)但高存活率、引用关系复杂的堆更容易回收,因此调优必须结合日志中的回收前后容量和各阶段耗时。
问题(项目追问题):异步导出造成频繁 GC(垃圾回收)时,应先调参数还是先改代码?
- 考点:分配速率、存活率、工程治理、调优顺序。
- 回答思路:先用证据区分瞬时分配过快、对象长期存活和堆配置不足,再决定代码或参数措施。
- 详细答案:先看 GC(垃圾回收)日志、分配速率、晋升速率和回收后占用。如果每批都创建巨大列表和字节数组,年轻代回收频繁且对象快速晋升,根因通常是全量加载、全量拼接和并发任务过多,应该改为游标分页、流式写出、限制并发和复用固定缓冲区。只有在代码负载合理、容量估算确认堆确实偏小后,才调整堆和年轻代比例。盲目增大堆可能只是推迟 OOM(内存溢出),并增加一次回收处理的数据量。
- 进阶追问:如何证明改造有效?
- 进阶回答:用同一数据规模和并发模型做前后压测,对比分配速率、Minor GC(年轻代垃圾回收)频率、晋升速率、Old(老年代)回收后占用、P99(99 分位响应时间)和导出吞吐;再做长稳测试确认内存曲线能够回落,而不是只看一次任务成功。
4.3 对象晋升流程
flowchart TD
A["新对象"] --> B{"是否大对象?"}
B -->|是| O["直接进入 Old(老年代)"]
B -->|否| E["进入 Eden(伊甸园区)"]
E --> C{"Eden(伊甸园区)满?"}
C -->|否| E
C -->|是| M["触发 Minor GC(年轻代垃圾回收)"]
M --> S["存活对象进入 Survivor(幸存者区)"]
S --> AGE{"年龄达到阈值?"}
AGE -->|否| S2["复制到另一个 Survivor(幸存者区)"]
AGE -->|是| O
S2 --> M晋升到 Old(老年代)的常见原因:
- 对象年龄达到阈值。
- 大对象直接进入 Old(老年代)。
- Survivor(幸存者区)空间不足,担保进入 Old(老年代)。
- 动态年龄判断,某年龄对象总大小超过 Survivor(幸存者区)一半。
数据演绎:假设 Eden(伊甸园区)有 800 MB(兆字节),两个 Survivor(幸存者区)各 100 MB(兆字节)。一次 Minor GC(年轻代垃圾回收)前产生了 700 MB(兆字节)对象,其中只有 60 MB(兆字节)存活,这 60 MB(兆字节)可复制到空的 Survivor(幸存者区);若有 180 MB(兆字节)存活,Survivor(幸存者区)放不下的部分就需要提前晋升 Old(老年代)。如果导出任务每次都让大量对象跨越多次回收,Old(老年代)会快速增长并增加 Mixed-GC(混合垃圾回收)或 Full GC(完全垃圾回收)压力。
热门面试题
问题(基础题):对象什么时候会从年轻代晋升到 Old(老年代)?
- 考点:对象年龄、空间担保、大对象、动态年龄。
- 回答思路:按正常年龄晋升、空间不足提前晋升和大对象特殊路径回答。
- 详细答案:对象通常先在 Eden(伊甸园区)分配,Minor GC(年轻代垃圾回收)后仍存活就复制到 Survivor(幸存者区)并增加年龄;达到晋升阈值后进入 Old(老年代)。如果 Survivor(幸存者区)容纳不下本次存活对象,会发生提前晋升;某年龄及以上对象总量达到动态年龄条件时,也可能整体晋升。大对象可能直接进入 Old(老年代)或 G1(垃圾优先回收器)的 Humongous(大对象区域),具体行为取决于收集器和参数。
- 进阶追问:为什么调大晋升年龄不一定能解决 Full GC(完全垃圾回收)?
- 进阶回答:如果 Survivor(幸存者区)空间不足,对象等不到年龄阈值就会提前晋升;如果对象本来就长期存活,延迟晋升只会增加年轻代复制成本。必须先看对象年龄分布、Survivor(幸存者区)利用率和晋升速率,再判断是扩大年轻代、减少长寿命临时对象,还是治理真正的长期引用。
问题(原理题):一次 Minor GC(年轻代垃圾回收)中对象和引用如何变化?
- 考点:Eden(伊甸园区)、Survivor(幸存者区)、复制、年龄、引用修正。
- 回答思路:从触发、根扫描、存活复制、年龄更新、晋升和清空原区域依次说明。
- 详细答案:Eden(伊甸园区)无法满足新分配时触发 Minor GC(年轻代垃圾回收)。回收器从根和跨代引用记录出发识别年轻代存活对象,把它们复制到空 Survivor(幸存者区)或晋升 Old(老年代),同时修正指向新地址的引用并更新对象年龄。复制成功后,原 Eden(伊甸园区)和原 Survivor(幸存者区)可以整体清空,因此没有零散碎片。跨代引用不能每次扫描整个 Old(老年代),通常依靠卡表或记忆集缩小扫描范围。
- 进阶追问:为什么跨代引用会增加回收成本?
- 进阶回答:年轻代回收需要知道哪些年轻对象被 Old(老年代)对象引用,否则可能误回收。扫描全部 Old(老年代)代价太大,所以写屏障会维护卡表或 Remembered-Set(记忆集)。跨代引用越多,需要扫描和维护的记录越多,应用写入和回收阶段都会增加成本。
问题(项目追问题):如何判断导出任务正在造成过早晋升?
- 考点:日志证据、对象寿命、批次设计。
- 回答思路:观察年轻代回收后存活量、晋升速率和 Old(老年代)增长是否与任务窗口一致。
- 详细答案:对齐导出任务开始时间与 GC(垃圾回收)日志,如果 Minor GC(年轻代垃圾回收)频率增加、每次回收后 Survivor(幸存者区)不足、Old(老年代)占用呈阶梯式上涨,任务结束后又迟迟不回落,就要怀疑批次对象跨越多次回收或被队列、缓存持有。用对象分配采样和 heap dump(堆转储)确认主要类型,再缩小分页、减少并发、流式写出并取消无界结果缓存。
- 进阶追问:临时止血可以怎么做?
- 进阶回答:先降低导出并发和单批大小,暂停非核心大任务,必要时滚动重启释放被持有内存;同时保留 GC(垃圾回收)日志和 heap dump(堆转储)。止血后再改流式处理和任务限流,不能把定时重启当长期方案。
4.4 G1(垃圾优先回收器)
G1(垃圾优先回收器)把堆划分为多个 Region(区域),不再严格使用连续年轻代和老年代。它通过优先回收垃圾最多、收益最高的 Region(区域)控制停顿时间。
G1(垃圾优先回收器)关键概念:
| 概念 | 说明 |
|---|---|
| Region(区域) | 堆的基本管理单位 |
| Humongous(大对象区域) | 超过 Region(区域)一半的大对象 |
| Remembered-Set(记忆集) | 记录跨 Region(区域)引用 |
| Mixed-GC(混合垃圾回收) | 同时回收年轻代和部分老年代 Region(区域) |
| Pause-Target(停顿目标) | 通过参数设置期望最大停顿时间 |
flowchart LR
A["Young GC(年轻代垃圾回收)<br/>疏散 Eden(伊甸园区)"] --> B["并发标记触发阈值到达"]
B --> C["Initial Mark(初始标记)<br/>短暂停顿"]
C --> D["Concurrent Mark(并发标记)<br/>与应用并行"]
D --> E["Remark(重新标记)<br/>修正并发变化"]
E --> F["Cleanup(清理统计)<br/>计算 Region(区域)收益"]
F --> G["Mixed GC(混合垃圾回收)<br/>回收年轻代和部分老年代"]
G --> H{"Old(老年代)占用是否受控?"}
H -->|"是"| A
H -->|"否"| I["可能退化为 Full GC(完全垃圾回收)"]热门面试题
问题(基础题):G1(垃圾优先回收器)为什么叫垃圾优先?
- 考点:Region(区域)、回收收益、停顿预测。
- 回答思路:说明堆被划分为 Region(区域),回收器按垃圾比例和预计成本挑选回收集合。
- 详细答案:G1(垃圾优先回收器)把堆拆成等大小 Region(区域),每个 Region(区域)可动态承担 Eden(伊甸园区)、Survivor(幸存者区)或 Old(老年代)角色。并发标记后,G1(垃圾优先回收器)知道各 Region(区域)的存活量和回收成本,在给定 Pause-Target(停顿目标)预算内优先选择“可释放空间多、预计复制成本低”的 Region(区域)组成 Collection-Set(回收集合),因此叫垃圾优先。它追求可预测停顿,不保证每一次都严格达到目标。
- 进阶追问:G1(垃圾优先回收器)为什么仍可能出现 Full GC(完全垃圾回收)?
- 进阶回答:如果对象分配和晋升速度超过并发回收释放速度、疏散时没有足够空 Region(区域)、Humongous(大对象区域)过多或并发标记启动太晚,G1(垃圾优先回收器)可能发生 Evacuation Failure(疏散失败)并退化为 Full GC(完全垃圾回收)。应检查分配速率、晋升速率、大对象、并发标记阈值和预留空间,而不是只调 Pause-Target(停顿目标)。
问题(原理题):G1(垃圾优先回收器)一次并发标记和 Mixed-GC(混合垃圾回收)如何配合?
- 考点:初始标记、并发标记、重新标记、清理统计、混合回收。
- 回答思路:按时间顺序解释每个阶段是否停顿、产出什么信息,以及后续如何选 Region(区域)。
- 详细答案:初始标记通常借助一次 Young-GC(年轻代垃圾回收)短暂停顿,标记根直接关联对象;并发标记与应用线程同时遍历对象图;Remark(重新标记)阶段短暂停顿,处理并发期间发生的引用变化;Cleanup(清理统计)计算各 Region(区域)存活率和回收价值。之后多次 Mixed-GC(混合垃圾回收)在回收年轻代的同时,逐批加入高收益 Old(老年代)Region(区域),避免一次处理整个老年代造成长停顿。
- 进阶追问:Remembered-Set(记忆集)有什么代价?
- 进阶回答:Remembered-Set(记忆集)记录跨 Region(区域)引用,使回收某个 Region(区域)时无需扫描整个堆,但应用写引用时需要写屏障维护记录,记忆集本身也占内存,引用关系复杂时扫描成本会上升。日志中的记忆集更新、合并和扫描耗时可以帮助判断跨区域引用是否成为瓶颈。
问题(项目追问题):线上使用 G1(垃圾优先回收器)时应监控哪些指标?
- 考点:停顿、吞吐、分配、晋升、回收效果和退化风险。
- 回答思路:不能只看平均停顿,要把流量、任务窗口和各阶段日志关联起来。
- 详细答案:至少监控 Young-GC(年轻代垃圾回收)和 Mixed-GC(混合垃圾回收)的频率与 P99(99 分位响应时间)停顿、应用有效运行时间比例、分配速率、晋升速率、Old(老年代)回收前后占用、Humongous(大对象区域)数量、并发标记周期、Evacuation Failure(疏散失败)和 Full GC(完全垃圾回收)。同时关联接口延迟、异步导出并发和容器内存,判断是业务分配尖峰还是回收器本身追不上。
- 进阶追问:Pause-Target(停顿目标)设置得越小越好吗?
- 进阶回答:不是。目标越小,每次允许回收的 Region(区域)越少,回收频率可能升高,吞吐下降;若回收速度长期小于分配速度,还会积累 Old(老年代)压力。应根据业务延迟服务等级目标、吞吐和硬件资源联合压测,找到可持续平衡点。
5. 架构图与流程图
5.1 OOM(内存溢出)排查流程
flowchart TD
A["发现 OOM(内存溢出)"] --> B["确认异常类型和日志"]
B --> C{"是否有 heap dump(堆转储)?"}
C -->|有| D["用 MAT(内存分析工具)分析大对象和引用链"]
C -->|无| E["开启 HeapDumpOnOutOfMemoryError(内存溢出自动转储)"]
B --> F["jstat(虚拟机统计工具)看 GC(垃圾回收)频率"]
F --> G{"Old(老年代)是否持续上涨?"}
G -->|是| H["怀疑内存泄漏或缓存无界增长"]
G -->|否| I["怀疑瞬时大对象或参数不足"]
H --> J["定位 GC Roots(垃圾回收根)引用链"]
I --> K["检查批处理、导出、集合、文件加载"]
J --> L["修复代码或限制缓存/批量大小"]
K --> L热门面试题
问题(基础题):发生 OOM(内存溢出)后,为什么第一步不是直接调大堆?
- 考点:异常分类、证据保全、容量不足与内存泄漏的区别。
- 回答思路:先根据异常文本确定堆、元空间、直接内存或线程资源,再判断是容量不足、瞬时峰值还是引用无法释放。
- 详细答案:调大堆只能延迟部分堆内存问题,无法解决类加载器泄漏、直接内存耗尽、线程过多或容器总内存超限,还可能延长 Full GC(完全垃圾回收)停顿。正确顺序是保存异常、GC(垃圾回收)日志、容器事件和 heap dump(堆转储),结合回收后占用曲线判断内存是否持续抬升,再定位具体引用链或分配峰值。
- 进阶追问:没有生成 heap dump(堆转储)时还能怎样取证?
- 进阶回答:先保留退出码、标准错误、容器事件和历史指标;存活实例可用 jcmd(虚拟机诊断命令)采集类直方图、原生内存和 GC(垃圾回收)信息,并在容量允许时补做转储。若是退出码 137,要优先查容器工作集和 OOMKilled(容器内存杀死),不能只看 Java(编程语言)堆。
问题(原理题):5.1 OOM(内存溢出)排查流程 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:从分配失败、触发回收、回收后重试、抛出异常到转储生成依次说明,并把异常区域映射到相应证据。
- 详细答案:对象分配无法满足时,JVM(Java 虚拟机)会按当前收集器策略尝试回收或扩容;若回收后仍没有足够空间,才抛出对应 OutOfMemoryError(内存溢出错误)。堆问题看对象直方图、支配树和到 GC-Roots(垃圾回收根)的路径;元空间看类与 ClassLoader(类加载器);直接内存和线程问题则看原生内存、缓冲区与线程数量。转储可能触发停顿和磁盘尖峰,因此必须预留落盘空间。
- 进阶追问:线上实例很大,生成转储可能拖垮磁盘时怎么办?
- 进阶回答:先限流或摘除实例,确认磁盘余量和写入路径,再在副本或隔离节点采集;紧急情况下先取多次类直方图和原生内存快照做趋势比较。生产参数应预先指定转储目录、保留策略和告警,避免事故时临时操作。
问题(项目追问题):异步导出服务发生 OOM(内存溢出)时,如何止血并保证任务可恢复?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:先暂停大导出和降低并发,再保留证据;恢复依赖任务状态、分片检查点和结果幂等,而不是依赖进程内存。
- 详细答案:止血时关闭大导出入口、摘除异常实例并降低消费者并发,避免重启后立即重现;任务表记录查询条件、分片游标、文件分片和状态,已完成分片按唯一任务号复用。修复采用分页查询、流式写文件和有界队列,并监控堆使用、分配速率、队列长度、单任务峰值与失败次数。
- 进阶追问:实例在文件上传成功、状态更新前退出,如何避免重复文件?
- 进阶回答:对象存储路径使用任务号和分片号确定性命名,上传完成后以条件更新推进状态;重试先查询对象元数据或校验摘要,存在且一致就复用,不一致才覆盖或转入人工核对。
5.2 CPU(中央处理器)飙高排查流程
flowchart TD
A["发现 CPU(中央处理器)飙高"] --> B["top(进程查看工具)找 Java(编程语言)进程 PID(进程标识)"]
B --> C["top -Hp PID(线程查看)找高 CPU(中央处理器)线程"]
C --> D["printf 转 16 进制线程 ID(线程标识)"]
D --> E["jstack(线程栈工具)导出线程栈"]
E --> F["搜索 nid(本地线程标识)"]
F --> G{"线程在做什么?"}
G -->|业务循环| H["检查死循环/大循环/递归"]
G -->|GC(垃圾回收)线程| I["检查内存压力和 Full GC(完全垃圾回收)"]
G -->|锁竞争| J["检查 synchronized(同步锁)/Lock(锁接口)阻塞"]
H --> K["止血、修复、复盘"]
I --> K
J --> K热门面试题
问题(基础题):CPU(中央处理器)飙高时,怎样从进程定位到具体 Java(编程语言)代码?
- 考点:进程、线程、本地线程标识和线程栈的对应关系。
- 回答思路:用系统工具锁定高占用线程,将十进制线程号转为十六进制,再到线程栈中匹配 nid(本地线程标识)。
- 详细答案:先用 top(进程监控命令)确认 Java(编程语言)进程,再查看进程内线程并连续采样,避免把瞬时尖峰误判为持续问题。将高占用线程标识转换后,在多份 jstack(线程栈工具)中匹配 nid(本地线程标识),观察栈顶是否稳定停在死循环、序列化、大集合遍历、锁自旋或 GC(垃圾回收)线程,再结合请求日志和代码版本定位。
- 进阶追问:为什么只抓一份线程栈可能误判?
- 进阶回答:一次快照只表示一个瞬间,正常线程也可能恰好执行热点方法。应间隔数秒抓取至少三份并与线程 CPU(中央处理器)增量比对;同一线程长期停在相同栈帧,证据才更强。
问题(原理题):5.2 CPU(中央处理器)飙高排查流程 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:解释操作系统按本地线程计时、JVM(Java 虚拟机)线程保存调用栈,以及两类标识如何建立映射。
- 详细答案:操作系统调度的是本地线程并累计用户态、内核态时间,JVM(Java 虚拟机)把 Java(编程语言)线程映射到本地线程,线程转储记录其 nid(本地线程标识)和当前栈帧。把系统线程号与 nid(本地线程标识)关联后,就能判断 CPU(中央处理器)消耗来自业务计算、频繁系统调用、锁竞争自旋还是垃圾回收。采样分析器还可按时间聚合调用栈,生成火焰图观察累计热点。
- 进阶追问:高 CPU(中央处理器)线程是垃圾回收线程时,下一步查什么?
- 进阶回答:查看 GC(垃圾回收)频率、停顿、分配速率和回收后占用;若回收频繁且效果差,继续检查大对象、晋升、泄漏和堆容量。不能把垃圾回收线程当根因,它通常是对象分配压力或存活集过大的结果。
问题(项目追问题):IoT(物联网)报警风暴导致 CPU(中央处理器)打满时怎样治理?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:先用限流和降级保护核心链路,再判断热点在规则计算、重复解析、日志还是垃圾回收,最后以聚合和分治降低计算量。
- 详细答案:止血阶段按设备、租户和规则限流,关闭非关键通知并把原始事件写入可重放队列。根治时用时间窗口聚合同设备重复报警,对规则结果做短期缓存,把计算按设备分片到不同消费者,并减少同步日志和重复序列化;监控事件进入速率、积压、线程 CPU(中央处理器)、处理耗时和丢弃数量。
- 进阶追问:降级期间如何保证关键报警不被普通报警淹没?
- 进阶回答:按告警等级拆分队列和线程池,关键报警保留独立配额;普通报警允许聚合或延迟。所有降级决策记录原因和原始事件位置,恢复后按优先级补放,并以事件号保证重复消费不会重复通知。
5.3 Full GC(完全垃圾回收)治理流程
flowchart LR
A["Full GC(完全垃圾回收)频繁"] --> B["看 Old(老年代)占用"]
B --> C["看对象晋升速度"]
C --> D["看大对象分配"]
D --> E["看元空间增长"]
E --> F["看直接内存"]
F --> G["结合业务流量和批任务"]
G --> H["限批量/修泄漏/调参数/拆任务"]热门面试题
问题(基础题):频繁 Full GC(完全垃圾回收)应先看哪些指标?
- 考点:触发原因、回收效果、分配与晋升速率。
- 回答思路:先看回收前后老年代变化,再看分配、晋升、大对象、元空间和业务流量。
- 详细答案:关键不是次数本身,而是触发原因、停顿时长以及回收后是否释放了足够空间。若回收后 Old(老年代)仍持续抬升,优先怀疑泄漏或长期存活缓存;若回收后很低但很快再次升高,重点查分配峰值、批任务和堆容量;若元空间触发,则检查动态类和 ClassLoader(类加载器)。
- 进阶追问:回收后老年代下降明显,为什么仍会频繁触发?
- 进阶回答:可能是突发批任务产生大量晋升或大对象,堆配置和业务峰值不匹配,也可能并发周期启动过晚。要把每次回收时间点与流量、导出任务和分配速率对齐,而不是只盯静态堆占用。
问题(原理题):5.3 Full GC(完全垃圾回收)治理流程 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:从分配担保或并发回收失败讲到安全点停顿、根扫描、标记、整理与恢复应用。
- 详细答案:当收集器无法通过常规年轻代或并发周期获得足够空间,可能进入覆盖更大堆范围的停顿回收。应用线程到达 Safepoint(安全点)后暂停,收集器从 GC-Roots(垃圾回收根)扫描存活图,处理引用与类卸载,再清除或压缩存活对象。存活集越大、引用关系越复杂,扫描和移动成本越高,因此调大堆也可能让最坏停顿更长。
- 进阶追问:为什么手工执行 System.gc(系统垃圾回收)通常不是治理方案?
- 进阶回答:它只是回收建议,可能触发昂贵的全堆停顿,却不会消除仍被引用的对象。除非有经过验证的特殊堆外资源场景,否则应定位引用、控制分配和选择合适收集器,并通过日志验证效果。
问题(项目追问题):批量导出窗口出现 Full GC(完全垃圾回收)风暴,如何分阶段治理?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:短期限制并发和批量,长期改为分片流式处理,并用压测确定堆与并发预算。
- 详细答案:先暂停新增大任务、降低线程池并发并隔离导出实例,保存 GC(垃圾回收)日志和转储。代码层把一次性列表改为分页游标和流式写出,队列设置有界容量,每个分片完成即释放对象;任务层用状态机和检查点支持重试。上线前用接近生产的数据分布压测,验证停顿分位数、峰值堆和吞吐。
- 进阶追问:加机器为什么可能仍解决不了问题?
- 进阶回答:若单个任务就能撑满一个实例,横向扩容只会并行制造更多内存峰值;若调度没有全局并发上限,新实例还会拉入更多任务。必须先限制单任务工作集和系统总并发,再决定是否扩容。
6. 表格对比
6.1 OOM(内存溢出)类型对比
| 类型 | 常见日志 | 典型原因 | 排查重点 |
|---|---|---|---|
| 堆 OOM(内存溢出) | Java-heap-space(Java 堆空间) | 集合无限增长、缓存无界、批量加载过大 | heap dump(堆转储)、对象引用链 |
| 元空间 OOM(内存溢出) | Metaspace(元空间) | 动态生成类过多、类加载器泄漏 | 类数量、ClassLoader(类加载器) |
| 直接内存 OOM(内存溢出) | Direct buffer memory(直接缓冲内存) | NIO(新输入输出)缓冲区未释放 | Netty(网络通信框架)、ByteBuffer(字节缓冲区) |
| 栈溢出 | StackOverflowError(栈溢出错误) | 递归过深、循环调用 | 线程栈、递归出口 |
| 无法创建线程 | unable to create native thread(无法创建本地线程) | 线程过多、系统资源不足 | 线程数、线程池、系统限制 |
热门面试题
问题(基础题):怎样根据报错快速区分几类 OOM(内存溢出)?
- 考点:堆、元空间、直接内存、线程与容器内存边界。
- 回答思路:把异常文本映射到资源区域,再选择相匹配的证据和工具。
- 详细答案:Java-heap-space(Java 堆空间)重点查对象数量和引用链;Metaspace(元空间)查动态类数量与 ClassLoader(类加载器);Direct buffer memory(直接缓冲内存)查直接缓冲和网络组件;unable to create native thread(无法创建本地线程)查线程数、栈大小和系统限制。若没有 Java(编程语言)异常而进程以 137 退出,应查容器工作集和 OOMKilled(容器内存杀死)。
- 进阶追问:StackOverflowError(栈溢出错误)和无法创建本地线程是一回事吗?
- 进阶回答:前者通常是单线程调用深度超过栈容量,常见于递归或循环调用;后者是进程无法再创建新线程,受总内存、每线程栈和操作系统限制影响。二者都与线程栈有关,但定位对象和治理方式不同。
问题(原理题):6.1 OOM(内存溢出)类型对比 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:分别说明受 JVM(Java 虚拟机)管理的堆、使用本地内存的元空间与直接缓冲,以及操作系统对进程总量的约束。
- 详细答案:堆对象由收集器管理,分配失败会尝试回收;元空间从本地内存申请并随类卸载释放;直接缓冲由本地内存承载,回收时机与包装对象可达性有关;线程创建还要为栈和本地线程结构分配资源。容器限制覆盖这些总和,所以堆未满也可能被系统终止。分类的本质是确认哪个资源池耗尽及其所有者。
- 进阶追问:为什么堆转储无法解释所有进程内存增长?
- 进阶回答:堆转储只描述堆对象图,不包含线程栈、代码缓存、元空间主体、直接内存和本地库分配。需要结合 Native Memory Tracking(原生内存跟踪)、进程内存映射、线程数和容器指标建立完整预算。
问题(项目追问题):容器内服务没有 OutOfMemoryError(内存溢出错误)日志却突然重启,如何排查?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:从退出原因和容器事件入手,重建堆与非堆预算,不能先假定是应用主动退出。
- 详细答案:查看 Pod(容器组)终止原因、退出码和节点内核日志,若为 OOMKilled(容器内存杀死),再把最大堆、线程栈、元空间、直接内存、代码缓存和本地库相加,与容器上限比较。结合时间序列确认是线程数、网络缓冲还是堆外缓存增长;止血可降低并发、缩小缓冲和提高内存限额,但最终要修复无界资源。
- 进阶追问:如何避免修改参数后再次被杀?
- 进阶回答:为堆和非堆分别设监控,告警使用容器工作集而非仅堆占用;压测覆盖最大线程和连接数,预留操作系统与瞬时峰值余量,并通过灰度观察退出码、工作集和请求延迟。
6.2 jmap(内存映射工具)、jstack(线程栈工具)、jstat(虚拟机统计工具)、Arthas(Java 诊断工具)
| 工具 | 主要用途 | 常用场景 |
|---|---|---|
| jmap(内存映射工具) | 导出堆、查看对象统计 | OOM(内存溢出)、内存泄漏 |
| jstack(线程栈工具) | 查看线程栈 | 死锁、CPU(中央处理器)飙高、线程阻塞 |
| jstat(虚拟机统计工具) | 查看 GC(垃圾回收)统计 | Full GC(完全垃圾回收)频繁、堆使用趋势 |
| Arthas(Java 诊断工具) | 在线观测方法、线程、类 | 不重启定位慢方法、异常参数 |
| JFR(Java 飞行记录器) | 低开销运行时记录 | 长周期性能分析 |
热门面试题
问题(基础题):jmap(内存映射工具)、jstack(线程栈工具)、jstat(虚拟机统计工具)和 Arthas(Java 诊断工具)分别适合查什么?
- 考点:工具输入、输出、适用问题和侵入性。
- 回答思路:按对象、线程、垃圾回收趋势和在线方法观测四类证据区分。
- 详细答案:jmap(内存映射工具)面向堆对象统计与转储;jstack(线程栈工具)面向线程状态、锁和调用栈;jstat(虚拟机统计工具)适合连续观察分代容量和回收次数;Arthas(Java 诊断工具)可在线查看线程、方法参数、返回值和耗时。工具必须由问题驱动组合,不能用一次快照代替趋势,也不能忽略采集开销。
- 进阶追问:线上为什么不应随意执行完整堆转储?
- 进阶回答:大堆转储可能触发停顿、占用大量磁盘并造成输入输出峰值。应先评估实例是否可摘流、磁盘余量和业务高峰,必要时先采类直方图或在隔离副本上操作。
问题(原理题):6.2 jmap(内存映射工具)、jstack(线程栈工具)、jstat(虚拟机统计工具)、Arthas(Java 诊断工具) 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:说明诊断命令如何向目标虚拟机请求运行时数据,以及安全点、字节码增强和采样带来的开销。
- 详细答案:传统诊断命令通过目标 JVM(Java 虚拟机)的诊断通道请求线程、堆或性能计数器;某些一致性快照需要线程到达 Safepoint(安全点)。Arthas(Java 诊断工具)通过 Attach(动态附加)机制加载代理,并可使用 Instrumentation(字节码增强接口)增强目标方法。增强范围过大、条件表达式复杂或采样频率过高都会增加 CPU(中央处理器)和内存开销。
- 进阶追问:目标进程负载已经很高时怎样降低诊断风险?
- 进阶回答:先限流摘流并使用低开销指标和线程快照,逐步缩小类与方法范围;观测命令设置次数、深度和超时,完成后撤销增强。每次操作记录时间点,便于把工具开销与原故障区分。
问题(项目追问题):生产环境如何建立一套可执行的 JVM(Java 虚拟机)诊断预案?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:预先规定告警、证据目录、命令权限、采集顺序和停止条件,并演练工具不可用时的替代路径。
- 详细答案:预案应包含按 OOM(内存溢出)、CPU(中央处理器)、死锁和长停顿分类的最小命令集,统一保存时间、实例、版本和请求标识。转储目录设置容量告警与清理策略,诊断权限遵循最小授权;业务侧准备限流、摘节点和任务暂停开关。采集后把指标、日志、线程栈和发布记录放到同一事故时间线。
- 进阶追问:进程已经退出时还能留下哪些关键证据?
- 进阶回答:依靠预先开启的 GC(垃圾回收)滚动日志、自动堆转储、错误日志、JFR(Java 飞行记录器)持续记录、容器事件和监控时序。进程外证据必须集中存储,不能跟随容器临时文件一起消失。
7. 数据演绎
7.1 异步导出 OOM(内存溢出)
假设导出 20 万订单,每条订单对象及关联字段约 3KB(千字节):
| 步骤 | 数据量 | 内存变化 | 风险 |
|---|---|---|---|
| 一次性查询 | 200000 条 | 约 600MB(兆字节)对象进入堆 | 堆压力暴涨 |
| 组装 Excel(电子表格) | 200000 行 | 行对象、样式、字符串继续增长 | Old(老年代)快速上涨 |
| 写入响应 | 全量驻留内存 | GC(垃圾回收)回收不了仍被引用的对象 | Full GC(完全垃圾回收)频繁 |
| 最终 | 堆耗尽 | 抛出 OOM(内存溢出) | 服务不可用 |
治理方案:
- 分页查询,不一次性拉全量。
- 流式写 Excel(电子表格),避免全量行对象驻留。
- 主任务拆分片任务,本地临时文件合并后上传 OSS(对象存储服务)。
- 任务状态落库,失败可重试,下载走异步通知。
- 对导出任务设置并发上限,防止多个大导出同时打满内存。
热门面试题
问题(基础题):为什么大数据导出即使放到异步线程仍可能 OOM(内存溢出)?
- 考点:异步与内存工作集的区别、对象存活时间、并发放大。
- 回答思路:异步只转移调用线程,不会自动减少一次性查询、文件模型和缓冲区占用。
- 详细答案:如果任务仍一次性查询 20 万条记录、在内存构建完整 Excel(电子表格)并等到最终上传才释放,数据对象、字符串、样式和字节缓冲会共同形成巨大工作集。异步线程池还可能并发运行多个任务,使峰值按任务数相乘;对象长期被任务和队列引用,年轻代回收后会晋升到 Old(老年代),最终出现长停顿或 OOM(内存溢出)。
- 进阶追问:怎样估算导出任务的内存上限?
- 进阶回答:用“单页记录数乘单条深对象大小,加写出窗口、队列和库内部缓冲”估算单任务工作集,再乘最大并发并留出应用基础占用和回收余量。对象大小应通过压测和堆统计校准,不能只按数据库字段字节数计算。
问题(原理题):7.1 异步导出 OOM(内存溢出) 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:沿数据库分页、对象组装、流式写盘、分片上传和结果合并说明对象何时创建与释放。
- 详细答案:调度器只领取有配额的任务,每次按稳定游标查询一页,转换后立即写入流式文件并清空页对象;分片达到阈值就关闭句柄、上传并记录摘要。全部分片完成后再合并清单或生成下载入口。内存被限制在若干页与写出窗口内,数据量增长主要延长执行时间和增加分片数,而不再线性扩大堆占用。
- 进阶追问:数据量扩大十倍后,哪个环节更可能先成为瓶颈?
- 进阶回答:通常是数据库扫描、临时磁盘吞吐或对象存储上传,而不是堆。需要分别限制查询速率、预留临时空间、并行上传少量已关闭分片,并用背压让上游页读取速度不超过下游写出速度。
问题(项目追问题):异步导出如何做到失败可重试且不重复消耗大量资源?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:用持久化任务状态、分片检查点、确定性文件名和有界重试构成恢复闭环。
- 详细答案:任务表保存查询快照条件、当前游标、已完成分片、重试次数和租约;实例崩溃后租约到期,其他实例从最后完成分片继续。对象存储键由任务号和分片号确定,上传前后记录摘要,重复执行先校验已有对象。失败采用指数退避并设置最大次数,避免故障任务在队列中无限制造内存和输入输出压力。
- 进阶追问:源数据在导出期间变化,如何保证结果一致?
- 进阶回答:任务创建时固定业务截止时间或版本条件,分页使用不可变主键游标并带快照边界;需要严格一致时使用历史表或离线快照,而不是让一个超长数据库事务覆盖整个导出过程。
7.2 OkHttp(HTTP 客户端)未单例导致 OOM(内存溢出)
如果每次调用第三方接口都创建新的 OkHttpClient(HTTP 客户端),可能造成连接池、线程池、缓冲对象重复创建。
| 时间 | 行为 | 结果 |
|---|---|---|
| T1 | 每次请求 new OkHttpClient(HTTP 客户端) | 创建新连接池 |
| T2 | 高并发请求持续进入 | 连接池和线程对象增多 |
| T3 | 对象无法及时回收 | 堆和本地线程资源上涨 |
| T4 | GC(垃圾回收)频繁但回收效果差 | Full GC(完全垃圾回收)增多 |
| T5 | 堆或线程资源耗尽 | OOM(内存溢出)或无法创建线程 |
修复:OkHttpClient(HTTP 客户端)设计为单例,统一连接池、超时、拦截器和监控。
热门面试题
问题(基础题):为什么 OkHttpClient(HTTP 客户端)应复用,而不是每次请求新建?
- 考点:连接池、调度线程、线程池与对象生命周期。
- 回答思路:客户端是承载共享资源的长期对象,请求对象才是短生命周期对象。
- 详细答案:每个 OkHttpClient(HTTP 客户端)拥有连接池、Dispatcher(调度器)、线程执行资源和配置。反复新建会失去连接复用,增加握手、套接字、线程和缓冲对象;旧客户端即使业务引用消失,其后台任务与连接也未必立即结束。高并发下这些资源叠加会造成堆、本地内存、文件描述符和线程压力。
- 进阶追问:单例客户端是否意味着所有下游只能用相同超时?
- 进阶回答:可以共享底层连接池和线程资源,同时通过 builder(构建器)派生配置或为差异显著的下游维护少量受控客户端。关键是生命周期集中管理、数量可观测,而不是机械地全局只有一个配置对象。
问题(原理题):7.2 OkHttp(HTTP 客户端)未单例导致 OOM(内存溢出) 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:从请求进入调度器、获取连接、读写响应到连接回池和响应体关闭说明资源归属。
- 详细答案:请求先进入 Dispatcher(调度器)控制并发,再从 ConnectionPool(连接池)复用可用连接或新建连接;响应完成并正确关闭响应体后,连接才能回池。若每次新建客户端,连接池彼此隔离,连接和执行资源无法复用;若响应体未关闭,即使客户端复用也会占住连接。故障根因要分别验证客户端数量、连接数量、线程和未关闭响应。
- 进阶追问:流量扩大十倍时最先可能耗尽什么?
- 进阶回答:取决于下游延迟和并发限制,常见先耗尽连接、文件描述符或调度队列,随后线程与缓冲增加并推高内存。应设置每主机并发、连接池上限、超时和熔断,并监控在途请求与连接复用率。
问题(项目追问题):第三方支付调用如何管理 HTTP(超文本传输协议)客户端并保证重试安全?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:集中管理客户端资源,支付副作用以业务幂等键保护,超时后先查单再决定是否重试。
- 详细答案:为支付渠道创建受 Spring(Java 应用框架)容器管理的长期客户端,统一连接池、超时、代理、证书和指标;每次响应都在受控作用域关闭。发起扣款携带订单级 Idempotency-Key(幂等键),本地先落支付尝试记录。遇到读超时不能直接认定失败,应按渠道流水查询状态,确认未受理后才重试。
- 进阶追问:服务重启后如何继续处理状态未知的支付请求?
- 进阶回答:扫描状态为处理中且超过阈值的记录,通过渠道查询接口对账;成功则幂等推进订单,失败则按策略重试,长期未知转人工。连接层重试不能替代业务状态机,因为网络失败无法证明第三方没有执行。
8. 线上排查与实战经验
8.1 OOM(内存溢出)标准排查步骤
- 先止血:摘除异常节点、限流、关闭大导出或大批处理入口。
- 保存现场:保留日志、heap dump(堆转储)、GC(垃圾回收)日志、线程栈。
- 判断类型:看异常是 Java-heap-space(Java 堆空间)、Metaspace(元空间)、Direct buffer memory(直接缓冲内存)还是 native thread(本地线程)。
- 分析对象:用 MAT(内存分析工具)看 dominator tree(支配树)、retained-size(保留大小)、GC-Roots(垃圾回收根)引用链。
- 结合业务:看是否批量导出、缓存、消息堆积、任务重试、第三方接口卡顿。
- 修复代码:限制批量、分页流式处理、缓存过期、线程池隔离、对象及时释放。
- 复盘:补监控、补压测、补告警阈值、补开关。
热门面试题
问题(基础题):一套合格的 OOM(内存溢出)标准排查步骤应包含哪些阶段?
- 考点:止血、分类、取证、根因、验证与复盘。
- 回答思路:按事故处理顺序回答,强调先保业务和证据,再做参数或代码修改。
- 详细答案:先确认影响面并限流、暂停高风险任务或摘节点;保存异常类型、容器事件、GC(垃圾回收)日志和转储;按堆、元空间、直接内存、线程或容器总内存分类;分析增长对象、引用链或原生资源;实施有针对性的代码和容量修复;最后通过相同数据量压测与灰度指标验证,并补告警和操作手册。
- 进阶追问:为什么止血后不能马上重启所有实例?
- 进阶回答:若触发条件仍存在,批量重启会同时拉取积压任务并再次耗尽资源,还会覆盖现场证据。应先保留至少一个现场或完成采集,再逐个恢复并限制入口与消费速率。
问题(原理题):8.1 OOM(内存溢出)标准排查步骤 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:把时间序列趋势、对象快照和引用图三类证据串成因果链。
- 详细答案:监控曲线回答“何时开始、增长多快、回收是否有效”,对象直方图回答“哪些类数量或体积异常”,支配树和到 GC-Roots(垃圾回收根)的路径回答“谁在持有”。再把持有者映射到代码、线程池队列、缓存或类加载器,结合发布和流量时间线验证触发条件。只有证据闭环后,才能区分泄漏、容量不足和瞬时峰值。
- 进阶追问:转储中最大的对象一定是根因吗?
- 进阶回答:不一定,大对象可能是合理缓存或共享基础结构。要看其 retained-size(保留大小)、增长趋势、到根路径和业务生命周期;真正异常的是超出设计边界且无法按预期释放的持有关系。
问题(项目追问题):如何让 OOM(内存溢出)排查能力在生产环境长期可用?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:把参数、存储、监控、权限和演练前置,避免事故时才临时准备。
- 详细答案:启用带轮转的 GC(垃圾回收)日志、自动错误日志和受控堆转储目录,将文件持久化到不会随容器删除的存储;监控堆、非堆、线程、直接缓冲和容器工作集。预置任务暂停和流量开关,限定诊断权限,并定期演练“采集失败、磁盘不足、进程已退出”三种情况。
- 进阶追问:自动转储会带来哪些新风险?
- 进阶回答:转储可能包含敏感数据、占满磁盘并延长停顿。需要加密和访问审计,设置专用目录、容量告警与生命周期清理,且只允许受控人员下载分析。
8.2 CPU(中央处理器)飙高标准排查步骤
# 1. 找到 Java(编程语言)进程
top
# 2. 查看进程内线程
top -Hp <pid>
# 3. 把十进制线程 ID(线程标识)转成十六进制
printf '%x\n' <tid>
# 4. 导出线程栈并搜索 nid(本地线程标识)
jstack <pid> > /tmp/thread.txt判断方式:
- 如果高 CPU(中央处理器)线程在业务方法,检查死循环、递归、大集合遍历。
- 如果高 CPU(中央处理器)线程是 GC(垃圾回收)线程,检查内存泄漏或对象分配过快。
- 如果大量线程 BLOCKED(阻塞),检查锁竞争和锁内远程调用。
- 如果大量线程 WAITING(无限等待),检查线程池、队列、条件变量。
热门面试题
问题(基础题):CPU(中央处理器)飙高标准流程为什么要同时看线程状态和 CPU(中央处理器)时间?
- 考点:运行、阻塞、等待与实际计算消耗的区别。
- 回答思路:线程状态说明在等什么,CPU(中央处理器)时间说明谁真正消耗计算资源,两类证据互补。
- 详细答案:大量 BLOCKED(阻塞)线程可能造成吞吐下降,但真正高 CPU(中央处理器)的可能只有锁持有者;大量 WAITING(无限等待)线程也不等于消耗处理器。先找 CPU(中央处理器)增量最大的本地线程,再看其 Java(编程语言)栈和锁关系,才能区分计算热点、锁自旋、系统调用与垃圾回收压力。
- 进阶追问:服务延迟很高但 CPU(中央处理器)不高,优先查什么?
- 进阶回答:优先查线程池耗尽、数据库或网络等待、锁阻塞、连接池和队列积压。低 CPU(中央处理器)说明处理器可能不是瓶颈,不能继续围绕计算优化。
问题(原理题):8.2 CPU(中央处理器)飙高标准排查步骤 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:从系统采样线程时间、标识转换、线程转储匹配到多次采样确认热点依次说明。
- 详细答案:系统按本地线程记录调度时间,线程查看命令给出十进制标识;线程转储中的 nid(本地线程标识)通常以十六进制表示,所以需要转换后匹配。连续抓取多份转储,若同一高占用线程长期停在相同业务栈帧,就能定位循环或重计算;若热点在收集器线程,则转向内存分配和存活集分析。
- 进阶追问:短生命周期线程频繁创建时,线程号匹配有什么风险?
- 进阶回答:线程可能在两次采样之间退出,标识还可能被系统复用,导致错配。应缩短采样间隔、同时记录线程名与启动上下文,必要时使用持续采样器按调用栈聚合,而不是依赖单次手工匹配。
问题(项目追问题):Runner(执行器)批任务导致 CPU(中央处理器)持续满载,怎样止血和恢复?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:暂停新任务、保留在途状态并降低并行度,再根据热点栈决定拆分算法还是治理垃圾回收。
- 详细答案:调度中心先停止向异常实例派发,租约确保未完成任务超时后可重新领取;实例侧降低工作线程并保存多次线程栈。若热点是大集合排序或序列化,就按数据分片并减少重复计算;若是垃圾回收线程,就限制批量和对象创建。恢复时逐步提高并发,观察吞吐、队列、CPU(中央处理器)和失败率。
- 进阶追问:任务已经产生部分外部副作用,重新领取会不会重复?
- 进阶回答:每个分片和业务动作使用确定性幂等键,状态推进采用条件更新;恢复者从持久化检查点继续,并在调用外部系统前查询结果。租约只解决所有权,不替代副作用幂等。
8.3 Full GC(完全垃圾回收)频繁治理
常见根因:
- 大对象频繁分配,如一次性导出、批量图片、PDF(便携式文档格式)合并。
- 缓存无上限,如 Map(映射接口)静态缓存不淘汰。
- 老年代长期占用高,可能有内存泄漏。
- 元空间增长,可能是动态代理、脚本、类加载器泄漏。
- 线程池堆积,任务对象在队列里长期被引用。
治理顺序:
- 先确认业务流量是否异常。
- 再看 GC(垃圾回收)日志和堆趋势。
- 然后找大对象和引用链。
- 最后才考虑调大堆或调 GC(垃圾回收)参数。
热门面试题
问题(基础题):治理频繁 Full GC(完全垃圾回收)为什么要先区分“回收不掉”和“很快又填满”?
- 考点:内存泄漏、存活集、分配峰值与容量模型。
- 回答思路:通过每次回收前后老年代占用判断问题属于持有关系还是分配速率。
- 详细答案:若 Full GC(完全垃圾回收)后 Old(老年代)仍很高并逐步抬升,说明大量对象仍可达,应查缓存、队列、ThreadLocal(线程本地变量)和静态引用;若回收后明显下降却很快再次升高,更可能是大批量、对象分配过快或堆容量不足。两者治理方向完全不同,前者修生命周期,后者控制工作集与并发。
- 进阶追问:如何从日志判断回收器退化?
- 进阶回答:查看触发原因、并发周期是否来得及完成、疏散失败或担保失败、停顿阶段和回收前后区域占用。将这些事件与分配速率和业务批任务对齐,确认是否由空间预留不足或大对象造成。
问题(原理题):8.3 Full GC(完全垃圾回收)频繁治理 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:建立“分配速率、存活率、回收速率、可用空间”四个量之间的关系。
- 详细答案:系统可持续运行要求回收器长期释放空间的速度不低于对象分配和晋升速度,并且在并发周期完成前保留足够空闲区。存活率升高会增加标记和复制成本,大对象会快速消耗连续区域,批任务会制造瞬时峰值。当安全余量耗尽时,收集器只能采用更重的停顿回收;因此治理要降低分配、缩短对象寿命或增加合理余量。
- 进阶追问:把堆扩大十倍一定能降低 Full GC(完全垃圾回收)吗?
- 进阶回答:可能降低短期频率,但若泄漏存在只会延后故障,并让转储和最坏停顿更昂贵。容量调整必须配合回收后占用、分配速率和压测结果,证明系统在峰值下能进入稳定状态。
问题(项目追问题):如何防止导出、缓存和线程池积压共同触发 Full GC(完全垃圾回收)?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:为每类长期引用设置上限和释放点,再用全局内存预算约束它们的叠加峰值。
- 详细答案:导出采用分页流式和任务并发上限;缓存设置容量、权重和过期策略;线程池使用有界队列并对拒绝任务持久化或返回明确失败。监控不仅看各自数量,还看它们同时增长时的堆工作集、Old(老年代)占用和分配速率。达到水位先停止非关键任务,而不是等回收风暴后再处理。
- 进阶追问:有界队列满后怎样避免任务丢失?
- 进阶回答:需要可靠性的任务先落数据库或消息系统,再由消费者按容量领取;拒绝只表示当前实例不接收,不代表业务数据消失。任务号保证重复投递幂等,积压量和最老等待时间用于告警与扩容决策。
9. 项目落地话术
9.1 异步导出 OOM(内存溢出)话术
“我处理过大数据量导出导致的 OOM(内存溢出)。当时问题不是简单内存小,而是一次性查询和一次性生成 Excel(电子表格)导致大量对象长期被引用,Full GC(完全垃圾回收)也回收不掉。我把同步导出改成主任务加分片任务:分页查询、流式写文件、本地临时文件合并、上传 OSS(对象存储服务)、最后通知下载。这样接口不再长时间占用线程,堆内存也不会被一次性打满。”
热门面试题
问题(基础题):讲异步导出 OOM(内存溢出)事故时,必须交代哪条内存因果链?
- 考点:概念边界、核心作用、适用场景。
- 回答思路:先用一句话定义 9.1 异步导出 OOM(内存溢出)话术,再说明它在系统稳定性、性能或一致性中的作用,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说一个使用场景。
- 进阶追问:如果这个机制使用不当,线上最容易出现什么故障?如何监控和止血?
问题(原理题):9.1 异步导出 OOM(内存溢出)话术 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:按“触发条件 -> 核心流程 -> 关键数据结构 -> 成功/失败分支 -> 资源释放或回滚”来讲,避免只背结论。
- 进阶追问:如果并发量、数据量或故障率扩大 10 倍,这个流程里哪个环节会先成为瓶颈?
问题(项目追问题):异步导出改造后,如何证明内存风险已经被控制?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制,说明正常路径、失败路径、重试策略、幂等保护和日志/指标/告警设计。
- 进阶追问:如果服务重启、网络抖动或第三方接口超时,如何保证数据不丢、不重、不乱?
9.2 CPU(中央处理器)飙高话术
“我排查 CPU(中央处理器)飙高会先用 top(进程监控命令)定位 Java(编程语言)进程,再用 top-Hp(线程查看命令)定位具体线程,把线程 ID(线程标识)转成十六进制后在 jstack(线程栈工具)里找 nid(本地线程标识)。如果线程在业务循环,就看循环条件和数据量;如果是 GC(垃圾回收)线程,就看对象分配和 Old(老年代)趋势;如果是锁竞争,就看 BLOCKED(阻塞)线程和锁持有者。”
热门面试题
问题(基础题):CPU(中央处理器)飙高的项目话术怎样体现从系统线程定位到业务代码?
- 考点:概念边界、核心作用、适用场景。
- 回答思路:先用一句话定义 9.2 CPU(中央处理器)飙高话术,再说明它在系统稳定性、性能或一致性中的作用,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说一个使用场景。
- 进阶追问:如果这个机制使用不当,线上最容易出现什么故障?如何监控和止血?
问题(原理题):9.2 CPU(中央处理器)飙高话术 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:按“触发条件 -> 核心流程 -> 关键数据结构 -> 成功/失败分支 -> 资源释放或回滚”来讲,避免只背结论。
- 进阶追问:如果并发量、数据量或故障率扩大 10 倍,这个流程里哪个环节会先成为瓶颈?
问题(项目追问题):CPU(中央处理器)事故中怎样同时设计限流、任务恢复和热点验证?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制,说明正常路径、失败路径、重试策略、幂等保护和日志/指标/告警设计。
- 进阶追问:如果服务重启、网络抖动或第三方接口超时,如何保证数据不丢、不重、不乱?
9.3 生产事故复盘话术
“我的线上事故处理习惯是先止血,再定位,再修复,再复盘。止血可以限流、摘节点、关开关;定位要保留 GC(垃圾回收)日志、线程栈、堆转储;修复不能只调参数,要找到代码和业务根因;复盘要补监控、补压测、补告警和回滚方案。”
热门面试题
问题(基础题):生产事故复盘为什么必须区分止血动作、直接原因和系统性根因?
- 考点:概念边界、核心作用、适用场景。
- 回答思路:先用一句话定义 9.3 生产事故复盘话术,再说明它在系统稳定性、性能或一致性中的作用,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说一个使用场景。
- 进阶追问:如果这个机制使用不当,线上最容易出现什么故障?如何监控和止血?
问题(原理题):9.3 生产事故复盘话术 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:按“触发条件 -> 核心流程 -> 关键数据结构 -> 成功/失败分支 -> 资源释放或回滚”来讲,避免只背结论。
- 进阶追问:如果并发量、数据量或故障率扩大 10 倍,这个流程里哪个环节会先成为瓶颈?
问题(项目追问题):如何把一次 JVM(Java 虚拟机)事故沉淀为可验证的预防措施?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制,说明正常路径、失败路径、重试策略、幂等保护和日志/指标/告警设计。
- 进阶追问:如果服务重启、网络抖动或第三方接口超时,如何保证数据不丢、不重、不乱?
9.4 JVM(Java 虚拟机)生命周期话术
“JVM(Java 虚拟机)生命周期可以从进程视角讲:操作系统先启动 Java(编程语言)进程,JVM(Java 虚拟机)初始化运行时数据区、类加载器、GC(垃圾回收)线程和 JIT(即时编译)线程;然后加载 main(主方法)所在类并执行 main(主方法)。运行过程中会不断发生类加载、字节码解释执行、热点代码 JIT(即时编译)、对象分配和 GC(垃圾回收)。退出时,如果 main(主方法)结束且没有 non-daemon thread(非守护线程),或者调用 System.exit(系统退出),JVM(Java 虚拟机)会进入 shutdown(关闭)流程,执行 Shutdown Hook(关闭钩子)后退出。但 kill -9(强杀信号)或 Runtime.halt(强制停止运行时)不会保证 Shutdown Hook(关闭钩子)执行。”
热门面试题
问题(基础题):讲 JVM(Java 虚拟机)生命周期时,为什么要同时覆盖启动、运行和退出边界?
- 考点:概念边界、核心作用、适用场景。
- 回答思路:先用一句话定义 9.4 JVM(Java 虚拟机)生命周期话术,再说明它在系统稳定性、性能或一致性中的作用,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说一个使用场景。
- 进阶追问:如果这个机制使用不当,线上最容易出现什么故障?如何监控和止血?
问题(原理题):9.4 JVM(Java 虚拟机)生命周期话术 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:按“触发条件 -> 核心流程 -> 关键数据结构 -> 成功/失败分支 -> 资源释放或回滚”来讲,避免只背结论。
- 进阶追问:如果并发量、数据量或故障率扩大 10 倍,这个流程里哪个环节会先成为瓶颈?
问题(项目追问题):支付或 Runner(执行器)任务怎样应对 JVM(Java 虚拟机)在任意阶段退出?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制,说明正常路径、失败路径、重试策略、幂等保护和日志/指标/告警设计。
- 进阶追问:如果服务重启、网络抖动或第三方接口超时,如何保证数据不丢、不重、不乱?
9.5 优雅停机话术
“在微服务里 JVM(Java 虚拟机)退出不是简单杀进程。比如订单履约、轨迹推送、Redis(远程字典服务)延迟队列消费这类任务,如果直接 kill -9(强杀信号),可能出现任务执行一半、锁未释放、状态没落库的问题。我的做法是接入优雅停机:先从注册中心摘流量,再停止接收新任务,然后等待线程池中任务完成或超时取消,最后释放资源。Shutdown Hook(关闭钩子)可以做兜底,但不能把全部可靠性寄托在 Shutdown Hook(关闭钩子)上,任务本身还要幂等和可恢复。”
热门面试题
问题(基础题):优雅停机在订单履约和消息消费中具体解决哪些中断风险?
- 考点:概念边界、核心作用、适用场景。
- 回答思路:先用一句话定义 9.5 优雅停机话术,再说明它在系统稳定性、性能或一致性中的作用,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说一个使用场景。
- 进阶追问:如果这个机制使用不当,线上最容易出现什么故障?如何监控和止血?
问题(原理题):9.5 优雅停机话术 底层是怎么工作的?请按执行流程讲一遍。
- 考点:底层数据结构、状态流转、关键线程或组件、性能成本。
- 回答思路:按“触发条件 -> 核心流程 -> 关键数据结构 -> 成功/失败分支 -> 资源释放或回滚”来讲,避免只背结论。
- 进阶追问:如果并发量、数据量或故障率扩大 10 倍,这个流程里哪个环节会先成为瓶颈?
问题(项目追问题):订单履约服务如何实现摘流、排空、超时终止和任务接管?
- 考点:工程落地、异常场景、幂等、补偿、可观测性。
- 回答思路:结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制,说明正常路径、失败路径、重试策略、幂等保护和日志/指标/告警设计。
- 进阶追问:如果服务重启、网络抖动或第三方接口超时,如何保证数据不丢、不重、不乱?
10. 高频面试题与追问
10.1 JVM(Java 虚拟机)堆和栈有什么区别?
堆是线程共享的,主要放对象实例,是 GC(垃圾回收)重点管理区域;栈是线程私有的,放方法调用栈帧、局部变量、操作数栈。堆容易出现 OOM(内存溢出),栈容易出现 StackOverflowError(栈溢出错误)。
10.2 OOM(内存溢出)一定是内存泄漏吗?
不一定。OOM(内存溢出)可能是内存泄漏,也可能是瞬时大对象、批处理过大、参数配置太小、线程过多、直接内存不足。面试时要先分类,再定位。
10.3 Full GC(完全垃圾回收)频繁怎么处理?
先看 GC(垃圾回收)日志和 jstat(虚拟机统计工具)趋势,确认 Old(老年代)是否持续上涨;再导出 heap dump(堆转储)分析大对象和引用链;结合业务看是否批任务、缓存、队列堆积;最后修代码或调参数。
10.4 G1(垃圾优先回收器)为什么适合大堆?
G1(垃圾优先回收器)按 Region(区域)管理堆,可以优先回收收益高的 Region(区域),并通过 Pause-Target(停顿目标)尽量控制停顿时间,比传统整代回收更适合大堆和低停顿场景。
10.5 类加载双亲委派有什么意义?
双亲委派可以保证核心类优先由上层类加载器加载,避免核心类被重复加载或篡改,保持 Java(编程语言)类型体系稳定。
10.6 JVM(Java 虚拟机)生命周期有哪些阶段?
可以按启动、初始化、运行、关闭讲。启动阶段操作系统创建 Java(编程语言)进程并创建 JVM(Java 虚拟机)实例;初始化阶段准备内存区域、类加载器、GC(垃圾回收)线程、JIT(即时编译)线程;运行阶段执行 main(主方法)、解释执行字节码、JIT(即时编译)、对象分配、GC(垃圾回收);关闭阶段执行 Shutdown Hook(关闭钩子)并释放资源。kill -9(强杀信号)这类强制终止不会保证关闭流程完整执行。
10.7 类生命周期和对象生命周期有什么区别?
类生命周期是 Class(类元数据)层面的:Loading(加载)、Linking(链接)、Initialization(初始化)、Using(使用)、Unloading(卸载)。对象生命周期是实例层面的:内存分配、零值初始化、对象头设置、构造方法执行、被引用使用、不可达、被 GC(垃圾回收)回收。一个类通常先被加载和初始化,才能创建这个类的对象。
10.8 为什么 main(主方法)结束后 JVM(Java 虚拟机)不一定退出?
因为 JVM(Java 虚拟机)退出取决于是否还存在 non-daemon thread(非守护线程)。如果线程池里的工作线程不是 daemon thread(守护线程),即使 main(主方法)结束,JVM(Java 虚拟机)也会继续运行。所以线上服务关闭时必须正确 shutdown(关闭)线程池。
10.9 Shutdown Hook(关闭钩子)一定会执行吗?
不一定。正常 System.exit(系统退出)或 SIGTERM(终止信号)通常会触发 Shutdown Hook(关闭钩子);但 kill -9(强杀信号)、Runtime.halt(强制停止运行时)、操作系统崩溃、JVM(Java 虚拟机)fatal error(致命错误)可能不会执行。所以 Shutdown Hook(关闭钩子)只能做优雅停机的一部分,业务还要靠幂等、状态机和补偿机制兜底。
模块综合热门面试题补强
10.6 JVM(Java 虚拟机)从启动到退出经历哪些阶段?
- 问题:JVM(Java 虚拟机)从启动到退出经历哪些阶段?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.7 类生命周期和对象生命周期有什么区别?
- 问题:类生命周期和对象生命周期有什么区别?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.8 Class-File(类文件)结构为什么重要?
- 问题:Class-File(类文件)结构为什么重要?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.9 操作数栈和局部变量表如何执行字节码?
- 问题:操作数栈和局部变量表如何执行字节码?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.10 JIT(即时编译)如何识别热点代码?
- 问题:JIT(即时编译)如何识别热点代码?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.11 逃逸分析如何减少对象分配?
- 问题:逃逸分析如何减少对象分配?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.12 强引用、软引用、弱引用、虚引用怎么区分?
- 问题:强引用、软引用、弱引用、虚引用怎么区分?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.13 Serial(串行收集器)、Parallel(并行收集器)、CMS(并发标记清除)、G1(垃圾优先回收器)怎么选?
- 问题:Serial(串行收集器)、Parallel(并行收集器)、CMS(并发标记清除)、G1(垃圾优先回收器)怎么选?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.14 如何阅读 GC(垃圾回收)日志?
- 问题:如何阅读 GC(垃圾回收)日志?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.15 Full GC(完全垃圾回收)频繁如何定位?
- 问题:Full GC(完全垃圾回收)频繁如何定位?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.16 OOM(内存溢出)一定是堆不够吗?
- 问题:OOM(内存溢出)一定是堆不够吗?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.17 DirectBuffer(直接缓冲区)为什么会导致本地内存问题?
- 问题:DirectBuffer(直接缓冲区)为什么会导致本地内存问题?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.18 Docker(容器技术)里 JVM(Java 虚拟机)内存如何设置?
- 问题:Docker(容器技术)里 JVM(Java 虚拟机)内存如何设置?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.19 jstack(线程栈工具)如何定位 CPU(中央处理器)飙高线程?
- 问题:jstack(线程栈工具)如何定位 CPU(中央处理器)飙高线程?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.20 Shutdown Hook(关闭钩子)为什么不能保证一定执行?
- 问题:Shutdown Hook(关闭钩子)为什么不能保证一定执行?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
10.21 线上 JVM(Java 虚拟机)事故如何复盘?
- 问题:线上 JVM(Java 虚拟机)事故如何复盘?
- 考点:核心概念、底层机制、失败边界、项目落地。
- 回答思路:先给结论,再讲工作流程和关键取舍,最后结合 异步导出 OOM(内存溢出)、CPU(中央处理器)飙高、容器内存限制 说明线上怎么兜底。
- 进阶追问:如果出现并发冲突、数据不一致、性能下降或服务重启,你如何定位、恢复和复盘?
11. 本模块复习清单
- 能完整讲出 JVM(Java 虚拟机)启动、初始化、运行、关闭生命周期。
- 能说明 main(主方法)结束后 JVM(Java 虚拟机)为什么不一定退出。
- 能说明 daemon thread(守护线程)和 non-daemon thread(非守护线程)的区别。
- 能说明 Shutdown Hook(关闭钩子)的作用和不可靠边界。
- 能讲出类生命周期:Loading(加载)、Linking(链接)、Initialization(初始化)、Using(使用)、Unloading(卸载)。
- 能讲出对象生命周期:分配、初始化、使用、不可达、GC(垃圾回收)回收。
- 能画出 JVM(Java 虚拟机)运行时内存结构图。
- 能说明堆、栈、元空间、直接内存分别可能出现什么问题。
- 能讲对象创建、分配、晋升、回收流程。
- 能解释 GC(垃圾回收)可达性分析和 GC-Roots(垃圾回收根)。
- 能对比标记清除、标记整理、复制算法。
- 能讲清 G1(垃圾优先回收器)Region(区域)模型。
- 能按步骤排查 OOM(内存溢出)、Full GC(完全垃圾回收)、CPU(中央处理器)飙高、死锁。
- 能把异步导出 OOM(内存溢出)、OkHttp(HTTP 客户端)未单例化内存问题讲成完整事故案例。
