JVM 是什么
书面笔记版
JVM 是 Java Virtual Machine,Java 虚拟机。它负责加载、校验和执行 .class 字节码,并屏蔽不同操作系统之间的差异。
三者关系:
| 名称 | 说明 |
|---|---|
| JDK | Java 开发工具包,包含编译器、调试工具、JRE 等 |
| JRE | Java 运行环境,包含 JVM 和基础类库 |
| JVM | 真正负责加载、校验、执行字节码的虚拟机 |
Java 能做到“一次编译,到处运行”,核心依赖 JVM 在不同平台上提供统一的字节码执行环境。
面试口述版
JVM 是 Java 虚拟机,负责加载和执行 .class 字节码。JDK 是开发工具包,JRE 是运行环境,JVM 是 JRE 里真正执行字节码的核心。Java 能跨平台,主要就是因为 Java 代码先编译成平台无关的字节码,再由不同平台上的 JVM 执行。
JVM 内存结构
书面笔记版
JVM 运行时内存结构,常见面试口径分为 5 块:
- 程序计数器。
- 虚拟机栈。
- 本地方法栈。
- 堆。
- 方法区,JDK8 以后主要对应元空间 Metaspace。
按线程归属分:
| 类型 | 区域 |
|---|---|
| 线程私有 | 程序计数器、虚拟机栈、本地方法栈 |
| 线程共享 | 堆、方法区 / 元空间 |
各区域作用:
| 区域 | 说明 |
|---|---|
| 程序计数器 | 当前线程执行到哪条字节码指令,线程切换后恢复位置,规范中唯一不会 OOM 的区域 |
| 虚拟机栈 | Java 方法调用栈,每个方法对应一个栈帧 |
| 本地方法栈 | 为 native 方法服务 |
| 堆 | 对象实例和数组主要分配区域,GC 重点区域 |
| 方法区 / 元空间 | 存类信息、常量池、静态变量、JIT 编译代码等元数据 |
面试口述版
JVM 内存结构常见分成 5 块:程序计数器、虚拟机栈、本地方法栈、堆、方法区。线程私有的是程序计数器、虚拟机栈和本地方法栈;线程共享的是堆和方法区。堆是对象实例主要分配的地方,也是 GC 的重点区域。方法区在 JDK8 以后主要对应元空间,存类元数据、常量池、静态变量这些信息。
堆区分几个部分
书面笔记版
面试里问堆分几个部分,常见回答是新生代和老年代;新生代内部通常再分 Eden、Survivor0、Survivor1。
堆 = 新生代 + 老年代
新生代 = Eden + S0 + S1
分代原因是大多数对象“朝生夕死”,少数对象存活很久。把短命对象和长寿对象分开处理,可以提高 GC 效率。
新对象通常优先进入 Eden。Eden 满了触发 Minor GC,存活对象复制到 Survivor;对象经历多次 GC 后年龄增加,达到阈值就可能晋升老年代。两个 Survivor 通常一个作为 From,一个作为 To,轮流复制,减少内存碎片并方便连续分配。
面试口述版
堆一般分成新生代和老年代,新生代再分 Eden、Survivor0、Survivor1。新对象通常先进入 Eden,Eden 满了会触发 Minor GC,活下来的对象会在两个 Survivor 之间复制,年龄达到阈值后晋升到老年代。这样设计是因为大部分对象生命周期都很短,分代回收效率更高。
GC Roots 和可达性分析
书面笔记版
JVM 判断对象是否存活,主要靠 GC Roots 和可达性分析,而不是引用计数。
常见 GC Roots:
- 虚拟机栈中引用的对象,比如局部变量、方法参数。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI 引用的对象。
- 被同步锁持有的对象。
- JVM 内部引用,比如类加载器、活动线程等。
可达性分析流程:
- 从 GC Roots 出发。
- 沿着引用链向下查找。
- 能被找到的对象是可达对象,认为还活着。
- 找不到的对象是不可达对象,可以被回收。
JVM 不使用引用计数作为主要回收判断,因为引用计数难以解决循环引用问题。两个对象互相引用,但它们已经和 GC Roots 断开时,引用计数不为 0,却应该被回收。
面试口述版
JVM 通过 GC Roots 做可达性分析来判断对象是否存活。常见 GC Roots 有栈里的局部变量引用、静态变量引用、常量引用、JNI 引用、被锁持有的对象等。JVM 不主要用引用计数,是因为引用计数解决不了循环引用问题。可达性分析是从 GC Roots 往下找,只要对象和 GC Roots 之间还有引用链,就认为它还活着。
GC 算法有多少个分别是什么怎么 GC
书面笔记版
GC 算法不要死背固定数字,常见可以按 4 类思想回答:
| 算法 | 流程 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记存活对象,清除垃圾对象 | 实现简单 | 产生内存碎片 |
| 复制算法 | 存活对象复制到另一块区域,清空原区域 | 无碎片,分配快 | 需要预留空间,存活率高时复制成本高 |
| 标记-整理 | 标记存活对象,把存活对象向一端移动 | 无碎片 | 移动对象成本高 |
| 分代收集 | 按对象生命周期分代选择算法 | 适配对象生命周期 | 是组合思想,不是单一动作 |
新生代对象大多存活时间短,每次 GC 真正活下来的对象少,所以适合复制算法。老年代对象存活率高,如果用复制算法,需要复制大量对象,还要预留大块空间,因此更适合标记-清除或标记-整理。
JVM 实际 GC 时,一般先从 GC Roots 做可达性分析,再根据代区特点使用不同策略。Eden 满时触发 Minor GC,存活对象复制到 Survivor 或晋升老年代;老年代空间不足、晋升失败或达到特定条件时,会触发更重的回收。
面试口述版
GC 算法常见可以答 4 类:标记清除、复制、标记整理、分代收集。分代收集的核心是根据对象生命周期选算法。新生代对象大多朝生夕死,存活对象少,所以适合复制算法;老年代对象存活率高,如果复制会很贵,还要浪费备用空间,所以更适合标记清除或标记整理。真正的垃圾收集器本质上也是这些算法思想的组合实现。
Minor GC Major GC Full GC 是什么
书面笔记版
常见面试口径:
| 名称 | 含义 |
|---|---|
| Minor GC | 回收新生代 |
| Major GC | 很多资料里指回收老年代,但不同语境不完全一致 |
| Full GC | 通常指整个堆,甚至包括方法区相关区域的一次更重回收 |
需要注意:Major GC 和Full GC 在不同资料、不同收集器语境下口径可能不完全一致。面试里最稳妥的是强调 Full GC 是一次全局性、成本较高的回收。
Full GC 常见触发条件:
- 老年代空间不足。
- 对象晋升失败。
- 大对象直接进入老年代导致空间紧张。
- 元空间不足。
- 显式调用
System.gc()。 - 担保失败。
- 收集器自身策略触发。
Full GC 停顿通常更长,对吞吐影响更大;线上频繁出现时,通常说明内存分配、对象生命周期或参数配置有问题。
面试口述版
Minor GC 主要回收新生代,Full GC 一般可以理解成一次更重的全局回收,通常涉及整个堆,甚至方法区相关区域。Major GC 在不同资料里口径不完全一致,面试里不要纠结死定义。Full GC 常见触发原因包括老年代空间不足、晋升失败、元空间不足、显式调用 System.gc() 等。线上频繁 Full GC 通常说明内存分配或对象生命周期出了问题。
线上服务频繁出现 Full GC 怎么办
书面笔记版
频繁 Full GC 不要上来就改参数,更不要直接加机器,应该先确认现象和根因。
排查顺序:
- 看监控:CPU、内存、QPS、RT、GC 次数和停顿。
- 看 GC 日志:触发原因、回收前后内存变化、晋升情况。
- 判断 Full GC 后内存是否明显回落。
- 如果回落不明显,导出 Heap Dump。
- 分析大对象、对象数量、支配树和 GC Roots 引用链。
- 回到代码定位缓存、集合、ThreadLocal、监听器等持有者。
- 最后再考虑调参、扩容或改代码。
常见原因:
- 内存泄漏,Full GC 后内存降不下来。
- 对象创建过快,新生代频繁回收并晋升。
- 大对象过多,直接进入老年代。
- 长生命周期对象过多,比如缓存不清理。
- 堆参数不合理,比如新生代太小、老年代预留不足。
- 元空间问题,大量动态生成类。
- 显式调用
System.gc()。
短期可以扩堆、降流量、关闭显式 GC、调大相关区域;根因治理要修复泄漏、优化缓存策略、减少大对象和临时对象、调整线程池和 GC 参数。
面试口述版
线上频繁 Full GC,我会先看监控确认是内存问题还是流量抖动,再看 GC 日志判断是老年代不足、晋升失败还是元空间问题。如果 Full GC 后内存回不去,就高度怀疑内存泄漏,导出 heap dump 分析大对象和引用链;如果能回落,就看是不是对象创建过快、缓存过大、堆参数不合理或显式 System.gc()。最后再结合代码和参数优化。
项目遇到内存占用高问题如何解决
书面笔记版
以 Java Spring Boot 服务为例,不管是 java -jar 直接启动,还是 Docker 容器启动,排查内存占用高都不能一上来就重启或盲目加内存。比较稳的思路是:先确认现象,再区分是哪类内存高,保留现场后再定位根因。
推荐排查流程:
- 确认影响范围:看是单个实例内存高,还是所有实例都高;接口是否超时、是否频繁 Full GC、是否已经 OOM 或被系统杀掉。
- 看整体资源:确认机器或容器内存、CPU、负载、磁盘是否异常,避免把系统缓存、容器限制误认为 Java 堆问题。
- 区分内存类型:判断是 JVM 堆高、堆外内存高、线程太多、元空间高,还是容器 / 系统层面的 RSS 高。
- 保留现场:进程还活着时,优先采集 GC 情况、heap dump、线程栈、JVM 参数、应用日志和系统日志。
- 分析根因:结合 dump、GC 日志、监控曲线和代码,定位是缓存过大、集合未清理、大对象查询、线程池堆积、ThreadLocal 泄漏、堆外内存泄漏还是参数不合理。
- 先止血再修复:线上先摘流量、限流、扩容或重启恢复;根因明确后再改代码、调参数、加监控。
常见判断方式:
| 现象 | 常见原因 | 排查重点 |
|---|---|---|
| 堆使用率持续升高,Full GC 后降不下来 | 内存泄漏、长生命周期对象堆积 | heap dump、Dominator Tree、GC Roots 引用链 |
| 堆使用率高,但 Full GC 后能回落 | 瞬时对象太多、流量突增、大对象创建 | GC 日志、接口 QPS、慢接口、大批量查询 |
| 进程 RSS 很高,但 JVM 堆不高 | 堆外内存、DirectBuffer、线程栈、Metaspace、native 内存 | jcmd VM.native_memory、线程数、直接内存配置 |
| Docker 容器内存高或被 OOMKilled | 容器内存限制太小、JVM 参数超过容器限制、堆外内存未预留 | docker stats、docker inspect、JVM -Xmx 和容器 limit |
| 线程数持续上涨 | 线程池配置不合理、线程泄漏、请求阻塞 | jstack、线程池监控、连接池等待 |
如果是 java -jar 直接启动,可以先用系统命令找到进程和内存占用:
top -o %MEM
ps -ef | grep app.jar
ps -o pid,ppid,rss,vsz,cmd -p <pid>
再看 JVM 内部情况:
jstat -gcutil <pid> 1000 10
jcmd <pid> VM.flags
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_dump /tmp/app.hprof
jstack <pid> > /tmp/app-thread.txt
也可以用 jmap:
jmap -histo <pid> | head
jmap -dump:live,format=b,file=/tmp/app.hprof <pid>
如果是 Docker 启动,先从容器维度看:
docker stats
docker ps
docker inspect <container_id> | grep -i memory
docker logs --tail=200 <container_id>
然后进入容器或在容器内执行 JVM 命令:
docker exec -it <container_id> sh
jps -l
jstat -gcutil <pid> 1000 10
jcmd <pid> GC.heap_info
jcmd <pid> GC.heap_dump /tmp/app.hprof
jstack <pid> > /tmp/app-thread.txt
如果容器里 Java 进程通常是 1 号进程,也可以直接:
docker exec <container_id> jcmd 1 GC.heap_info
docker exec <container_id> jcmd 1 GC.heap_dump /tmp/app.hprof
Docker 场景要特别注意 JVM 参数和容器内存限制的关系。比如容器限制是 1G,如果 -Xmx 也设置成 1G,看起来堆没有超过限制,但 JVM 进程还需要元空间、线程栈、直接内存、JIT、JNI、CodeCache 等额外内存,容器仍然可能被 OOM Killer 杀掉。生产里一般要给堆外和系统开销预留空间,例如容器 1G 时,-Xmx 不要直接顶满,可以设置成 512m 或 700m,具体要结合业务压测和监控。
Spring Boot 常见例子:
假设一个订单导出接口一次性查询几十万条订单,并把结果全部放到 List<OrderDTO> 里,再生成 Excel。上线后发现内存占用快速升高,Full GC 频繁,甚至偶发 OOM。
排查时可以这样做:
- 通过监控发现内存升高和导出接口调用时间一致。
- 用
jstat看到 Old 区使用率升高,Full GC 后回落不明显或回落很慢。 - 导出 heap dump,用 MAT / VisualVM 查看大对象,发现大量
OrderDTO、ArrayList、Excel 行对象占用内存。 - 回到代码发现接口一次性查全量数据,没有分页,也没有流式写出。
- 修复方式是改成分页查询、游标读取、流式写 Excel,限制单次导出数量,并把导出任务异步化。
再比如使用本地缓存时,如果直接用 ConcurrentHashMap 保存用户会话、商品信息或接口结果,但没有 TTL 和容量上限,访问量越大缓存越多,Full GC 后内存也降不下来。修复时应该改成 Caffeine / Redis 这类可控缓存,设置最大容量、过期时间和淘汰策略。
线程池也可能导致内存高。比如 Executors.newFixedThreadPool 使用无界队列,请求量大时任务对象不断堆积,内存会持续上涨。修复方式是使用 ThreadPoolExecutor 显式配置队列大小、拒绝策略、线程数和告警指标。
常见优化方向:
- 代码层面:避免一次性加载大结果集,分页 / 流式处理大文件,及时关闭流和连接,
ThreadLocal用完remove()。 - 缓存层面:缓存必须有容量上限、过期时间和淘汰策略,不要无限增长。
- 线程池层面:不用无界队列,控制线程数和队列长度,避免任务无限堆积。
- JVM 参数:合理设置
-Xms、-Xmx、MaxMetaspaceSize、MaxDirectMemorySize,开启 GC 日志和 OOM dump。 - Docker 参数:容器内存限制和 JVM 堆大小要匹配,给堆外内存和线程栈预留空间。
- 监控告警:监控堆使用率、Full GC 次数和耗时、容器 RSS、线程数、缓存大小、接口大对象操作。
生产环境建议提前加好参数,方便出问题时保留现场:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100M
如果是 Docker 部署,要把 dump 和日志目录挂载到宿主机或持久化存储,避免容器重建后现场文件丢失。
面试口述版
项目遇到内存占用高,我会先看是单个实例还是整体都高,再看服务是否频繁 Full GC、接口是否超时、有没有 OOM。然后区分是 JVM 堆高,还是进程 RSS 高但堆不高。如果是堆高,就用 jstat 看 GC,用 jmap 或 jcmd 导出 heap dump,分析大对象、对象数量和 GC Roots 引用链,常见原因是缓存无限增长、大集合没清理、一次性查询太多数据、ThreadLocal 没 remove、线程池队列堆积。如果是 Docker 部署,还要看 docker stats 和容器内存限制,确认 -Xmx 有没有把容器内存顶满,因为 JVM 除了堆还需要元空间、线程栈和堆外内存。线上处理时先摘流量、限流、扩容或重启止血,保留 dump 和日志后再做根因修复。
类加载过程
书面笔记版
类加载完整生命周期常见写法是:加载、验证、准备、解析、初始化、使用、卸载。面试最常问前 5 个。
| 阶段 | 说明 |
|---|---|
| 加载 | 把 .class 字节码读进 JVM,生成 Class 对象 |
| 验证 | 校验字节码是否合法、安全 |
| 准备 | 为类变量分配内存并设置默认值 |
| 解析 | 把常量池中的符号引用替换成直接引用 |
| 初始化 | 执行 <clinit>(),真正给静态变量赋值并执行静态代码块 |
准备和初始化最容易混淆。例如:
public static int a = 10;
准备阶段 a 先是默认值 0,初始化阶段才变成 10。
面试口述版
类加载过程一般分为加载、验证、准备、解析、初始化、使用、卸载。加载是把字节码读进 JVM,验证是校验安全性,准备阶段给类变量分配内存并设置默认值,解析是把符号引用变成直接引用,初始化阶段才真正执行静态变量赋值和静态代码块。面试里最容易错的是把准备和初始化混在一起。
双亲委派模型
书面笔记版
双亲委派的核心流程:
- 类加载器收到类加载请求。
- 先不自己加载,而是委派给父加载器。
- 父加载器继续向上委派。
- 一直到启动类加载器。
- 父加载器都加载不了,子加载器才自己尝试加载。
常见类加载器:
- Bootstrap ClassLoader。
- Platform / Extension ClassLoader。
- Application ClassLoader。
- 自定义 ClassLoader。
设计价值:
- 保证核心类安全,比如不能让业务代码伪造
java.lang.String。 - 避免类重复加载,减少类型不兼容问题。
典型打破双亲委派场景:
- SPI:父加载器需要反向加载子路径下的实现类。
- Tomcat:不同 Web 应用需要类隔离。
- OSGi、模块化容器:类加载关系更复杂。
面试口述版
双亲委派指类加载请求先交给父加载器处理,只有父加载器加载不了,子加载器才自己加载。这样设计主要是为了保证 Java 核心类安全,避免核心类被篡改,同时避免同一个类被重复加载。典型打破双亲委派的场景有 SPI 和 Tomcat,因为它们需要反向查找实现类或者做应用隔离。
对象创建过程
书面笔记版
Java 执行 new 时,大致经历:
- 类加载检查。
- 分配内存。
- 初始化零值。
- 设置对象头。
- 执行构造方法
<init>。
分配内存方式:
- 指针碰撞:适用于内存规整。
- 空闲列表:适用于内存不规整。
- TLAB:线程本地分配缓冲,减少多线程分配竞争。
对象一般优先在 Eden 分配,因为新生代对象大多短命,批量创建、批量回收效率高。
对象进入老年代的常见情况:
- 年龄达到阈值。
- 大对象直接进入老年代。
- Survivor 放不下。
- 动态年龄判断触发晋升。
面试口述版
对象创建大致分 5 步:先检查类有没有加载,然后分配内存、初始化零值、设置对象头,最后执行构造方法。对象一般优先分配在 Eden,经过多次 Minor GC 后如果还存活,年龄达到阈值就会晋升到老年代。为了提高分配效率,JVM 还会用 TLAB 减少多线程分配竞争。
内存泄漏定位实战
书面笔记版
内存泄漏的典型表现不是单纯“内存高”,而是对象本来应该被回收,却一直被引用链持有。
典型现象:
- 堆使用量持续上涨。
- Full GC 后仍然降不下来。
- 一段时间后可能 OOM。
定位链路:
- 看监控,确认堆使用量是否持续爬升。
- 看 GC 日志,确认 Full GC 后内存是否明显回落。
- 如果回落不明显,导出 Heap Dump。
- 用工具看大对象、对象数量、Dominator Tree、引用链。
- 找到是谁从 GC Roots 持有这些对象。
- 回到代码修复引用关系。
常见泄漏场景:
- 静态集合或全局缓存不清理。
- 线程池场景下
ThreadLocal使用后不remove()。 - 监听器、回调没有注销。
- 连接、流、会话对象没有正确关闭。
- 缓存无上限,没有 TTL 或容量限制。
面试口述版
内存泄漏我会先看堆使用趋势和 GC 日志,如果 Full GC 后内存降不下来,就导出 heap dump。分析时看哪些类实例数最多、占用最大,再看支配树和从 GC Roots 到对象的引用链,找到是谁持有了本该释放的对象。常见问题有静态集合不清理、ThreadLocal 没 remove、监听器没注销、缓存没有上限。
线上 OOM 了怎么办
书面笔记版
线上 JVM OOM 的处理原则是:先止血,再保留现场,最后定位根因。不要一上来只重启,因为重启会丢掉现场;但如果服务已经不可用,也不能为了保留现场一直不恢复。
处理流程:
- 判断影响范围:看是单个实例 OOM,还是整组服务异常;看接口是 500、超时、无响应,还是进程已经被杀掉。
- 先摘流量:从负载均衡或注册中心摘掉异常实例,K8s 场景让异常 Pod 不再接收流量。
- 保留现场:如果 JVM 进程还活着,优先采集 heap dump、线程栈、GC 情况和系统日志。
- 恢复服务:证据足够后重启异常实例;如果服务完全不可用,优先恢复服务。
- 根因分析:用 heap dump、GC 日志、线程栈、监控曲线定位是泄漏、突发大对象、参数不合理,还是堆外 / 系统资源问题。
常用现场命令:
jcmd <pid> GC.heap_dump /tmp/app.hprof
jstack <pid> > /tmp/thread.txt
jstat -gcutil <pid> 1000 10
jmap -dump:format=b,file=/tmp/app.hprof <pid>
如果是多实例服务,可以保留 1 个异常实例用于排查,其余实例先重启恢复流量。这样既保留现场,又能降低业务影响。
Spring Boot OOM 后,不一定所有接口都会返回 500。进程可能还活着、端口也还占着,但服务状态已经不可信,常见表现有:
- 某些请求分配内存失败,返回 500。
- JVM 持续 Full GC,请求大量超时。
- 连接能建立,但业务线程卡住,没有正常响应。
- 少量简单接口还能返回 200。
- 进程直接被 Linux / K8s OOM Killer 杀掉,端口消失。
所以线上不能只看端口是否存在。端口占用只能说明进程还在,不代表服务健康。
生产环境建议提前配置 OOM 自动 dump:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/logs/heapdump
-XX:+ExitOnOutOfMemoryError
JDK 11+ 可以加 GC 日志:
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100M
-XX:+ExitOnOutOfMemoryError 表示 OOM 后直接退出 JVM,让 systemd、Docker 或 K8s 拉起新实例。很多线上系统会这样配置,因为 OOM 后进程即使还活着,也可能处于半不可用状态。
常见 OOM 类型:
| OOM 类型 | 排查方向 |
|---|---|
Java heap space |
堆内存不足,重点看 heap dump 中大对象、对象数量、GC Roots 引用链 |
GC overhead limit exceeded |
GC 一直回收不动,通常也是泄漏、缓存过大或长生命周期对象堆积 |
Metaspace |
类元数据过多,关注类加载器泄漏、动态代理、热部署、脚本引擎 |
Direct buffer memory |
堆外内存不足,关注 Netty、NIO、直接内存上限 |
unable to create new native thread |
线程创建失败,关注线程数、系统 ulimit、容器资源限制 |
| 进程直接消失 | 可能不是 Java OOM,而是 Linux / K8s OOM Killer 杀进程 |
面试口述版
线上 OOM 我会先止血再定位。第一步看影响范围,如果是单个实例,先从负载均衡或注册中心摘掉流量;如果进程还活着,就尽量导出 heap dump、线程栈、GC 信息和日志。证据保留后再重启恢复服务。如果服务已经完全不可用,恢复优先级更高,多实例场景可以保留一个异常实例排查,其余实例先重启。
Spring Boot OOM 后不一定所有接口都是 500,可能是部分 500、部分超时、连接无响应,也可能少量简单接口还能正常返回。端口还在只能说明进程没退出,不代表服务健康。生产上通常会配置 HeapDumpOnOutOfMemoryError 和 ExitOnOutOfMemoryError,让 JVM OOM 时先 dump 再退出,由容器或守护进程重新拉起。
jmap jstack 和 GC 日志分析思路
书面笔记版
jmap 常用于看堆信息和导出堆快照:
jmap -heap <pid>
jmap -histo <pid>
jmap -dump:live,format=b,file=heap.hprof <pid>
关注点:
- 堆各区域使用情况。
- 哪些类实例数量最多。
- 哪些类占用内存最大。
- dump 后结合工具分析引用链。
jstack 主要看线程状态,常用于 CPU 飙高、线程阻塞、死锁、请求卡住:
jstack <pid>
GC 日志重点看:
- Young GC 是否过于频繁。
- Full GC 是否频繁。
- 单次停顿时间长不长。
- GC 后内存是否回落。
- 晋升是否异常。
如果 GC 后内存明显回落,更像对象分配过快或参数不合理;如果回落不明显,更像内存泄漏或长生命周期对象堆积。
面试口述版
jmap 主要看堆和导出 dump,关注对象数量、内存占用和引用链;jstack 主要看线程栈,适合排查 CPU 飙高、死锁、阻塞。GC 日志先看 Young GC 和 Full GC 是否频繁,再看停顿时间、触发原因、回收前后内存变化和晋升情况。Full GC 后内存回不去,优先怀疑泄漏;能回落则更多看分配过快或参数问题。
高频追问速答
书面笔记版
| 追问 | 答案 |
|---|---|
| JVM 内存结构里哪块不会 OOM | 程序计数器 |
| 堆为什么要分代 | 大部分对象生命周期短,分代处理 GC 效率更高 |
| Full GC 一定只回收老年代吗 | 不严谨,Full GC 通常是更重的全局回收,具体看收集器 |
| 什么对象可以作为 GC Roots | 栈引用、静态变量、常量、JNI 引用、锁持有对象等 |
| 为什么要双亲委派 | 保证核心类安全,避免重复加载 |
| 对象一定先进入新生代吗 | 不一定,大对象或特殊配置可能直接进入老年代 |
| 准备和初始化区别 | 准备赋默认值,初始化执行静态赋值和静态代码块 |
| 频繁 Full GC 第一反应 | 先看监控和 GC 日志,不要先拍脑袋调参 |
面试口述版
JVM 高频追问可以这样记:程序计数器是规范里唯一不会 OOM 的区域;堆分代是因为大部分对象生命周期短;Full GC 不要死说只回收老年代,要说是更重的全局回收;GC Roots 常见有栈引用、静态变量、常量、JNI 引用;双亲委派是为了核心类安全和避免重复加载;对象不一定都先进新生代;类加载准备阶段是默认值,初始化阶段才执行静态赋值。
三分钟串讲版
书面笔记版
JVM 是 Java 程序的运行平台,负责加载和执行字节码。JVM 内存结构常见分为程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中线程私有的是程序计数器、虚拟机栈和本地方法栈,线程共享的是堆和方法区。堆是 GC 核心区域,一般分新生代和老年代,新生代再分 Eden、Survivor0、Survivor1。
JVM 判断对象是否可回收,靠 GC Roots 和可达性分析,而不是引用计数。常见 GC 算法有标记清除、复制、标记整理和分代收集。新生代一般用复制思想,老年代一般用标记清除或标记整理。Full GC 可以理解成一次更重的全局回收,常见触发原因有老年代空间不足、晋升失败、元空间不足和显式 System.gc()。
线上频繁 Full GC 时,先看监控和 GC 日志,判断是老年代打满、对象晋升过快,还是 Full GC 后内存回不去。如果回不去,就导出 dump 分析大对象、支配树和 GC Roots 引用链;如果不是泄漏,再看对象创建过快、缓存过大、参数不合理或显式 GC。类加载、双亲委派和对象创建过程也是 JVM 高频问题。
面试口述版
JVM 是 Java 的运行平台,负责加载和执行字节码。它的内存结构常见分为程序计数器、虚拟机栈、本地方法栈、堆和方法区,其中堆是 GC 的重点区域。对象是否能回收,靠 GC Roots 和可达性分析。GC 算法可以讲标记清除、复制、标记整理和分代收集,新生代适合复制,老年代适合标记清除或标记整理。线上频繁 Full GC 时,我会先看监控和 GC 日志,再根据 Full GC 后内存是否回落判断是泄漏、分配过快还是参数问题。