JVM/八股详解(下部):垃圾收集、JVM 调优与类加载机制
JVM八股(下部):垃圾收集、JVM 调优与类加载机制
目录
三、垃圾收集
-
- 讲讲 JVM 的垃圾回收机制
-
- 如何判断对象仍然存活?
-
- Java 中可作为 GC Roots 的引用有哪几种?
-
- finalize() 方法了解吗?
-
- 垃圾收集算法了解吗?
-
- Minor GC、Major GC、Mixed GC、Full GC 都是什么意思?
-
- Young GC 什么时候触发?
-
- 什么时候会触发 Full GC?
-
- 知道哪些垃圾收集器?
-
- 能详细说一下 CMS 的垃圾收集过程吗?
-
- G1 垃圾收集器了解吗?
-
- 有了 CMS,为什么还要引入 G1?
-
- 你们线上用的什么垃圾收集器?
-
- 垃圾收集器应该如何选择?
四、JVM 调优
-
- 用过哪些性能监控的命令行工具?
-
- 了解哪些可视化的性能监控工具?
-
- JVM 的常见参数配置知道哪些?
-
- 做过 JVM 调优吗?
-
- CPU 占用过高怎么排查?
-
- 内存飙高问题怎么排查?
-
- 频繁 Minor GC 怎么办?
-
- 频繁 Full GC 怎么办?
五、类加载机制
-
- 了解类的加载机制吗?(补充)
-
- 类加载器有哪些?
-
- 能说一下类的生命周期吗?
-
- 类装载的过程知道吗?
-
- 什么是双亲委派模型?
-
- 如何破坏双亲委派机制?
-
- 有哪些破坏双亲委派模型的典型例子?
-
- Tomcat 的类加载机制了解吗?
-
- 你觉得应该怎么实现一个热部署功能?
-
- 说说解释执行和编译执行的区别
三、垃圾收集
23. 讲讲 JVM 的垃圾回收机制
垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。
JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。
GC Roots 是一组必须活跃的引用,它们是程序运行时的起点,是一切引用链的源头。
在确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。
垃圾回收的过程是什么?
Java 的垃圾回收过程主要分为标记存活对象、清除无用对象、以及内存压缩/整理三个阶段。不同的垃圾回收器在执行这些步骤时会采用不同的策略和算法。
面经问题:
- 字节跳动面经–有了解JVM吗
- 字节跳动面经–了解过JVM么?讲一下JVM的特性
- 美团面经–了解 GC 吗?不可达对象知道吗?
- 腾讯面经–JVM 垃圾删除
- 得物面经–Java 中垃圾回收的原理
- 快手面经–JVM 了解吗?内存回收机制说一下?
- OPPO面经–垃圾回收的过程是什么?
- vivo面经–说一下GC,有哪些方法
- 荣耀面经–对垃圾回收的理解?
- 字节跳动面经–垃圾回收机制为什么要学jvm 内存泄漏场景
- 腾讯面经–GC?怎么样去识别垃圾?
- 理想汽车面经–说说你对GC的了解?
24. 如何判断对象仍然存活?
Java 通过可达性分析算法来判断一个对象是否还存活。
通过一组名为“GC Roots”的根对象,进行递归扫描,无法从根对象到达的对象就是“垃圾”,可以被回收。
这也是 G1、CMS 等主流垃圾收集器使用的主要算法。
什么是引用计数法?
每个对象有一个引用计数器,记录引用它的次数。当计数器为零时,对象可以被回收。
引用计数法无法解决循环引用的问题。例如,两个对象互相引用,但不再被其他对象引用,它们的引用计数都不为零,因此不会被回收。
做可达性分析的时候,应该有哪些前置性的操作?
在进行垃圾回收之前,JVM 会暂停所有正在执行的应用线程(STW)。
这是因为可达性分析过程必须确保在执行分析时,内存中的对象关系不会被应用线程修改。如果不暂停应用线程,可能会出现对象引用的改变,导致垃圾回收过程中判断对象是否可达的结果不一致,从而引发严重的内存错误或数据丢失。
面经问题:
- 京东面经–如何判断一个对象是否可以回收
- 快手面经–做可达性分析的时候,应该有哪些前置性的操作?
- 京东面经–什么样的对象作垃圾对象
- 小米面经–gc中判断对象可回收的方式有哪些
25. Java 中可作为 GC Roots 的引用有哪几种?
所谓的 GC Roots,就是一组必须活跃的引用,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种:
- 虚拟机栈中的引用(方法的参数、局部变量等)
- 本地方法栈中 JNI 的引用
- 类静态变量
- 运行时常量池中的常量(String 或 Class 类型)
详细说明:
1. 虚拟机栈中的引用
- 具体例子:方法中的局部变量、方法参数
- 生命周期:方法执行期间,局部变量引用的对象是活跃的;方法执行完毕后,若没有其他引用指向该对象,它将有资格被垃圾回收
- 示例代码:
public void test() { Object obj = new Object(); // obj 是 GC Root // 方法执行完毕,obj 出栈,若没有其他引用,Object 对象可被回收 }
2. 本地方法栈中 JNI 的引用
- 具体例子:JNI 方法中创建的引用、通过 JNI 传入的 Java 对象引用
- 生命周期:本地方法执行期间,该引用保持 Java 对象活跃;方法执行完毕后,除非是全局引用,否则对象将被回收
- 注意:全局 JNI 引用需要手动释放,否则会导致内存泄漏
3. 类静态变量
- 具体例子:static 修饰的成员变量
- 生命周期:类加载后,静态变量引用的对象一直存活,直到类被卸载
- 示例代码:
public class Test { private static Object staticObj = new Object(); // staticObj 是 GC Root }
4. 运行时常量池中的常量
- 具体例子:字符串常量、类类型常量
- 生命周期:常量所在的类未被卸载时,常量引用的对象一直存活
- 示例代码:
public class Test { private static final String CONST_STR = "test"; // CONST_STR 引用的字符串对象是 GC Root }
面经问题:
- 帆软面经–哪些对象可以作为 GC Roots
- 腾讯面经–GC Root?
- 小米面经–那些对象可以作为gc root
26. finalize() 方法了解吗?
垃圾回收就是古代的秋后问斩,finalize() 就是刀下留人。
如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选。
筛选的条件是对象是否有必要执行 finalize() 方法。
如果对象在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它就“逃过一劫”;否则就会被回收。
面经问题:
- 通用面经–finalize()方法了解吗?
27. 垃圾收集算法了解吗?
答案:
垃圾收集算法主要有三种,分别是标记-清除算法、标记-复制算法和标记-整理算法。
1. 标记-清除算法
- 执行过程:分为两个阶段
- 标记阶段:标记所有需要回收的对象
- 清除阶段:回收所有被标记的对象
- 优点:实现简单
- 缺点:回收过程中会产生内存碎片
- 适用场景:老年代(对象存活率高)
- 工作示意图:(markdown笔记处理图片有点麻烦,这里就忽略吧,这部分推荐大家去看看图片,一下就理解了)
2. 标记-复制算法
- 执行过程:
- 将内存空间划分为两块,每次只使用其中一块
- 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
- 清理掉这一块内存
- 优点:解决内存碎片问题
- 缺点:浪费了一半的内存空间
- 适用场景:新生代(对象生命周期短)
- 工作示意图:
3. 标记-整理算法
- 执行过程:
- 不再划分内存空间,而是将存活的对象向内存的一端移动
- 清理边界以外的内存
- 优点:无内存碎片
- 缺点:移动对象的成本比较高
- 适用场景:老年代(对象存活率高)
- 工作示意图:
4. 分代收集算法
- 核心思想:根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代
- 新生代:使用复制算法,因为大部分对象生命周期短,可以快速回收
- 老年代:使用标记-整理算法,因为对象存活率较高,可以减少移动对象的成本
- 为什么要用分代收集:根据对象的生命周期优化垃圾回收,提高回收效率。新生代的对象生命周期短,使用复制算法可以快速回收;老年代的对象生命周期长,使用标记-整理算法可以减少移动对象的成本
- 工作原理:
- 新生代:对象创建时首先分配在新生代,大部分对象会很快被回收
- 晋升机制:经过多次 GC 仍然存活的对象会晋升到老年代
- 老年代:存放生命周期较长的对象,使用标记-整理算法减少内存碎片
- 优势:
- 针对不同生命周期的对象使用不同的收集策略
- 提高垃圾收集的效率和针对性
- 减少垃圾收集对应用程序的影响
标记复制的标记过程和复制过程会不会停顿?
- 在标记-复制算法中,标记阶段和复制阶段都会触发 STW(Stop The World)
- 标记阶段停顿是为了保证对象的引用关系不被修改
- 复制阶段停顿是防止对象在复制过程中被修改
面经问题:
- 字节跳动面经–垃圾回收算法了解多少?
- 小米面经–垃圾回收的算法及详细介绍
- 腾讯面经–回收的方法?分代收集算法里面具体是怎么回收的?为什么要用分代收集呢?
- 百度面经–GC 算法有哪些?
- 京东面经–问了垃圾回收算法,针对问了每个算法的优缺点
- 小米面经–gc垃圾回收算法有哪些
28. Minor GC、Major GC、Mixed GC、Full GC 都是什么意思?
- Minor GC(Young GC):发生在年轻代的垃圾收集。
- Major GC(Old GC):发生在老年代的垃圾收集(CMS 特有行为)。
- Mixed GC:G1 垃圾收集器特有,在一次 GC 中同时清理年轻代和部分老年代。
- Full GC:涉及整个 Java 堆和方法区,是最耗时的 GC。
Full GC 怎么去清理的?
Full GC 会从 GC Root 出发,标记所有可达对象。新生代使用复制算法,清空 Eden 区;老年代使用标记-整理算法,回收对象并消除碎片。停顿时间较长,会影响系统响应性能。
面经问题:
- 阿里面经–full gc 和 young gc 的区别
- 腾讯面经–FULL gc 怎么去清理的?
29. Young GC 什么时候触发?
如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。
面经问题:
- 百度面经–什么时候会触发 GC?
30. 什么时候会触发 Full GC?
- 在进行 Young GC 的时候,如果发现老年代可用的连续内存空间 < 新生代历次 Young GC 后升入老年代的对象总和的平均大小,说明本次 Young GC 后升入老年代的对象大小,可能会超过老年代当前可用的内存空间,就会触发 Full GC。
- 执行 Young GC 后老年代没有足够的内存空间存放转入的对象,会立即触发一次 Full GC。
- 执行
System.gc()、jmap -dump等命令会触发 Full GC。
空间分配担保是什么?
空间分配担保是指在进行 Minor GC 前,JVM 会确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足,可能会触发 Full GC。
面经问题:
- 快手面经–如何判断死亡对象?GC Roots有哪些?空间分配担保是什么?
31. 知道哪些垃圾收集器?
答案:
JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器。
分代收集器:
根据对象存活周期的经验规律,将堆内存划分为新生代和老年代等固定区域,并为不同区域采用最适合的垃圾回收算法。
- Serial 收集器:最基础的单线程收集器,进行垃圾收集时必须暂停所有工作线程(STW)
- ParNew 收集器:Serial 收集器的多线程并行版本
- Parallel Scavenge 收集器:关注垃圾收集的吞吐量,适用于多核处理器
- Serial Old 收集器:Serial 收集器的老年代版本,使用标记-整理算法
- Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,使用标记-整理算法
- CMS 收集器:第一个关注 GC 停顿时间的垃圾收集器,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除
分区收集器:
将整个堆内存划分为多个大小相等的独立区域(Region),通过优先回收垃圾比例最高的区域,来实现可预测的低停顿时间
- G1 收集器:在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器
- ZGC 收集器:JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间才 1.68 ms,性能远胜于 G1 和 CMS
各收集器特点对比:
- Serial:单线程,简单高效,适用于单核心环境
- ParNew:多线程,与 CMS 配合使用
- Parallel Scavenge:关注吞吐量,适用于后台计算
- CMS:低延迟,适用于对响应时间敏感的应用
- G1:可预测停顿时间,适用于大内存环境
- ZGC:极低延迟,适用于超大内存环境
各收集器详细介绍:
说说 Serial 收集器?
Serial 收集器是最基础、历史最悠久的收集器。
如同它的名字(串行),它是一个单线程工作的收集器,使用一个处理器或一条收集线程去完成垃圾收集工作。并且进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束——这就是所谓的"Stop The World"。
说说 ParNew 收集器?
ParNew 收集器实质上是 Serial 收集器的多线程并行版本,使用多条线程进行垃圾收集。
说说 Serial Old 收集器?
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
说说 Parallel Old 收集器?
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,基于标记-整理算法实现,使用多条 GC 线程在 STW 期间同时进行垃圾回收。
说说 CMS 收集器?
CMS 在 JDK 1.5 时引入,JDK 9 时被标记弃用,JDK 14 时被移除。
CMS 是一种低延迟的垃圾收集器,采用标记-清除算法,分为初始标记、并发标记、重新标记和并发清除四个阶段,优点是垃圾回收线程和应用线程同时运行,停顿时间短,适合延迟敏感的应用,但容易产生内存碎片,可能触发 Full GC。
说说 G1 收集器?
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
G1 是一种面向大内存、高吞吐场景的垃圾收集器,它将堆划分为多个小的 Region,通过标记-整理算法,避免了内存碎片问题。优点是停顿时间可控,适合大堆场景,但调优较复杂。
说说 ZGC 收集器?
ZGC 是 JDK 11 时引入的一款低延迟的垃圾收集器,最大特点是将垃圾收集的停顿时间控制在 10ms 以内,即便在 TB 级别的堆内存下也能保持较低的停顿时间。
它通过并发标记和重定位来避免大部分 Stop-The-World 停顿,主要依赖指针染色来管理对象状态。
- 标记对象的可达性:通过在指针上增加标记位,不需要额外的标记位即可判断对象的存活状态。
- 重定位状态:在对象被移动时,可以通过指针染色来更新对象的引用,而不需要等待全局同步。
适用于需要超低延迟的场景,比如金融交易系统、电商平台。
垃圾回收器的作用是什么?
垃圾回收器的核心作用是自动管理 Java 应用程序的运行时内存。它负责识别哪些内存是不再被应用程序使用的,并释放这些内存以便重新使用。
面经问题:
- 字节跳动面经–jvm结构 运行时数据区有什么结构堆存什么
- 携程面经–有哪些垃圾回收器,选一个讲一下垃圾回收的流程
- 京东面经–常见的 7 个 GC 回收器
- 美团面经–讲一下知道的垃圾回收器,问知不知道乙GCD回收器(不知道)
- 阿里云面经–cms和g1的区别
- 京东面经–怎么理解并发和并行,Parallel Old和CMS有什么区别?
32. 能详细说一下 CMS 的垃圾收集过程吗?
CMS 使用标记-清除算法,分为 4 步:
- 初始标记:标记 GC Roots 直接可达的对象(STW,速度快)。
- 并发标记:从初始标记对象出发,遍历所有可达对象(并发进行)。
- 重新标记:处理并发标记期间遗漏的引用变化(短暂 STW)。
- 并发清除:清除未被标记的对象(并发进行)。
重新标记(Remark)具体是怎么执行的?三色标记法?
是的,remark 阶段通常结合三色标记法来确保所有存活对象被正确标记。目的是修正并发标记阶段可能遗漏的对象引用变化。
在 remark 阶段,垃圾收集器会停止应用线程,以确保在这个阶段不会有引用关系的进一步变化。这种暂停通常很短暂。remark 阶段主要包括以下操作:
- 处理写屏障记录的引用变化:在并发标记阶段,应用程序可能会更新对象的引用(比如一个黑色对象新增了对一个白色对象的引用),这些变化通过写屏障记录下来。在 remark 阶段,GC 会处理这些记录,确保所有可达对象都正确地标记为灰色或黑色。
- 扫描灰色对象:再次遍历灰色对象,处理它们的所有引用,确保引用的对象正确标记为灰色或黑色。
- 清理:确保所有引用关系正确处理后,灰色对象标记为黑色,白色对象保持不变。这一步完成后,所有存活对象都应当是黑色的。
三色标记法将对象分为三类:
- 白色:尚未访问的对象。垃圾回收结束后,仍然为白色的对象会被认为是不可达的对象,可以回收。
- 灰色:已经访问到但未标记完其引用的对象。灰色对象是需要进一步处理的。
- 黑色:已经访问到并且其所有引用对象都已经标记过。黑色对象是完全处理过的,不需要再处理。
三色标记法的工作流程:
- 初始标记(Initial Marking):从 GC Roots 开始,标记所有直接可达的对象为灰色。
- 并发标记(Concurrent Marking):在此阶段,标记所有灰色对象引用的对象为灰色,然后将灰色对象自身标记为黑色。这个过程是并发的,和应用线程同时进行。
此阶段的一个问题是,应用线程可能在并发标记期间修改对象的引用关系,导致一些对象的标记状态不准确。 - 重新标记(Remark):重新标记阶段的目标是处理并发标记阶段遗漏的引用变化。为了确保所有存活对象都被正确标记,remark 需要在 STW 暂停期间执行。
- 使用写屏障(Write Barrier):来捕捉并发标记阶段应用线程对对象引用的更新。通过遍历这些更新的引用来修正标记状态,确保遗漏的对象不会被错误地回收。
面经问题:
- 携程面经–有哪些垃圾回收器,选一个讲一下垃圾回收的流程
- 携程面经–对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
- 收钱吧面经–CMS用了什么垃圾回收算法?你提到了 remark,那它 remark 具体是怎么执行的?三色标记法?
- 京东面经–问了 CMS 垃圾收集器
33. G1 垃圾收集器了解吗?
G1 在 JDK 1.7 引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
G1 将 Java 堆划分为多个大小相等的 Region,每个 Region 可扮演新生代或老年代的角色。
大对象:超过一个 Region 大小的 50%,会被放入 Humongous 区。例如,每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 区。
这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。
G1 收集器的运行过程:
- 并发标记:通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。
- 混合收集:在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。
- 可预测的停顿:G1 在垃圾回收期间仍然需要 “Stop the World”。不过,G1 在停顿时间上添加了预测机制,用户可以在 JVM 启动时指定期望停顿时间,G1 会尽可能在这个时间内完成垃圾回收。
面经问题:
- 京东面经–说说 G1 垃圾回收器的原理
- 携程面经–对象创建到销毁,内存如何分配的,(类加载和对象创建过程,CMS,G1 内存清理和分配)
- 百度面经–了解过 G1 垃圾回收器吗?
- 理想汽车面经–了解过 G1 垃圾回收器吗?
34. 有了 CMS,为什么还要引入 G1?
| 特性 | CMS | G1 |
|---|---|---|
| 设计目标 | 低停顿时间 | 可预测的停顿时间 |
| 并发性 | 是 | 是 |
| 内存碎片 | 容易产生碎片 | 通过区域划分和压缩减少碎片 |
| 收集范围 | 年轻代和老年代 | 整个堆,但区分年轻代和老年代 |
| 并发阶段 | 并发标记、并发清理 | 并发标记、并发清理、并发回收 |
| 停顿时间预测 | 较难预测 | 可配置停顿时间目标 |
| 容易出现的问题 | 内存碎片、Concurrent Mode Failure | 较少出现长时间停顿 |
| 适用场景 | 对延迟敏感的应用 | 大内存、多核处理器环境 |
CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。
G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。
面经问题:
- 快手面经–CMS 垃圾收集器和 G1 垃圾收集器什么区别
35. 你们线上用的什么垃圾收集器?
回答一(推荐):我们生产环境采用 G1 垃圾收集器,因为它不仅能满足低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题。G1 非常适合大内存、多核处理器的环境。
回答二(实际):大多数情况下我们使用 JDK 8 默认的 Parallel GC(Parallel Scavenge + Parallel Old),适用于多核处理器、高吞吐量场景。
查看命令:
java -XX:+PrintCommandLineFlags -version
命令输出示例:
java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)
不同垃圾收集器的算法:
- G1 收集器:采用分区式标记-整理算法,将堆划分为多个区域,按需回收,适用于大内存和多核环境,能够同时考虑吞吐量和暂停时间。
- CMS 收集器:采用标记-清除算法,能够并发标记和清除垃圾,减少暂停时间,适用于对延迟敏感的应用。
- Parallel 收集器:年轻代使用复制算法,老年代使用标记-整理算法,适用于高吞吐量要求的应用。
工作中项目使用的什么垃圾回收算法?
我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,G1 采用的是分区式标记-整理算法,将堆划分为多个区域,按需回收,适用于大内存和多核环境,能够同时考虑吞吐量和暂停时间。
或者:
我们系统采用的是 CMS 收集器,CMS 采用的是标记-清除算法,能够并发标记和清除垃圾,减少暂停时间,适用于对延迟敏感的应用。
再或者:
我们系统采用的是 Parallel 收集器,Parallel 采用的是年轻代使用复制算法,老年代使用标记-整理算法,适用于高吞吐量要求的应用。
面经问题:
- 华为OD面经–工作中项目使用的什么垃圾回收算法
36. 垃圾收集器应该如何选择?
答案:
选择垃圾收集器需要根据应用场景和硬件环境来决定:
如果应用程序只需要一个很小的内存空间(大约 100 MB),或者对停顿时间没有特殊的要求,可以选择 Serial 收集器。
如果优先考虑应用程序的峰值性能,并且没有时间要求,或者可以接受 1 秒或更长的停顿时间,可以选择 Parallel 收集器。
如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内,可以选择 CMS/G1 收集器。
如果响应时间是高优先级的,或者堆空间比较大,可以选择 ZGC 收集器。
面经问题:
- 通用面经–垃圾收集器应该如何选择?
四、JVM 调优
37. 用过哪些性能监控的命令行工具?
- 操作系统层面:
- top:实时监控系统整体资源使用情况,包括CPU、内存、进程占用率等。
- vmstat:报告系统虚拟内存统计信息,涵盖CPU活动、内存使用、I/O操作等系统级指标。
- iostat:专注于设备和分区的I/O统计信息,如磁盘读写速率、I/O等待时间等。
- netstat:显示网络连接、路由表、接口统计等信息,用于监控网络使用情况。
- JDK 命令行工具:
- jps:列出当前运行的Java进程,类似ps命令但专门针对Java应用。
- jstat:监控JVM运行时统计信息,如垃圾回收、类加载、内存池使用情况等。
- jinfo:查看或动态修改JVM配置参数(如堆大小、GC策略等)。
- jmap:生成Java堆转储快照(heap dump),并提供内存映射信息,用于内存分析。
- jhat:分析jmap生成的堆转储文件,提供Web界面浏览对象引用关系。
- jstack:捕获Java线程转储(thread dump),用于诊断死锁、线程阻塞等问题。
- jcmd:多功能工具,整合了多个JDK工具的功能,可执行VM命令、生成堆转储、查看GC日志等。
你一般都怎么用 jmap?
jmap -heap <pid>:查看堆内存摘要。jmap -histo <pid>:查看对象分布。jmap -dump:format=b,file=<path> <pid>:生成堆转储文件。
面经问题:
- 哔哩哔哩面经–你是如何使用jmap,你用过哪些命令?
38. 了解哪些可视化的性能监控工具?
- JDK 自带:
- JConsole:基于 JMX 的监控工具,可以监控堆内存、线程、类加载、CPU 使用情况等,界面简洁直观。
- VisualVM:集成了多个 JDK 工具的可视化平台,支持内存分析、线程分析、GC 监控等,功能全面。
- Java Mission Control(JMC):JDK 8 开始提供的专业监控工具,包含 Flight Recorder 功能,可详细记录 JVM 运行状态。
- 第三方工具:
- MAT(Memory Analyzer Tool):专注于内存分析,可分析堆转储文件,定位内存泄漏问题。
- GChisto:GC 日志分析工具,可视化展示 GC 行为,帮助调优垃圾收集器。
- JProfiler:专业的性能分析工具,支持 CPU 分析、内存分析、线程分析等,功能强大但收费。
- arthas:阿里巴巴开源的 Java 诊断工具,支持实时查看 JVM 状态、类加载信息、方法执行时间等,无需重启应用。
- async-profiler:低开销的性能分析工具,支持 CPU、内存、锁等分析,适合生产环境使用。
面经问题:
- 华为面经–如何查看当前 Java 程序里哪些对象正在使用,哪些对象已经被释放
39. JVM 的常见参数配置知道哪些?
配置堆内存大小:
-Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:设置年轻代大小-XX:NewRatio=n:设置年轻代和老年代比值-XX:SurvivorRatio=n:设置 Eden 与 Survivor 区比值
配置 GC 收集器:
-XX:+UseSerialGC:串行收集器-XX:+UseParallelGC:并行收集器-XX:+UseConcMarkSweepGC:并发收集器(CMS)-XX:+UseG1GC:G1 收集器
配置并行收集参数:
-XX:MaxGCPauseMillis=n:最大垃圾回收停顿时间-XX:ParallelGCThreads=n:并行收集器线程数
打印 GC 日志:
-XX:+PrintGC:输出 GC 日志-XX:+PrintGCDetails:输出详细 GC 日志-Xloggc:filename:指定 GC 日志文件路径
面经问题:
- 通用面经–JVM 的常见参数配置知道哪些?
40. 做过 JVM 调优吗?
做法:
JVM 调优是一个复杂过程,调优对象包括堆内存、垃圾收集器和 JVM 运行时参数等。
步骤:
- 分析系统运行情况:使用监控工具观察 GC 频率、内存使用等。
- 确定调优目标:如减少 Full GC 频率、降低停顿时间。
- 调整参数:如增大堆内存(
-Xmx)、调整新生代比例、更换收集器。 - 监控与验证:调优后持续监控,对比调优前后差异。
举例:在项目中,我们配置 -Xms2g -Xmx2g 设置堆内存,并使用 VisualVM 监控 GC 日志,发现频繁 Full GC 后通过分析 Heap Dump 定位内存泄漏问题,优化代码后解决。
面经问题:
- 华为面经–说说你对 JVM 调优的了解
41. CPU 占用过高怎么排查?
排查步骤:
top命令查看 CPU 占用高的进程 ID。top -H -p <pid>查看该进程下所有线程的 CPU 占用。- 将高占用线程 ID 转换为十六进制:
printf "%x\n" <tid>。 jstack <pid> > thread-dump.txt抓取线程堆栈。- 在堆栈文件中搜索对应十六进制线程 ID,定位到具体业务方法。
- 分析是否有死循环、频繁 GC、资源竞争等问题。
面经问题:
- 阿里面经–上线的业务出了问题怎么调试,比如某个线程 cpu 占用率高,怎么看堆栈信息
- 快手面经–服务器的 CPU 占用持续升高,有哪些排查问题的手段?排查后发现是项目产生了内存泄露,如何确定问题出在哪里?
42. 内存飙高问题怎么排查?
内存飙高一般是因为创建了大量的 Java 对象导致的,如果持续飙高则说明垃圾回收跟不上对象创建的速度,或者内存泄漏导致对象无法回收。
排查的方法主要分为以下几步:
第一步,先观察垃圾回收情况:
- 使用
jstat -gc <pid> 1000查看 GC 次数和时间。 - 或者使用
jmap -histo <pid> | head -20查看内存占用空间最大的前 20 个对象类型。
第二步,通过 jmap 命令 dump 出堆内存信息:
jmap -dump:format=b,file=heap.hprof <pid>
执行后会生成堆转储文件(如 heap.hprof)。
第三步,使用可视化工具分析 dump 文件:
- 使用 VisualVM、MAT 等工具分析 dump 文件,找到占用内存高的对象。
- 再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。
面经问题:
- 联想面经–怎么定位线上的内存问题。
43. 频繁 Minor GC 怎么办?
频繁的 Minor GC 通常意味着新生代中的对象频繁地被垃圾回收,可能是因为新生代空间设置的过小,或者是因为程序中存在大量的短生命周期对象(如临时变量)。
排查与分析:
- 使用 GC 日志进行分析,查看 GC 的频率和耗时,找到频繁 GC 的原因:
-XX:+PrintGCDetails -Xloggc:gc.log - 或者使用监控工具查看堆内存的使用情况,特别是新生代(Eden 和 Survivor 区)的使用情况。
解决方案:
-
增加新生代大小:如果是因为新生代空间不足,可以通过
-Xmn增加新生代的大小,减缓新生代的填满速度。java -Xmn256m your-app.jar -
调整 Survivor 区比例:如果对象需要长期存活,但频繁从 Survivor 区晋升到老年代,可以通过
-XX:SurvivorRatio参数调整 Eden 和 Survivor 的比例。- 默认比例是 8:1,表示 8 个空间用于 Eden,1 个空间用于 Survivor 区。
- 调整为 6 的话,会减少 Eden 区的大小,增加 Survivor 区的大小,以确保对象在 Survivor 区中存活的时间足够长,避免过早晋升到老年代:
-XX:SurvivorRatio=6
-
优化代码:减少临时对象创建,优化短生命周期对象的使用。
面经问题:
- 京东面经–young GC频繁如何排查?修改哪些参数?
44. 频繁 Full GC 怎么办?
频繁的 Full GC 通常意味着老年代中的对象频繁被垃圾回收,可能是因为老年代空间设置的过小,或者是因为程序中存在大量的长生命周期对象。
排查方法:
- 使用监控系统:通过专门的性能监控系统,查看 GC 的频率和堆内存的使用情况,然后根据监控数据分析 GC 的原因。
- 使用 JDK 自带工具:
- 查看堆内存各区域的使用率以及 GC 情况:
jstat -gcutil -h20 <pid> 1000 - 查看堆内存中的存活对象,并按空间排序:
jmap -histo <pid> | head -n20 - 生成堆内存转储文件:
jmap -dump:format=b,file=heap.hprof <pid>
- 查看堆内存各区域的使用率以及 GC 情况:
- 使用可视化工具:如 VisualVM、JConsole 等,查看堆内存的使用情况。
解决方案:
-
大对象直接进入老年代:
- 通过
-XX:PretenureSizeThreshold参数设置大对象直接进入老年代的阈值。 - 或者将大对象拆分成小对象,减少大对象的创建(例如分页)。
- 通过
-
内存泄漏:
- 通过分析堆内存 dump 文件找到内存泄漏的对象。
- 再找到内存泄漏的代码位置并修复。
-
长生命周期对象:
- 及时释放资源,比如 ThreadLocal、数据库连接、IO 资源等。
-
GC 参数配置不合理:
- 通过调整 GC 参数优化 GC 行为。
- 或者直接更换更适合的 GC 收集器,如 G1、ZGC 等。
-
老年代空间不足:
- 适当增加老年代空间大小。
面经问题:
- 得物面经–Java 中 full gc 频繁,有哪些原因
五、类加载机制
45.了解类的加载机制吗?(补充)
了解。
JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终转化成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。
其中最重要的三个概念就是:类加载器、类加载过程和双亲委派模型。
- 类加载器:负责加载类文件,将类文件加载到内存中,生成 Class 对象。
- 类加载过程:包括加载、验证、准备、解析和初始化等步骤。
- 双亲委派模型:当一个类加载器接收到类加载请求时,它会把请求委派给父类加载器去完成,依次递归,直到最顶层的类加载器,如果父类加载器无法完成加载请求,子类加载器才会尝试自己去加载。
面经问题:
- 小米面经–你了解类的加载机制吗?
- 美团面经–java 的类加载机制 双亲委派机制 这样设计的原因是什么
46. 类加载器有哪些?
主要有四种:
①、启动类加载器,负责加载 JVM 的核心类库,如 rt.jar 和其他核心库位于 JAVA_HOME/jre/lib 目录下的类。
②、扩展类加载器,负责加载 JAVA_HOME/jre/lib/ext 目录下,或者由系统属性 java.ext.dirs 指定位置的类库,由 sun.misc.LauncherExtClassLoader实现。③、应用程序类加载器,负责加载classpath的类库,由sun.misc.LauncherExtClassLoader 实现。 ③、应用程序类加载器,负责加载 classpath 的类库,由 sun.misc.LauncherExtClassLoader实现。③、应用程序类加载器,负责加载classpath的类库,由sun.misc.LauncherAppClassLoader 实现。
我们编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器。
④、用户自定义类加载器,通常用于加载网络上的类、执行热部署(动态加载和替换应用程序的组件),或者为了安全考虑,从不同的源加载类。
通过继承 java.lang.ClassLoader 类来实现。
面经问题:
- 通用面经–类加载器有哪些?
47. 能说一下类的生命周期吗?
类的生命周期包括 7 个阶段:加载(Loading)→ 验证(Verification)→ 准备(Preparation)→ 解析(Resolution)→ 初始化(Initialization)→ 使用(Using)→ 卸载(Unloading)。
| 阶段 | 步骤 | 具体内容 |
|---|---|---|
| 加载 | 1. 定位 Class 文件 2. 将字节流加载为内存中的运行时数据结构 3. 生成对应 Class 对象作为访问入口 |
通过类加载器完成,最终生成 Class 对象 |
| 验证 | 1. 文件格式验证 2. 元数据验证 3. 字节码验证 4. 符号引用验证 |
确保 Class 文件的字节流中包含的信息符合当前 JVM 的要求 |
| 准备 | 为静态变量分配内存并设置初始值 | 初始值是数据类型的零值,如 0、false、null 等 |
| 解析 | 将常量池中的符号引用替换为直接引用 | 符号引用是用一组符号描述所引用的目标,直接引用是直接指向目标的指针、相对偏移量或句柄 |
| 初始化 | 执行类构造器 <clinit>() 方法 |
初始化静态变量为指定值,执行静态代码块 |
| 使用 | 实例化对象、调用方法等 | 程序正常运行阶段 |
| 卸载 | 类的 Class 对象被 GC 回收 | 不再使用该类时发生 |
注意:加载、验证、准备、解析、初始化这五个阶段是类加载的过程,而使用和卸载是类在内存中的生命周期。
面经问题:
- 通用面经–能说一下类的生命周期吗?
48. 类装载的过程知道吗?
答案:
知道。
类装载过程包括三个阶段:载入、链接和初始化。
①、载入:将类的二进制字节码加载到内存中。
②、链接可以细分为三个小的阶段:
- 验证:检查类文件格式是否符合 JVM 规范
- 准备:为类的静态变量分配内存并设置默认值。
- 解析:将符号引用替换为直接引用。
③、初始化:执行静态代码块和静态变量初始化。
在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。比如说 static int a = 1;在准备阶段,a 的值为 0,在初始化阶段,a 的值为 1。
换句话说,初始化阶段是在执行类的构造方法,也就是 javap 中看到的 ()。
载入过程 JVM 会做什么?
- 1)通过一个类的全限定名来获取定义此类的二进制字节流。
- 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的访问入口。
面经问题:
- 小米面经–你了解类的加载机制吗?
- 美团面经–讲一下类加载过程,双亲委派模型,双亲委派的好处
- 美团面经–类加载过程
- 快手面经–类装载的执行过程?双亲委派模式是什么?为什么使用这种模式?
49. 什么是双亲委派模型?
答案:
双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。
这个过程会一直向上递归,也就是说,从子加载器到父加载器,再到更上层的加载器,一直到最顶层的启动类加载器。
启动类加载器会尝试加载这个类。如果它能够加载这个类,就直接返回;如果它不能加载这个类,就会将加载任务返回给委托它的子加载器。
子加载器尝试加载这个类。如果子加载器也无法加载这个类,它就会继续向下传递这个加载任务,依此类推。
直到某个加载器能够加载这个类,或者所有加载器都无法加载这个类,最终抛出 ClassNotFoundException。
双亲委派模型的好处:
- 避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
- 保证核心类库的安全性:如
java.lang.*只能由 Bootstrap ClassLoader 加载,防止被篡改。
面经问题:
- 小米面经–你了解类的加载机制吗?
- 阿里云面经–双亲委派机制
50. 如何破坏双亲委派机制?
答案:
重写 ClassLoader 的 loadClass() 方法,不再委托父加载器。
如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法,那些无法被父类加载器加载的类最终会通过这个方法被加载。
面经问题:
- 通用面经–如何破坏双亲委派机制?
51. 有哪些破坏双亲委派模型的典型例子?
- SPI 机制加载 JDBC 驱动:
DriverManager使用线程上下文类加载器加载 SPI 实现类(如 MySQL 驱动)。 - 热部署框架:如 Spring Boot DevTools,自定义类加载器优先加载新版本的类。
SPI 机制:Java 扩展机制,通过 META-INF/services 文件指定服务提供者实现类,允许子加载器加载具体实现。
热部署:在不重启服务器的情况下更新应用程序代码,通常需要自定义类加载器并破坏双亲委派。
面经问题:
- 通用面经–有哪些破坏双亲委派模型的典型例子?
52. Tomcat 的类加载机制了解吗?
Tomcat 扩展了双亲委派模型,主要类加载器包括:
- Bootstrap ClassLoader:加载 Java 核心类库。
- Catalina ClassLoader:加载 Tomcat 的核心类库。
- Shared ClassLoader:加载共享类库。
- WebApp ClassLoader:加载 Web 应用程序类库,支持多应用隔离,优先加载应用自定义类库(破坏了双亲委派)。
面经问题:
- 通用面经–Tomcat 的类加载机制了解吗?
53. 你觉得应该怎么实现一个热部署功能?
思路:
热部署是指在不重启服务器的情况下,动态加载、更新或卸载应用程序的组件,比如类、配置文件等。需要在类加载器的基础上,实现类的重新加载。
我的思路是:
第一步,使用文件监控机制,如 Java NIO 的 WatchService 来监控类文件或配置文件的变化。当监控到文件变更时,触发热部署流程。
第二步,创建一个自定义类加载器,继承 java.lang.ClassLoader,并重写 findClass() 方法,用来加载新的类文件。
示例代码框架:
class HotSwapClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classBytes = loadClassData(name); // 从文件系统加载字节码
return defineClass(name, classBytes, 0, classBytes.length);
}
}
提示:IntelliJ IDEA 提供了热部署功能,当我们修改了代码后,IDEA 会自动保存并编译,如果是 Web 项目,还可以在 Chrome 浏览器中装一个 LiveReload 插件,一旦编译完成,页面就会自动刷新看到最新的效果。对于测试或者调试来说,非常方便。
面经问题:
- 小米面经–那你知道类的热更新的?
54. 说说解释执行和编译执行的区别
- 解释执行:程序运行时,将源代码逐行转换为机器码并执行。
- 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。
Java 的执行方式:
- 源代码编译为字节码(
.class文件)。 - 运行时,JVM 解释器逐行将字节码转换为机器码执行。
- JIT(即时编译):将热点代码编译后存入
CodeCache,下次直接执行机器码,大幅提升效率。
因此,Java 是解释执行与编译执行结合的语言。
面经问题:
- 通用面经–说说解释执行和编译执行的区别
以上内容整理自《面渣逆袭 JVM 篇 V2.1》,百家博客与博主,结合我自己的理解,涵盖垃圾收集、JVM 调优与类加载机制三大部分的核心八股问题与答案,适用于面试准备与技术复习。
更多推荐



所有评论(0)