JVM面试精选 20 题(终)
目录
- 1. Java 虚拟机是如何判断一个类是否可以卸载的?
- 2. Java 虚拟机栈中存储了什么?栈帧又是什么?
- 3. 什么叫 JVM 的内存泄漏(Memory Leak)?
- 4. 什么是偏向锁、轻量级锁和重量级锁?
- 5. 为什么说 ConcurrentHashMap 线程安全,并且效率高?
- 6. JVM 怎么实现方法重载(Overload)和方法重写(Override)?
- 7. 什么是逃逸分析的标量替换?
- 8. JVM 为什么需要常量池?
- 9. 什么是 JNI(Java Native Interface)?
- 10. JVM 的内存模型(JMM)是什么?它解决了什么问题?
- 11. 什么是 GC Roots?哪些对象可以作为 GC Roots?
- 12. 什么是 JVM 的“元空间”(Metaspace)?
- 13. 说说什么是 JVM 内存模型中的“主内存”和“工作内存”?
- 14. 什么是指令重排?为什么会发生?
- 15. 什么是对象头中的 Mark Word?
- 16. 什么是 Java 中的类初始化 `<clinit>()` 方法?
- 17. 说说 Java 中类的加载器有哪些?
- 18. 什么是 JVM 的内存屏障(Memory Barrier)?
- 19. 说说 JVM 的即时编译模式有哪些?
- 20. 简述 JVM 的调优思路和步骤。
1. Java 虚拟机是如何判断一个类是否可以卸载的?
解答:
判断一个类是否可以卸载,需要满足以下三个条件:
- 该类的所有实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
只有同时满足这三个条件,JVM 才会在 GC 时考虑卸载这个类。
2. Java 虚拟机栈中存储了什么?栈帧又是什么?
解答:
Java 虚拟机栈是线程私有的,它的生命周期与线程相同。它存储了用于方法执行过程中的数据,包括:
- 局部变量表(Local Variable Table):存放方法参数和方法内部定义的局部变量。
- 操作数栈(Operand Stack):用于存放方法执行时的中间计算结果。
- 动态链接(Dynamic Linking):指向运行时常量池中该栈帧所属方法的引用。
- 方法返回地址(Return Address):当方法执行完毕后,记录返回到哪里继续执行。
栈帧(Stack Frame) 是虚拟机栈的元素,它是用于支持虚拟机进行方法调用和方法执行的数据结构。每个方法从调用到执行结束,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
3. 什么叫 JVM 的内存泄漏(Memory Leak)?
解答:
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,导致系统可用的内存越来越少。虽然 JVM 有自动垃圾回收机制,但仍然可能发生内存泄漏。
常见的内存泄漏场景:
- 长生命周期的对象持有短生命周期的对象引用:例如,一个静态
HashMap
缓存了很多对象,但这些对象使用完后没有从HashMap
中移除,导致它们永远无法被回收。 - 资源未关闭:如数据库连接、网络连接、文件流等,使用后没有调用
close()
方法释放资源,导致内存泄漏。 - 内部类持有外部类的引用:非静态内部类会隐式持有外部类的引用,如果内部类的生命周期长于外部类,可能导致外部类无法被回收。
4. 什么是偏向锁、轻量级锁和重量级锁?
解答:
这是 Java 对象锁在 JVM 中的不同状态,它们是 JVM 为了提高锁的性能而引入的优化机制。
- 偏向锁(Biased Locking):当一个线程第一次获得锁时,JVM 会在对象头中记录该线程 ID。如果该线程再次进入同步块,则无需任何同步操作,直接执行。当其他线程竞争时,偏向锁会升级为轻量级锁。
- 轻量级锁(Lightweight Locking):当偏向锁升级后,或者多个线程交替访问同步块时,JVM 会使用轻量级锁。它通过自旋(Spinning)来等待锁释放,避免了线程的挂起和恢复,开销较小。
- 重量级锁(Heavyweight Locking):当多个线程同时竞争锁,且自旋无法获取锁时,轻量级锁会膨胀为重量级锁。此时,没有获取到锁的线程会被阻塞(
wait
),进入等待队列,直到锁被释放。
5. 为什么说 ConcurrentHashMap 线程安全,并且效率高?
解答:
ConcurrentHashMap
相比 Hashtable
和 synchronizedMap
,效率高的原因在于它采用了更细粒度的锁机制:
- JDK 1.7:使用 分段锁(Segment Lock)。它将整个哈希表分为多个
Segment
,每个Segment
都是一个独立的锁。当一个线程修改某个Segment
中的数据时,其他线程可以同时访问或修改其他Segment
,实现了并发。 - JDK 1.8:放弃了分段锁,而是采用 CAS(Compare-and-Swap) 和
synchronized
结合的方式。put
操作时,只对需要修改的哈希桶加锁,粒度更小。大多数并发操作(如get
)甚至不需要加锁,通过volatile
保证可见性。
6. JVM 怎么实现方法重载(Overload)和方法重写(Override)?
解答:
-
方法重载(Overload):
- 实现:发生在编译期,通过**方法签名(Method Signature)**来区分,即方法名相同,但参数列表(参数类型、个数和顺序)不同。
- 原理:编译器会根据传入的参数类型、个数等信息,在编译时就确定调用哪个重载方法。
-
方法重写(Override):
- 实现:发生在运行期,子类对父类的方法进行重新实现,要求方法名、参数列表和返回值类型都相同。
- 原理:利用 多态(Polymorphism)。在运行时,JVM 根据对象的实际类型(而不是声明的类型),调用对应的方法。这是通过 虚方法表(Virtual Method Table) 实现的。
7. 什么是逃逸分析的标量替换?
解答:
标量(Scalar) 指一个无法再分解成更小的数据的数据,如 int
、long
等基本类型。聚合量(Aggregate) 则可以继续分解,如对象。
标量替换是指当逃逸分析证明一个对象不会被外部访问,并且这个对象可以被分解时,JVM 不再创建这个对象本身,而是直接创建它的成员变量。
举例:
public void test() {Point point = new Point(1, 2); // Point 是聚合量System.out.println("x=" + point.x + ", y=" + point.y);
}
如果经过逃逸分析,JVM 发现 point
对象只在 test
方法内部使用,并且可以分解,它可能会直接将 point.x
和 point.y
替换为两个独立的局部变量,从而消除对象创建的开销,减轻了 GC 压力。
8. JVM 为什么需要常量池?
解答:
常量池(Constant Pool) 是 class
文件中的一部分,它存储了编译期生成的各种字面量和符号引用。常量池的存在有以下重要作用:
- 节省空间:例如,程序中多次出现的字符串常量,只会在常量池中存储一份。
- 动态链接:在类加载的解析阶段,会将常量池中的符号引用转换为直接引用。
- 支持字面量和符号引用:
- 字面量:如文本字符串、
final
常量值等。 - 符号引用:如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
- 字面量:如文本字符串、
9. 什么是 JNI(Java Native Interface)?
解答:
JNI(Java Native Interface)是 Java 本地接口,它允许 Java 代码与其他语言(主要是 C/C++)编写的代码进行交互。
使用场景:
- 调用底层操作系统或硬件接口:Java 本身无法直接访问操作系统底层功能,通过 JNI 可以调用 C/C++ 实现的本地库。
- 性能敏感的代码:对于某些计算密集型的任务,如果 Java 语言无法达到理想的性能,可以将其核心逻辑用 C/C++ 实现,然后通过 JNI 调用。
- 利用现有 C/C++ 库:重用已有的 C/C++ 代码库,无需重新用 Java 实现。
10. JVM 的内存模型(JMM)是什么?它解决了什么问题?
解答:
JMM(Java Memory Model)是 Java 虚拟机规范中的一部分,它定义了线程和主内存之间的关系,以及程序中各种变量的访问规则。JMM 解决了并发编程中可见性、原子性和有序性的问题。
- 可见性:一个线程对共享变量的修改,对其他线程是立即可见的。
volatile
和synchronized
关键字可以保证可见性。 - 原子性:一个或多个操作,要么全部执行成功,要么全部不执行。
synchronized
可以保证原子性。 - 有序性:程序执行的顺序和代码的编写顺序一致。JMM 允许编译器和处理器进行指令重排,以提高性能。
volatile
和synchronized
可以禁止指令重排。
JMM 旨在屏蔽底层硬件和操作系统的差异,确保 Java 程序在各种平台上都能得到一致的内存访问效果。
11. 什么是 GC Roots?哪些对象可以作为 GC Roots?
解答:
GC Roots 是垃圾回收器进行可达性分析的起始点。从这些根节点开始,GC 遍历所有引用链,任何无法被根节点直接或间接访问到的对象,都被认为是可回收的。
可以作为 GC Roots 的对象包括:
- 虚拟机栈中引用的对象:栈帧中的局部变量表所引用的对象。
- 方法区中静态属性引用的对象:类中的静态变量。
- 方法区中常量引用的对象:字符串常量池中的引用。
- 本地方法栈中 JNI 引用的对象:
native
方法引用的对象。 - 所有被同步锁(
synchronized
)持有的对象。
12. 什么是 JVM 的“元空间”(Metaspace)?
解答:
元空间(Metaspace) 是 JDK 1.8 中用来取代永久代(Permanent Generation) 的内存区域。
- 永久代:在 JDK 1.7 及之前,用于存储类元数据(如类信息、常量池等),它位于 JVM 的堆内存中,受
-XX:MaxPermSize
限制,容易发生 OOM。 - 元空间:在 JDK 1.8 之后,元数据被移出堆,存放在本地内存(Native Memory) 中。理论上,只要服务器物理内存足够,元空间的大小就不受限制。这降低了 OOM 的风险,但也可能导致操作系统内存耗尽。
13. 说说什么是 JVM 内存模型中的“主内存”和“工作内存”?
解答:
这是 JMM 的一个抽象概念。
- 主内存(Main Memory):所有线程共享的内存,存储了所有的共享变量。对应于计算机中的物理内存。
- 工作内存(Working Memory):每个线程私有的内存,存储了该线程操作的共享变量的副本。对应于计算机中的高速缓存(Cache)。
线程对共享变量的操作(读取和修改)都必须在工作内存中进行,不能直接操作主内存。线程之间也无法直接访问彼此的工作内存,线程间通信必须通过主内存。
14. 什么是指令重排?为什么会发生?
解答:
指令重排是指编译器或处理器为了优化程序执行效率,对源代码中的指令执行顺序进行调整,但不改变单线程下的执行结果。
为什么会发生?
- 编译器优化:在不改变结果的前提下,编译器可以调整指令顺序以提高效率。
- 处理器优化:为了充分利用 CPU 的乱序执行能力,处理器会调整指令顺序。
指令重排可能带来的问题:
在并发场景下,如果一个线程修改了共享变量,而另一个线程读取该变量,由于指令重排的存在,可能会导致意想不到的错误。volatile
关键字可以禁止指令重排,保证有序性。
15. 什么是对象头中的 Mark Word?
解答:
Mark Word 是 Java 对象头的一部分,它存储了对象自身的运行时数据。在 64 位 JVM 中,它通常是 64 位(8 字节),包括:
- 哈希码(HashCode):当对象调用
hashCode()
方法时,会存储哈希值。 - GC 年龄(Age):对象在新生代经历的 GC 次数。
- 锁状态标志位:标记该对象所处的锁状态,如无锁、偏向锁、轻量级锁、重量级锁。
- 偏向线程 ID:如果处于偏向锁状态,会存储获取锁的线程 ID。
16. 什么是 Java 中的类初始化 <clinit>()
方法?
解答:
<clinit>()
方法是 JVM 编译器自动生成的一个特殊方法,用于执行类初始化。它包括以下内容:
- 所有静态变量的赋值语句。
- 所有静态代码块中的语句。
特点:
- 线程安全:JVM 会保证一个类的
<clinit>()
方法在多线程环境下被正确地加锁和同步,确保只执行一次。 - 子类初始化:在初始化一个子类之前,会先初始化它的父类。
- 惰性加载:
<clinit>()
方法只在第一次主动使用该类时才会执行。
17. 说说 Java 中类的加载器有哪些?
解答:
Java 默认提供了三层类加载器:
-
启动类加载器(Bootstrap ClassLoader):
- 负责加载
$JAVA_HOME/jre/lib
下的 JDK 核心类库,如rt.jar
。 - 它不是用 Java 实现的,而是用 C++ 实现,无法被 Java 代码直接引用。
- 负责加载
-
扩展类加载器(Extension ClassLoader):
- 负责加载
$JAVA_HOME/jre/lib/ext
目录下的扩展类库。 - 它是用 Java 实现的,其父类加载器是启动类加载器。
- 负责加载
-
应用程序类加载器(Application ClassLoader):
- 负责加载用户类路径(
CLASSPATH
)上的类库。 - 它是我们平时编写的 Java 程序的默认加载器。其父类加载器是扩展类加载器。
- 负责加载用户类路径(
18. 什么是 JVM 的内存屏障(Memory Barrier)?
解答:
内存屏障是 JVM 插入在指令序列中的特殊指令,用于禁止处理器对指令进行重排序,以保证内存操作的可见性和有序性。
- 写屏障(Store Barrier):强制将处理器缓存中的数据写回主内存,保证其他线程的可见性。
- 读屏障(Load Barrier):强制从主内存中读取最新的数据,而不是从处理器缓存中读取。
volatile
关键字就是通过插入内存屏障来保证变量的可见性和有序性。
19. 说说 JVM 的即时编译模式有哪些?
解答:
HotSpot JVM 支持三种即时编译模式:
- 解释模式(-Xint):JVM 纯粹使用解释器执行字节码,不进行 JIT 编译。这种模式启动快,但执行效率低。
- 编译模式(-Xcomp):JVM 优先使用 JIT 编译器,将所有代码编译为本地代码后执行。这种模式启动慢,但执行效率高。
- 混合模式(-Xmixed):JVM 默认的模式。程序启动时使用解释器执行,快速启动;同时,JVM 会监控热点代码,并由 JIT 编译器将其编译成高效的本地代码,以达到最佳性能。
20. 简述 JVM 的调优思路和步骤。
解答:
JVM 调优是一个复杂的过程,一般可以遵循以下步骤:
- 监控和分析:使用 JDK 自带工具(如 JConsole、VisualVM) 或其他 APM 工具,监控 JVM 的各项指标,如 CPU、内存、GC 频率和耗时、线程状态等。
- 确定性能瓶颈:通过监控数据,找出主要问题。通常是 GC 频繁、GC 耗时长、内存泄漏或 CPU 使用率过高等。
- 制定调优策略:
- 内存问题:如果 GC 频繁,可能是堆内存设置过小;如果 Full GC 耗时过长,可能是老年代过大或 GC 算法不合适。
- CPU 问题:可能是线程死锁或热点代码效率低下。
- 调整 JVM 参数:根据策略,调整
-Xmx
、-Xms
、-XX:NewRatio
等参数,或更换垃圾回收器(如从 CMS 切换到 G1)。 - 反复测试和验证:每次只修改一个参数,然后进行压力测试,验证调优效果。重复这个过程,直到达到预期的性能目标。
核心思想:先找出问题,再对症下药,每次只做最小化改动,并进行验证。
希望这 20 道 JVM 面试题的详细解析能帮助你对 JVM 有更深入的理解,并在面试中脱颖而出!