第一模块:内存结构与模型 (The Basics)
1. 讲一下 JVM 的运行时数据区(内存结构)包含哪些部分?
-
核心回答:
-
线程私有: 程序计数器 (Program Counter Register)、虚拟机栈 (VM Stack)、本地方法栈 (Native Method Stack)。
-
线程共享: 堆 (Heap)、方法区 (Method Area / Metaspace)。
-
-
详解:
-
堆:存放对象实例,GC 的主要区域。
-
栈:存放局部变量表、操作数栈、动态链接、方法出口等。
-
方法区:存放类信息、常量、静态变量等(JDK 8 以后由元空间 Metaspace 实现)。
-
-
博客加分项: 提到 JDK 1.7 到 1.8 中字符串常量池移动到了堆中,以及元空间使用的是直接内存(Direct Memory)而非虚拟机内存。
2. 什么是 Java 内存模型 (JMM)?它和 JVM 内存结构有什么区别?
-
核心回答:
-
JMM 是一组规范,定义了多线程之间共享变量的可见性以及如何在工作内存和主内存之间同步数据。
-
区别:内存结构 是物理/逻辑上的数据存储划分(堆、栈等);JMM 是并发编程相关的抽象模型(原子性、可见性、有序性)。
-
-
博客加分项: 解释
volatile关键字在 JMM 中的作用(保证可见性、禁止指令重排)以及 Happens-Before 原则。
3. 栈溢出 (StackOverflowError) 和 内存溢出 (OutOfMemoryError) 有什么区别?
-
核心回答:
-
StackOverflowError: 线程请求的栈深度大于虚拟机所允许的深度,常见于死循环递归。
-
OutOfMemoryError (OOM): 堆内存不足以分配新对象,且无法通过 GC 回收更多空间;或者元空间/方法区溢出。
-
-
博客加分项: 提到可以通过
-Xss调整栈大小,通过-Xms/-Xmx调整堆大小。
第二模块:垃圾回收 (Garbage Collection)
4. 如何判断一个对象是否可以被回收?
-
核心回答:
-
引用计数法(Reference Counting):虽简单但无法解决循环引用问题(Java 未采用)。
-
可达性分析算法(Reachability Analysis):从 GC Roots 开始向下搜索,搜索不到的对象即为不可达。
-
-
博客加分项: 列举哪些对象可以作为 GC Roots(如:栈帧中的局部变量、静态变量、常量、JNI 指针)。
5. Java 中有哪些引用类型?它们有什么区别?
-
核心回答:
-
强引用 (Strong) :最常见的引用(
new),只要存在,GC 就不会回收。 -
软引用 (Soft):内存不足时会被回收,常用于缓存。
-
弱引用 (Weak):下一次 GC 时无论内存是否充足都会被回收。
-
虚引用 (Phantom):无法通过它获取对象实例,主要用于跟踪对象被 GC 回收的状态(堆外内存管理)。
-
6. 说说常见的垃圾回收算法?
-
核心回答:
-
标记-清除 (Mark-Sweep):产生内存碎片。
-
标记-复制 (Copying):无碎片,但浪费一半内存空间(新生代常用)。
-
标记-整理 (Mark-Compact):无碎片,不浪费空间,但效率相对较低(老年代常用)。
-
分代收集算法:根据对象存活周期的不同,将内存划分为新生代和老年代,选择最合适的算法。
-
7. CMS 收集器和 G1 收集器的区别是什么?
-
核心回答:
-
CMS (Concurrent Mark Sweep) :以获取最短回收停顿时间为目标,基于"标记-清除"算法,用于老年代。缺点是对 CPU 敏感、有碎片。
-
G1 (Garbage-First) :面向服务端的收集器,将堆划分为多个 Region,基于"标记-整理"和"复制"算法。它能预测停顿时间,且避免了碎片问题。
-
-
博客加分项: 提到 JDK 9 以后 G1 成为默认收集器,以及 ZGC(JDK 11+)作为低延迟收集器的出现。
第三模块:类加载机制 (Class Loading)
8. 类加载的流程是怎样的?
-
核心回答:
-
加载 (Loading):查找并加载类的二进制数据。
-
连接 (Linking):
-
验证 (Verification):确保被加载类的正确性。
-
准备 (Preparation):为静态变量分配内存并初始化默认值。
-
解析 (Resolution):将符号引用转换为直接引用。
-
-
初始化 (Initialization) :执行类构造器
<clinit>,初始化静态变量赋值。
-
9. 什么是"双亲委派机制"?为什么要这么设计?
-
核心回答:
-
机制: 当一个类加载器收到加载请求时,它首先不会自己去尝试加载,而是把请求委派给父类加载器,依次向上,直到启动类加载器。只有当父类加载器反馈无法完成加载时,子类加载器才会尝试自己加载。
-
原因:
-
安全性 :防止核心 API 被篡改(例如自己写一个
java.lang.String也是无法加载的)。 -
避免重复加载:父类已经加载过,子类就不用再加载了。
-
-
-
博客加分项: 什么时候需要打破双亲委派模型?(例如:Tomcat 的 WebApp 类加载器、JDBC 驱动加载、OSGi)。
第四模块:调优与实战 (Tuning & Tools)
10. 常见的 JVM 调优参数有哪些?
-
核心回答:
-
-Xms:初始堆大小。 -
-Xmx:最大堆大小(通常设置与 -Xms 相同,避免运行时自动扩展带来的性能损耗)。 -
-Xmn:新生代大小。 -
-XX:MetaspaceSize:元空间大小。 -
-XX:+PrintGCDetails:打印 GC 详细信息。
-
11. 平时用什么工具排查 JVM 问题?
-
核心回答:
-
命令行工具:
-
jps:查看 Java 进程。 -
jstat:查看 GC 统计信息。 -
jmap:生成堆转储快照 (Dump)。 -
jstack:查看线程堆栈(排查死锁、CPU 占用过高)。
-
-
可视化工具:JVisualVM、JConsole、Arthas(阿里开源,强烈推荐)。
-
12. 假如生产环境 CPU 飙升到 100%,你如何排查?
-
核心回答 (排查步骤):
-
top命令找到占用 CPU 最高的进程 PID。 -
top -Hp <PID>找到该进程下占用 CPU 最高的线程 PID。 -
printf "%x\n" <线程PID>将线程 PID 转换为十六进制。 -
jstack <进程PID> | grep <十六进制线程PID> -A 20查看该线程的堆栈信息。 -
分析堆栈,定位是哪个业务方法或者是 GC 线程导致的(如果是 GC 线程频繁,说明可能发生了 OOM 前兆)。
-