【Java高频面试问题】JVM篇
类加载机制
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
加载(Loading)
- 通过全限定类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
连接(Linking)
- 验证(Verification):检查字节码是否符合JVM规范(文件格式、元数据、字节码、符号引用等验证)。
- 准备(Preparation):为静态变量分配内存并设置默认初始值(数据类型默认的零值)。若静态变量被
final
修饰且值在编译期可知,则直接赋值。 - 解析(Resolution):将常量池中的符号引用替换为直接引用(如方法或字段的内存地址)。
初始化(Initialization)
- 执行静态变量的显式赋值操作和静态代码块(
static{}
),合并到()
方法中按顺序执行。
注意:加载、验证、准备、初始化的顺序固定,解析阶段可能在初始化之后(支持动态绑定)。
使用(Using)与卸载(Unloading)
类实例化后进入使用阶段;当无引用且类加载器可回收时,类被卸载。
卸载类需要满足 3 个要求:
- 该类的所有的实例对象都已被 GC
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。
类加载器和双亲委派模型
类加载器
JVM 中内置了三个重要的 ClassLoader
:
BootstrapClassLoader
(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库(%JAVA_HOME%/lib
目录下的rt.jar
jar 包和类)以及被-Xbootclasspath
参数指定的路径下的所有类。ExtensionClassLoader
(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。AppClassLoader
(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
如果要自定义类加载器,需要继承 ClassLoader
抽象类。
ClassLoader
类有两个关键的方法:
protected Class loadClass(String name, boolean resolve)
:加载指定二进制名称的类,实现了双亲委派机制 。name
为类的二进制名称,resolve
如果为 true,在加载时调用resolveClass(Class c)
方法解析该类。protected Class findClass(String name)
:根据类的二进制名称来查找类,默认实现是空方法。
如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
双亲委派模型
- 自底向上查找判断类是否被加载
- 自顶向下尝试加载类
每当一个类加载器接收到加载请求时,它会先将请求委派给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
扩展:JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。
好处:避免类的重复加载和防止核心 API 被篡改。
垃圾收集算法
标记-清除算法
标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片。
复制算法
为了解决标记-清除算法的效率和内存碎片问题,复制(Copying)收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用过的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
- 可用内存变小:可用内存缩小为原来的一半。
- 不适合老年代:如果存活对象数量比较大,复制性能会变得很差。
标记-整理算法
标记-整理(Mark-and-Compact)算法是根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 适合老年代这种垃圾回收频率不是很高的场景。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾收集器
串行收集器
Serial 收集器
只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。单线程、适合客户端应用
新生代采用标记-复制算法,老年代采用标记-整理算法。
Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
并行收集器
- 并行(Parallel)[吞吐量优先]:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
多线程高吞吐,适合后台计算
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器。Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。
Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。
JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old
并发收集器
- 并发(Concurrent)[停顿时间优先]:指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户线程的运行。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 短暂停顿,标记直接与 root 相连的对象(根对象);
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。(三色标记)
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对 CPU 资源敏感;
- 无法处理浮动垃圾;
- 它使用的“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 区域化分代,平衡吞吐与停顿
。
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器的运作大致分为以下几个步骤:
- 初始标记: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象
- 并发标记:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。
- 最终标记: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。
- 筛选回收:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从 JDK9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
JVM优化
一、常见JVM调优场景(高频考点)
频繁Full GC或GC停顿过长
- 现象:响应延迟、吞吐量下降,GC日志显示"Concurrent Mode Failure"(CMS)或"Evacuation Failure"(G1)。
- 原因:堆内存不足、大对象分配过量、新生代/老年代比例失调、垃圾回收器选型不当。
- 优化方向:调整堆大小(
-Xms/-Xmx
)、优化分代比例(-XX:NewRatio
)、切换合适GC器。
内存泄漏或OOM(OutOfMemoryError)
- 定位工具:
jmap -histo
分析对象分布、MAT
分析堆转储文件(-XX:+HeapDumpOnOutOfMemoryError
)。 - 典型原因:未释放大对象(如无淘汰策略的缓存)、静态集合长期持有引用、线程泄漏。
- 定位工具:
吞吐量不足或启动性能差
- 优化手段:增大新生代减少Young GC(
-Xmn
)、调整JIT编译阈值(-XX:CompileThreshold
)、类加载优化。
- 优化手段:增大新生代减少Young GC(
二、JVM调优核心步骤
- 数据采集与分析
工具:
- GC日志:
-Xloggc:gc.log -XX:+PrintGCDetails
→ 用GCViewer/GCEasy分析。 - 实时监控:
jstat -gc [pid]
(GC频率/耗时)、jcmd [pid] VM.native_memory
(内存分布)。
- GC日志:
关键指标:Young GC频率≤1次/秒、Full GC频率小时级、GC耗时占比<10%。
堆内存配置
-Xms
和-Xmx
:生产环境建议设为相同值(如-Xms4g -Xmx4g
),避免堆动态扩容引发的Full GC。新生代比例:
- 默认
-XX:NewRatio=2
(老年代:新生代=2:1)。 - 对象朝生夕死场景:增大新生代(
-Xmn
占堆1/3~1/2)。
- 默认
垃圾回收器选型
回收器 | 特点 | 适用场景 |
---|---|---|
Parallel GC | 吞吐量优先 | 批处理、计算密集型任务 |
CMS | 低延迟(并发标记) | Web服务、实时系统 |
G1 | 平衡吞吐/延迟,大堆内存首选 | JDK9+默认,堆>4GB |
ZGC/Shenandoah | 亚毫秒级停顿 | 百GB级堆、金融交易系统 |
- 关键参数示例
# G1调优示例:目标停顿200ms,启用字符串去重
java -XX:+UseG1GC -Xms8g -Xmx8g -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -jar app.jar
# 元空间防OOM(Java 8+)
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
三、调优实战工具链
监控诊断
- JVisualVM:图形化监控堆/线程/GC,支持堆转储分析。
- Arthas:在线诊断线程阻塞、方法执行耗时。
内存分析
- MAT(Memory Analyzer) :定位内存泄漏对象及引用链。
- jmap:生成堆转储文件(
jmap -dump:format=b,file=heap.hprof [pid]
)。
四、面试加分回答要点
调优目标量化:明确优化方向(如“将GC停顿从500ms降至100ms内”)。
结合业务场景:举例说明调优效果(如“电商大促前通过G1替换CMS,峰值期GC停顿降低80%”)。
避坑指南:
- 避免盲目设置
-Xmx
过大引发系统Swap。 - 元空间(Metaspace)泄漏需关注动态类生成(如CGLib)。
- 避免盲目设置
持续更新中…