
目录
-
-
- [1. 什么是 JVM、JDK 和 JRE?它们之间的关系是什么?](#1. 什么是 JVM、JDK 和 JRE?它们之间的关系是什么?)
- [2. Java 内存区域(运行时数据区)有哪些?](#2. Java 内存区域(运行时数据区)有哪些?)
- [3. 说说你对 JVM 垃圾回收机制的理解。](#3. 说说你对 JVM 垃圾回收机制的理解。)
- [4. 常用的垃圾回收算法有哪些?](#4. 常用的垃圾回收算法有哪些?)
- [5. 什么是 Minor GC、Major GC 和 Full GC?](#5. 什么是 Minor GC、Major GC 和 Full GC?)
- [6. JVM 调优的常用参数有哪些?](#6. JVM 调优的常用参数有哪些?)
- [7. 说说 Java 对象的创建过程。](#7. 说说 Java 对象的创建过程。)
- [8. 什么是 JVM 类加载机制?](#8. 什么是 JVM 类加载机制?)
- [9. 什么是双亲委派模型?](#9. 什么是双亲委派模型?)
- [10. 常见的 JVM 垃圾回收器有哪些?](#10. 常见的 JVM 垃圾回收器有哪些?)
- [11. 为什么说 CMS 会产生内存碎片?](#11. 为什么说 CMS 会产生内存碎片?)
- [12. 什么是 JIT 编译器?它的作用是什么?](#12. 什么是 JIT 编译器?它的作用是什么?)
- [13. 什么是逃逸分析?](#13. 什么是逃逸分析?)
- [14. 谈谈你对强引用、软引用、弱引用、虚引用的理解。](#14. 谈谈你对强引用、软引用、弱引用、虚引用的理解。)
- [15. 什么是 OOM(Out of Memory)?如何排查?](#15. 什么是 OOM(Out of Memory)?如何排查?)
- [16. JVM 发生 GC 时,STW(Stop-The-World)是什么?](#16. JVM 发生 GC 时,STW(Stop-The-World)是什么?)
- [17. 为什么说 JVM 堆是分代的?](#17. 为什么说 JVM 堆是分代的?)
- [18. 对象在 JVM 中的内存布局是怎样的?](#18. 对象在 JVM 中的内存布局是怎样的?)
- [19. JVM 中的线程死锁如何排查?](#19. JVM 中的线程死锁如何排查?)
- [20. 简述 JVM 的执行引擎。](#20. 简述 JVM 的执行引擎。)
-
JVM(Java Virtual Machine)作为 Java 语言的核心,是每个 Java 开发者都绕不开的话题。在面试中,JVM 相关问题几乎是必考项。本文整理了 20 道常见的 JVM 面试题,并附带详细解答,帮助你更好地准备面试。
1. 什么是 JVM、JDK 和 JRE?它们之间的关系是什么?
解答:
- JVM(Java Virtual Machine) :Java 虚拟机,是运行 Java 字节码的虚拟机。它负责将编译好的
.class
文件翻译成机器码并执行。JVM 只是一个规范,不同的厂商可以有不同的实现,比如 HotSpot。 - JRE(Java Runtime Environment) :Java 运行时环境,它包含了 JVM 和运行 Java 程序所需的核心类库(如
java.lang
、java.util
等)。如果你只需要运行一个 Java 程序,安装 JRE 就足够了。 - JDK(Java Development Kit) :Java 开发工具包,是提供给 Java 开发人员使用的,它包含了 JRE、编译器(
javac
)、调试工具(jdb
)等开发工具。如果你需要编写和编译 Java 程序,必须安装 JDK。
关系:
JDK > JRE > JVM。JDK 包含 JRE,而 JRE 包含 JVM 和核心类库。
2. Java 内存区域(运行时数据区)有哪些?
解答:
根据《Java 虚拟机规范》,Java 虚拟机运行时数据区分为以下几个部分:
- 程序计数器(Program Counter Register) :一块较小的内存空间,是当前线程所执行的字节码的行号指示器。每个线程都有独立的程序计数器,它是线程私有的。
- Java 虚拟机栈(Java Virtual Machine Stacks) :每个方法在执行时都会创建一个栈帧 ,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程私有。
- 本地方法栈(Native Method Stacks) :与虚拟机栈类似,但是为虚拟机使用到的 Native 方法 服务。线程私有。
- Java 堆(Java Heap) :虚拟机所管理的内存中最大的一块。所有线程共享,用于存放对象实例和数组。它是垃圾回收的主要区域。
- 方法区(Method Area) :用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。所有线程共享 。在 JDK 1.8 之后,方法区被 元空间(Metaspace) 取代,元空间在本地内存中,不受 JVM 内存限制。
3. 说说你对 JVM 垃圾回收机制的理解。
解答:
垃圾回收 (Garbage Collection, GC)是 JVM 自动管理内存的一种机制。它的主要任务是回收堆内存中不再使用的对象,释放内存空间。
GC 的基本思想是:找到那些不再被任何引用所指向的对象,然后将其占用的内存回收。为了判断对象是否"存活",JVM 采用了两种主要算法:
- 引用计数算法 :给每个对象添加一个引用计数器。当有地方引用它时,计数器加 1;引用失效时,计数器减 1。当计数器为 0 时,说明该对象可以被回收。但是,它无法解决对象之间循环引用的问题,所以现代 JVM 不使用此算法。
- 可达性分析算法 :通过一系列称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。GC Roots 包括虚拟机栈中的引用对象、方法区中的静态变量和常量等。
4. 常用的垃圾回收算法有哪些?
解答:
-
标记-清除(Mark-Sweep):
- 标记:从 GC Roots 开始标记所有可达对象。
- 清除:遍历整个堆,回收所有未被标记的对象。
- 缺点 :会产生大量不连续的内存碎片,导致后续需要大块连续内存的对象无法分配,提前触发 GC。
-
复制(Copying):
- 将内存分为大小相等的两块,每次只使用其中一块。当这块内存用完时,将存活的对象复制到另一块上,然后清空已使用的这块内存。
- 优点:不会产生内存碎片,实现简单高效。
- 缺点 :内存利用率只有 50%。常用于新生代。
-
标记-整理(Mark-Compact):
- 标记:同标记-清除,标记所有存活对象。
- 整理:让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。
- 优点:不会产生内存碎片。
- 缺点 :效率比标记-清除低,因为需要移动对象。常用于老年代。
-
分代收集:
- 结合了上述算法,根据对象的生命周期将堆分为新生代 和老年代。
- 新生代 :大部分对象"朝生夕灭",采用复制算法,效率高。
- 老年代 :对象存活率高,采用标记-整理或标记-清除算法,减少移动开销。
5. 什么是 Minor GC、Major GC 和 Full GC?
解答:
-
Minor GC(新生代 GC):
- 指发生在新生代的垃圾回收。
- 新生代采用复制算法,因为对象存活率低,效率高。
- 触发条件:Eden 区满时。
-
Major GC(老年代 GC):
- 指发生在老年代的垃圾回收。
- Major GC 通常会伴随一次 Minor GC。
-
Full GC(全堆 GC):
- 指对整个堆(新生代、老年代和方法区/元空间)进行垃圾回收。
- 触发条件:老年代空间不足;方法区空间不足;调用
System.gc()
等。 - Full GC 的代价很高,会造成较长的 STW(Stop-The-World),应尽量避免。
6. JVM 调优的常用参数有哪些?
解答:
-Xms<size>
:设置 JVM 的初始堆内存,等价于-XX:InitialHeapSize
。-Xmx<size>
:设置 JVM 的最大堆内存,等价于-XX:MaxHeapSize
。-Xmn<size>
:设置新生代的大小。-XX:NewRatio=<ratio>
:设置新生代和老年代的比例,例如-XX:NewRatio=2
表示新生代与老年代的比例为 1:2。-XX:MaxMetaspaceSize=<size>
:设置元空间的最大大小。-XX:+PrintGCDetails
:打印详细的 GC 日志。-XX:+UseG1GC
:使用 G1 垃圾回收器。-Xss<size>
:设置每个线程的栈大小。
7. 说说 Java 对象的创建过程。
解答:
- 类加载检查 :当 JVM 遇到
new
指令时,首先检查指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化。 - 分配内存:在类加载检查通过后,为新对象分配内存。
- 初始化零值:内存分配完成后,JVM 会将分配到的内存空间都初始化为零值(不包括对象头),这保证了对象的实例字段在不赋初值时可以直接使用。
- 设置对象头:JVM 会设置对象头中的元数据,比如哈希码、GC 年龄、锁信息、对象所属的类等。
- 执行
<init>
方法:执行对象的构造方法,按照代码中的逻辑进行初始化。
8. 什么是 JVM 类加载机制?
解答:
类加载机制 是 JVM 将 class
文件加载到内存,并对其进行校验、准备、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
加载过程:
- 加载(Loading) :通过类的全限定名获取二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生成一个代表该类的
java.lang.Class
对象。 - 验证(Verification):确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
- 准备(Preparation) :为类的静态变量分配内存,并设置默认初始值(例如
int
类型为 0,boolean
类型为false
)。 - 解析(Resolution):将常量池中的符号引用替换为直接引用。
- 初始化(Initialization) :执行
<clinit>()
方法,对类的静态变量和静态代码块进行赋值。
9. 什么是双亲委派模型?
解答:
双亲委派模型(Parent Delegation Model)是 Java 类加载器的一种工作机制。当一个类加载器收到类加载请求时,它并不会自己先去加载,而是先把这个请求委派给它的父类加载器 去执行。如果父类加载器还存在父类加载器,则继续向上委派,直到最顶层的启动类加载器。只有当父类加载器在它的搜索范围内找不到所需的类时,子类加载器才会尝试自己去加载。
优点:
- 避免重复加载:确保每个类在 JVM 中只加载一次。
- 保证安全性 :防止恶意代码替换核心类库,例如,用户不能自己写一个
java.lang.String
类来欺骗 JVM。
10. 常见的 JVM 垃圾回收器有哪些?
解答:
- Serial(串行):单线程 GC,简单高效,但会造成较长的 STW。适用于单核 CPU 或内存较小的客户端应用。
- ParNew:Serial 的多线程版本,用于新生代。
- Parallel Scavenge:关注吞吐量(Throughput),即 CPU 用于执行用户代码的时间与 GC 时间的比值。可以有效利用多核 CPU。
- CMS(Concurrent Mark Sweep):以获取最短停顿时间为目标的 GC,采用"标记-清除"算法。在并发阶段,GC 线程和用户线程可以同时运行。
- G1(Garbage First) :分代收集器 ,将堆划分为一个个的区域(Region),通过维护一个优先列表,优先回收垃圾最多的区域,从而实现可预测的停顿时间。适用于大内存服务器。
- ZGC / Shenandoah:新一代的低延迟垃圾回收器,旨在实现几乎不中断的 GC 停顿时间(小于 10ms),适用于超大内存的应用。
11. 为什么说 CMS 会产生内存碎片?
解答:
CMS 垃圾回收器采用了标记-清除算法,这个算法的特点是:
- 标记:遍历堆,标记所有存活对象。
- 清除:直接清除所有未标记对象占用的内存。
这个过程中,存活对象的位置不会改变,因此被清除的对象所占用的空间就成了不连续的"空洞",也就是内存碎片。当一个需要大块连续内存的新对象需要分配时,如果现有空闲内存虽然总量足够,但是没有足够大的连续空间,就会导致分配失败,从而不得不触发一次 Full GC。
12. 什么是 JIT 编译器?它的作用是什么?
解答:
JIT(Just-In-Time)编译器 ,又称即时编译器,是 HotSpot JVM 中的一个重要组成部分。它的作用是在程序运行时,将频繁执行的热点代码(Hot Spot Code)编译为本地机器码,从而提高代码的执行效率。
Java 程序最初是解释执行的,即由解释器逐行翻译字节码。JIT 编译器的出现弥补了这一缺点。当 JVM 发现某段代码被多次调用或者是一个循环时,就会将其识别为热点代码,并交由 JIT 编译器编译。编译后的本地代码可以直接运行在操作系统上,效率更高。
13. 什么是逃逸分析?
解答:
**逃逸分析(Escape Analysis)**是 JVM 编译器的一项优化技术,它分析对象是否会被方法外部访问。
- 不逃逸:一个对象只在方法内部使用,不会被外部引用。
- 方法逃逸:对象作为方法的返回值,或者作为参数传递给其他方法。
- 线程逃逸:对象被多个线程共享,例如作为静态变量或者被添加到公共集合中。
逃逸分析的优化:
- 栈上分配:如果一个对象不逃逸,可以直接在栈上分配内存。栈上的内存随着方法结束自动回收,减轻了 GC 压力。
- 同步消除 :如果一个对象只在一个线程中使用,即使它被
synchronized
包裹,JVM 也可以消除这个锁,因为不会发生竞争。 - 标量替换:如果一个对象不逃逸,并且可以拆分为基本类型,那么可以不创建这个对象,直接创建它的字段,节省内存。
14. 谈谈你对强引用、软引用、弱引用、虚引用的理解。
解答:
- 强引用(Strong Reference) :最常见的引用类型,如
Object obj = new Object()
。只要强引用存在,垃圾回收器永远不会回收被引用的对象。 - 软引用(Soft Reference):用于描述一些还有用但非必需的对象。当内存空间不足时,JVM 会回收这些对象。常用于缓存。
- 弱引用(Weak Reference) :用于描述那些非必需的对象。只要发生垃圾回收,无论内存是否充足,都会回收被弱引用关联的对象。常用于
WeakHashMap
。 - 虚引用(Phantom Reference):最弱的引用,无法通过虚引用获取对象实例。它唯一的用途是,在对象被回收时收到一个系统通知。常用于管理直接内存。
15. 什么是 OOM(Out of Memory)?如何排查?
解答:
OOM 指程序在申请内存时,JVM 没有足够的内存空间来分配。
常见的 OOM 类型:
java.lang.OutOfMemoryError: Java heap space
:Java 堆内存不足,可能是创建了太多大对象,或者内存泄漏。java.lang.OutOfMemoryError: Metaspace
:元空间不足,可能是加载了太多类。java.lang.OutOfMemoryError: unable to create new native thread
:无法创建新的本地线程,可能是线程创建过多,或者操作系统对线程数有限制。
排查方法:
- 分析错误日志:查看 OOM 错误的具体类型和信息。
- 分析堆转储文件 :使用
jmap
或HeapDumpOnOutOfMemoryError
生成.hprof
文件,然后使用 MAT(Memory Analyzer Tool) 或 VisualVM 等工具分析堆中对象的分布,找出导致 OOM 的"大对象"或对象数量异常增长。 - 查看 GC 日志 :通过
PrintGCDetails
等参数打印 GC 日志,分析 GC 频率和耗时,判断是否频繁 GC 导致内存不足。
16. JVM 发生 GC 时,STW(Stop-The-World)是什么?
解答:
**STW(Stop-The-World)**是指在进行垃圾回收时,JVM 停止所有的应用线程,直到 GC 过程结束。所有用户线程都被暂停,无法响应请求,就像整个世界都停止了一样。
STW 的目的是为了保证 GC 过程中的数据一致性。如果在 GC 时,用户线程还在不断创建新对象、修改引用关系,那么 GC 线程将无法准确地判断哪些对象是存活的,导致回收错误。
现代的垃圾回收器(如 CMS、G1、ZGC)都在努力减少 STW 的时间,甚至实现并发 GC,让 GC 线程和用户线程同时运行,从而减少对应用程序的影响。
17. 为什么说 JVM 堆是分代的?
解答:
**分代(Generational)**是 JVM 堆内存的一种管理策略,基于一个重要的假设:绝大多数对象都是"朝生夕灭"的。
- 新生代(Young Generation) :用于存放新创建的对象。这里的大多数对象在 Minor GC 后都会被回收。采用复制算法,效率很高。
- 老年代(Old Generation) :用于存放经过多次 Minor GC 仍然存活的对象。这些对象生命周期较长。采用标记-整理 或标记-清除算法,减少移动开销。
这种分代管理可以根据不同区域对象的特点,采用最适合的 GC 算法,从而提高 GC 效率。
18. 对象在 JVM 中的内存布局是怎样的?
解答:
一个 Java 对象在堆内存中主要包含三部分:
-
对象头(Object Header):
- Mark Word:存储对象的哈希码、GC 年龄、锁信息等。
- Klass Pointer:指向对象所属类的元数据指针。
-
实例数据(Instance Data):
- 存储对象的所有成员变量(包括父类的成员变量)。
-
对齐填充(Padding):
- 保证对象的大小是 8 字节的倍数。这不是必需的,只是为了方便 CPU 访问。
19. JVM 中的线程死锁如何排查?
解答:
- 使用
jps
命令:找到 Java 进程 ID。 - 使用
jstack
命令 :jstack <pid>
。jstack
会打印出 JVM 中所有线程的堆栈信息,如果存在死锁,它会明确地在日志中报告死锁信息,并列出涉及死锁的线程和锁。 - 分析
jstack
结果 :仔细阅读jstack
的输出,找到Found one Java-level deadlock
的信息,然后根据堆栈信息分析是哪些线程、哪些锁导致了死锁。 - 使用图形化工具:如 VisualVM,它可以直观地显示线程状态、CPU 使用情况,并能自动分析死锁。
20. 简述 JVM 的执行引擎。
解答:
**执行引擎(Execution Engine)**是 JVM 的核心组成部分,它负责执行被加载到内存中的字节码。执行引擎的工作方式有两种:
- 解释执行:由**解释器(Interpreter)**逐条读取和翻译字节码指令,然后执行。这种方式启动快,但执行效率低。
- 编译执行 :由 JIT 编译器将热点代码编译成机器码。这种方式启动慢,但一旦编译完成,执行效率高。
现代的 JVM 普遍采用解释器和 JIT 编译器并存的混合模式。程序刚启动时,解释器快速执行;当代码被多次调用后,JIT 编译器介入,将热点代码编译成高效的本地代码,以达到最佳性能。