返回八股知识点
八股知识点 / 发布 2026-04-22 22:03 / 更新 2026-06-15 01:11

JVM

JVM 是 Java Virtual Machine,Java 虚拟机。它的作用是把 .class 字节码加载进来,然后在不同操作系统上执行。

JavaJVM

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 编译代码等元数据
graph TD A[JVM 运行时数据区] --> B[线程私有] A --> C[线程共享] B --> D[程序计数器] B --> E[虚拟机栈] B --> F[本地方法栈] C --> G[堆] C --> H[方法区 / 元空间]

面试口述版

JVM 内存结构常见分成 5 块:程序计数器、虚拟机栈、本地方法栈、堆、方法区。线程私有的是程序计数器、虚拟机栈和本地方法栈;线程共享的是堆和方法区。堆是对象实例主要分配的地方,也是 GC 的重点区域。方法区在 JDK8 以后主要对应元空间,存类元数据、常量池、静态变量这些信息。

堆区分几个部分

书面笔记版

面试里问堆分几个部分,常见回答是新生代和老年代;新生代内部通常再分 Eden、Survivor0、Survivor1。

堆 = 新生代 + 老年代
新生代 = Eden + S0 + S1

分代原因是大多数对象“朝生夕死”,少数对象存活很久。把短命对象和长寿对象分开处理,可以提高 GC 效率。

新对象通常优先进入 Eden。Eden 满了触发 Minor GC,存活对象复制到 Survivor;对象经历多次 GC 后年龄增加,达到阈值就可能晋升老年代。两个 Survivor 通常一个作为 From,一个作为 To,轮流复制,减少内存碎片并方便连续分配。

graph LR A[新对象] --> B[Eden] B --> C[Minor GC] C --> D[Survivor0 / Survivor1] D --> E[年龄增加] E --> F{达到阈值} F -- 是 --> G[老年代] F -- 否 --> D

面试口述版

堆一般分成新生代和老年代,新生代再分 Eden、Survivor0、Survivor1。新对象通常先进入 Eden,Eden 满了会触发 Minor GC,活下来的对象会在两个 Survivor 之间复制,年龄达到阈值后晋升到老年代。这样设计是因为大部分对象生命周期都很短,分代回收效率更高。

GC Roots 和可达性分析

书面笔记版

JVM 判断对象是否存活,主要靠 GC Roots 和可达性分析,而不是引用计数。

常见 GC Roots:

  • 虚拟机栈中引用的对象,比如局部变量、方法参数。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。
  • 被同步锁持有的对象。
  • JVM 内部引用,比如类加载器、活动线程等。

可达性分析流程:

  1. 从 GC Roots 出发。
  2. 沿着引用链向下查找。
  3. 能被找到的对象是可达对象,认为还活着。
  4. 找不到的对象是不可达对象,可以被回收。

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 GCFull 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 不要上来就改参数,更不要直接加机器,应该先确认现象和根因。

排查顺序:

  1. 看监控:CPU、内存、QPS、RT、GC 次数和停顿。
  2. 看 GC 日志:触发原因、回收前后内存变化、晋升情况。
  3. 判断 Full GC 后内存是否明显回落。
  4. 如果回落不明显,导出 Heap Dump。
  5. 分析大对象、对象数量、支配树和 GC Roots 引用链。
  6. 回到代码定位缓存、集合、ThreadLocal、监听器等持有者。
  7. 最后再考虑调参、扩容或改代码。

常见原因:

  • 内存泄漏,Full GC 后内存降不下来。
  • 对象创建过快,新生代频繁回收并晋升。
  • 大对象过多,直接进入老年代。
  • 长生命周期对象过多,比如缓存不清理。
  • 堆参数不合理,比如新生代太小、老年代预留不足。
  • 元空间问题,大量动态生成类。
  • 显式调用 System.gc()

短期可以扩堆、降流量、关闭显式 GC、调大相关区域;根因治理要修复泄漏、优化缓存策略、减少大对象和临时对象、调整线程池和 GC 参数。

面试口述版

线上频繁 Full GC,我会先看监控确认是内存问题还是流量抖动,再看 GC 日志判断是老年代不足、晋升失败还是元空间问题。如果 Full GC 后内存回不去,就高度怀疑内存泄漏,导出 heap dump 分析大对象和引用链;如果能回落,就看是不是对象创建过快、缓存过大、堆参数不合理或显式 System.gc()。最后再结合代码和参数优化。

项目遇到内存占用高问题如何解决

书面笔记版

以 Java Spring Boot 服务为例,不管是 java -jar 直接启动,还是 Docker 容器启动,排查内存占用高都不能一上来就重启或盲目加内存。比较稳的思路是:先确认现象,再区分是哪类内存高,保留现场后再定位根因。

推荐排查流程:

  1. 确认影响范围:看是单个实例内存高,还是所有实例都高;接口是否超时、是否频繁 Full GC、是否已经 OOM 或被系统杀掉。
  2. 看整体资源:确认机器或容器内存、CPU、负载、磁盘是否异常,避免把系统缓存、容器限制误认为 Java 堆问题。
  3. 区分内存类型:判断是 JVM 堆高、堆外内存高、线程太多、元空间高,还是容器 / 系统层面的 RSS 高。
  4. 保留现场:进程还活着时,优先采集 GC 情况、heap dump、线程栈、JVM 参数、应用日志和系统日志。
  5. 分析根因:结合 dump、GC 日志、监控曲线和代码,定位是缓存过大、集合未清理、大对象查询、线程池堆积、ThreadLocal 泄漏、堆外内存泄漏还是参数不合理。
  6. 先止血再修复:线上先摘流量、限流、扩容或重启恢复;根因明确后再改代码、调参数、加监控。

常见判断方式:

现象 常见原因 排查重点
堆使用率持续升高,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 statsdocker 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 不要直接顶满,可以设置成 512m700m,具体要结合业务压测和监控。

Spring Boot 常见例子:

假设一个订单导出接口一次性查询几十万条订单,并把结果全部放到 List<OrderDTO> 里,再生成 Excel。上线后发现内存占用快速升高,Full GC 频繁,甚至偶发 OOM。

排查时可以这样做:

  1. 通过监控发现内存升高和导出接口调用时间一致。
  2. jstat 看到 Old 区使用率升高,Full GC 后回落不明显或回落很慢。
  3. 导出 heap dump,用 MAT / VisualVM 查看大对象,发现大量 OrderDTOArrayList、Excel 行对象占用内存。
  4. 回到代码发现接口一次性查全量数据,没有分页,也没有流式写出。
  5. 修复方式是改成分页查询、游标读取、流式写 Excel,限制单次导出数量,并把导出任务异步化。

再比如使用本地缓存时,如果直接用 ConcurrentHashMap 保存用户会话、商品信息或接口结果,但没有 TTL 和容量上限,访问量越大缓存越多,Full GC 后内存也降不下来。修复时应该改成 Caffeine / Redis 这类可控缓存,设置最大容量、过期时间和淘汰策略。

线程池也可能导致内存高。比如 Executors.newFixedThreadPool 使用无界队列,请求量大时任务对象不断堆积,内存会持续上涨。修复方式是使用 ThreadPoolExecutor 显式配置队列大小、拒绝策略、线程数和告警指标。

常见优化方向:

  • 代码层面:避免一次性加载大结果集,分页 / 流式处理大文件,及时关闭流和连接,ThreadLocal 用完 remove()
  • 缓存层面:缓存必须有容量上限、过期时间和淘汰策略,不要无限增长。
  • 线程池层面:不用无界队列,控制线程数和队列长度,避免任务无限堆积。
  • JVM 参数:合理设置 -Xms-XmxMaxMetaspaceSizeMaxDirectMemorySize,开启 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,用 jmapjcmd 导出 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,验证是校验安全性,准备阶段给类变量分配内存并设置默认值,解析是把符号引用变成直接引用,初始化阶段才真正执行静态变量赋值和静态代码块。面试里最容易错的是把准备和初始化混在一起。

双亲委派模型

书面笔记版

双亲委派的核心流程:

  1. 类加载器收到类加载请求。
  2. 先不自己加载,而是委派给父加载器。
  3. 父加载器继续向上委派。
  4. 一直到启动类加载器。
  5. 父加载器都加载不了,子加载器才自己尝试加载。

常见类加载器:

  • Bootstrap ClassLoader。
  • Platform / Extension ClassLoader。
  • Application ClassLoader。
  • 自定义 ClassLoader。

设计价值:

  • 保证核心类安全,比如不能让业务代码伪造 java.lang.String
  • 避免类重复加载,减少类型不兼容问题。

典型打破双亲委派场景:

  • SPI:父加载器需要反向加载子路径下的实现类。
  • Tomcat:不同 Web 应用需要类隔离。
  • OSGi、模块化容器:类加载关系更复杂。

面试口述版

双亲委派指类加载请求先交给父加载器处理,只有父加载器加载不了,子加载器才自己加载。这样设计主要是为了保证 Java 核心类安全,避免核心类被篡改,同时避免同一个类被重复加载。典型打破双亲委派的场景有 SPI 和 Tomcat,因为它们需要反向查找实现类或者做应用隔离。

对象创建过程

书面笔记版

Java 执行 new 时,大致经历:

  1. 类加载检查。
  2. 分配内存。
  3. 初始化零值。
  4. 设置对象头。
  5. 执行构造方法 <init>

分配内存方式:

  • 指针碰撞:适用于内存规整。
  • 空闲列表:适用于内存不规整。
  • TLAB:线程本地分配缓冲,减少多线程分配竞争。

对象一般优先在 Eden 分配,因为新生代对象大多短命,批量创建、批量回收效率高。

对象进入老年代的常见情况:

  • 年龄达到阈值。
  • 大对象直接进入老年代。
  • Survivor 放不下。
  • 动态年龄判断触发晋升。

面试口述版

对象创建大致分 5 步:先检查类有没有加载,然后分配内存、初始化零值、设置对象头,最后执行构造方法。对象一般优先分配在 Eden,经过多次 Minor GC 后如果还存活,年龄达到阈值就会晋升到老年代。为了提高分配效率,JVM 还会用 TLAB 减少多线程分配竞争。

内存泄漏定位实战

书面笔记版

内存泄漏的典型表现不是单纯“内存高”,而是对象本来应该被回收,却一直被引用链持有。

典型现象:

  • 堆使用量持续上涨。
  • Full GC 后仍然降不下来。
  • 一段时间后可能 OOM。

定位链路:

  1. 看监控,确认堆使用量是否持续爬升。
  2. 看 GC 日志,确认 Full GC 后内存是否明显回落。
  3. 如果回落不明显,导出 Heap Dump。
  4. 用工具看大对象、对象数量、Dominator Tree、引用链。
  5. 找到是谁从 GC Roots 持有这些对象。
  6. 回到代码修复引用关系。

常见泄漏场景:

  • 静态集合或全局缓存不清理。
  • 线程池场景下 ThreadLocal 使用后不 remove()
  • 监听器、回调没有注销。
  • 连接、流、会话对象没有正确关闭。
  • 缓存无上限,没有 TTL 或容量限制。

面试口述版

内存泄漏我会先看堆使用趋势和 GC 日志,如果 Full GC 后内存降不下来,就导出 heap dump。分析时看哪些类实例数最多、占用最大,再看支配树和从 GC Roots 到对象的引用链,找到是谁持有了本该释放的对象。常见问题有静态集合不清理、ThreadLocal 没 remove、监听器没注销、缓存没有上限。

线上 OOM 了怎么办

书面笔记版

线上 JVM OOM 的处理原则是:先止血,再保留现场,最后定位根因。不要一上来只重启,因为重启会丢掉现场;但如果服务已经不可用,也不能为了保留现场一直不恢复。

处理流程:

  1. 判断影响范围:看是单个实例 OOM,还是整组服务异常;看接口是 500、超时、无响应,还是进程已经被杀掉。
  2. 先摘流量:从负载均衡或注册中心摘掉异常实例,K8s 场景让异常 Pod 不再接收流量。
  3. 保留现场:如果 JVM 进程还活着,优先采集 heap dump、线程栈、GC 情况和系统日志。
  4. 恢复服务:证据足够后重启异常实例;如果服务完全不可用,优先恢复服务。
  5. 根因分析:用 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、部分超时、连接无响应,也可能少量简单接口还能正常返回。端口还在只能说明进程没退出,不代表服务健康。生产上通常会配置 HeapDumpOnOutOfMemoryErrorExitOnOutOfMemoryError,让 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 后内存是否回落判断是泄漏、分配过快还是参数问题。