当前位置: 首页 > ops >正文

JVM 面试精选 20 题(续)

在这里插入图片描述

目录

      • 1. 什么是 Java 内存模型(JMM)?
      • 2. volatile 关键字的作用是什么?
      • 3. synchronized 关键字的作用是什么?它的实现原理是?
      • 4. 什么是偏向锁、轻量级锁和重量级锁?
      • 5. JVM 为什么要使用 CAS(Compare-and-Swap)?
      • 6. JVM 中,方法区和永久代(PermGen)是什么关系?
      • 7. JVM 为什么要将新生代分为 Eden、From Survivor 和 To Survivor 区?
      • 8. 什么是 JVM 类加载器的层次结构?
      • 9. 什么是 Java 中的常量池?
      • 10. 如何判断一个对象是否可以被回收?
      • 11. G1 垃圾回收器的工作原理是什么?
      • 12. JVM 中的 AOT 编译是什么?
      • 13. JVM 中的本地方法接口(JNI)是什么?
      • 14. JVM 调优的步骤和思路是什么?
      • 15. JVM 的即时编译(JIT)和解释器有什么区别?
      • 16. 说说 Minor GC 的过程。
      • 17. 什么是 JVM 的分代收集?为什么分代?
      • 18. 如何判断 GC Roots?
      • 19. JVM 的类加载器可以自定义吗?为什么?
      • 20. 什么是 JVM 的 safepoint?


JVM 面试精选 20 题

1. 什么是 Java 内存模型(JMM)?

解答:

Java 内存模型(Java Memory Model,JMM) 是 Java 虚拟机规范中定义的,用于抽象化计算机硬件内存模型,以解决多线程环境下对共享变量的访问问题。它定义了线程和主内存之间的关系,以及各种操作的可见性、有序性和原子性。

核心概念:

  • 主内存(Main Memory):所有线程共享的内存区域,包含了所有对象的实例数据。
  • 工作内存(Working Memory):每个线程私有的内存区域,保存了该线程使用到的变量在主内存中的副本。

JMM 规定了以下操作:

  • lockunlock:同步操作,确保同一时刻只有一个线程可以持有某个锁。
  • readload:将主内存中的变量值读取到线程的工作内存中。
  • storewrite:将工作内存中的变量值写回到主内存中。

JMM 通过定义一系列规则,确保在多线程环境下,对共享变量的读写操作是可预测的,避免了可见性、有序性等问题。

2. volatile 关键字的作用是什么?

解答:

volatile 关键字是 Java 虚拟机提供的最轻量级的同步机制。它主要有两个作用:

  1. 保证可见性:当一个线程修改了 volatile 变量的值时,新值会立即同步到主内存中,并且其他线程的工作内存中的旧值会失效。因此,其他线程再次读取该变量时,会从主内存中获取最新值。
  2. 保证有序性volatile 禁止了指令重排序。它通过插入内存屏障(Memory Barrier)来确保在其之前的读写操作都已执行完成,并且在其之后的读写操作不会被提前执行。

需要注意的是volatile 只能保证可见性和有序性,不能保证原子性。例如,i++ 操作并不是原子的,它包含“读取-修改-写入”三个步骤,如果多个线程同时操作,仍然会出错。要保证原子性,需要使用 synchronizedjava.util.concurrent.atomic 包下的类。

3. synchronized 关键字的作用是什么?它的实现原理是?

解答:

synchronized 是 Java 中的一个同步关键字,它可以修饰方法或代码块,用于实现线程间的同步。它主要保证了三个特性:

  1. 原子性:被 synchronized 包裹的代码块是不可分割的,要么执行完成,要么不执行。
  2. 可见性:当一个线程执行完 synchronized 代码块后,对共享变量的修改会立即同步到主内存中,其他线程可以看到最新的值。
  3. 有序性synchronized 代码块内的指令不会发生重排序。

实现原理:
synchronized 的实现基于 Monitor(管程)

  • 修饰方法:JVM 通过在方法字节码中添加 ACC_SYNCHRONIZED 标志位来实现。当线程进入该方法时,会获取 Monitor 锁;当方法执行完成(无论正常退出还是异常退出)时,会释放 Monitor 锁。
  • 修饰代码块:JVM 通过 monitorentermonitorexit 字节码指令来实现。monitorenter 指令在进入同步代码块时执行,获取 Monitor 锁;monitorexit 指令在退出同步代码块时执行,释放 Monitor 锁。

4. 什么是偏向锁、轻量级锁和重量级锁?

解答:

这是 HotSpot JVM 为了提高 synchronized 的性能而采用的锁升级策略,锁的状态从无锁逐步升级,直到重量级锁。

  • 偏向锁(Biased Locking)

    • 适用场景:一个线程多次获取同一个锁。
    • 原理:当一个线程第一次获取锁时,JVM 会在对象头中记录该线程 ID,此后该线程进入同步代码块时,无需再次加锁,直接执行。
    • 升级:当另一个线程尝试获取该锁时,偏向锁会升级为轻量级锁。
  • 轻量级锁(Lightweight Locking)

    • 适用场景:在短时间内,多个线程交替获取锁,但没有竞争。
    • 原理:线程在自己的栈帧中创建锁记录(Lock Record),并将对象头的 Mark Word 复制到锁记录中。然后,通过 CAS(Compare-and-Swap)操作尝试将 Mark Word 替换为指向锁记录的指针。如果成功,则获取锁。
    • 升级:如果 CAS 失败(说明有多个线程在竞争),轻量级锁会升级为重量级锁。
  • 重量级锁(Heavyweight Locking)

    • 适用场景:多个线程在激烈竞争同一个锁。
    • 原理:依赖操作系统底层的 Mutex(互斥量)来实现。线程会阻塞并进入内核态,释放 CPU 资源。上下文切换的开销较大。

5. JVM 为什么要使用 CAS(Compare-and-Swap)?

解答:

CAS(Compare-and-Swap) 是一种乐观锁机制,它是一种无锁的、非阻塞的算法。

核心思想:它包含三个操作数:

  • V(需要更新的变量值)
  • A(预期的旧值)
  • B(要更新的新值)

当且仅当 V 的值等于 A 时,才会将 V 的值更新为 B,否则不进行任何操作。

JVM 使用 CAS 的目的:

  • 提高性能:CAS 是一种原子操作,由 CPU 硬件指令支持。它不需要像重量级锁那样,让线程阻塞、进入内核态,减少了上下文切换的开销。
  • 实现无锁并发:通过 CAS,可以在不使用重量级锁的情况下,实现线程安全。例如,java.util.concurrent.atomic 包下的原子类,就是通过 CAS 来保证操作的原子性。

6. JVM 中,方法区和永久代(PermGen)是什么关系?

解答:

方法区(Method Area) 是《Java 虚拟机规范》中定义的一块运行时数据区,用于存放类信息、常量、静态变量等。

永久代(Permanent Generation,PermGen) 是 HotSpot JVM 在 JDK 1.8 之前,对方法区的一种具体实现。它将方法区的数据放在了 Java 堆中。由于永久代有固定大小,因此容易发生 OutOfMemoryError: PermGen space

JDK 1.8 后的变化:

  • 在 JDK 1.8 中,永久代被彻底移除,取而代之的是 元空间(Metaspace)
  • 元空间 将方法区的数据存放到了本地内存中,而不是 JVM 堆中。
  • 这样做的好处是,元空间的大小只受本地内存大小的限制,避免了永久代固定的内存大小带来的 OOM 问题。

7. JVM 为什么要将新生代分为 Eden、From Survivor 和 To Survivor 区?

解答:

为了提高复制算法的效率和内存利用率。

  • 如果只用两个区(复制算法),内存利用率只有 50%。
  • 如果分成 Eden、From Survivor 和 To Survivor 三个区:
    • Eden 区:大部分新创建的对象都分配在这里。
    • Survivor 区:用于存放经过一次 Minor GC 后仍然存活的对象。

GC 流程:

  1. 新对象在 Eden 区创建。
  2. 当 Eden 区满了,触发 Minor GC。
  3. 存活的对象被复制到 To Survivor 区,并且年龄加 1。
  4. From Survivor 区中存活的对象,如果年龄达到晋升老年代的阈值,则进入老年代;否则也复制到 To Survivor 区,年龄加 1。
  5. 清空 Eden 区和 From Survivor 区。
  6. 交换 From 和 To 的角色,保证每次 GC 都只使用一块 Survivor 区。

这种方式将复制算法的内存利用率提升到了 90%(Eden 区占 8/10,两个 Survivor 各占 1/10)。

8. 什么是 JVM 类加载器的层次结构?

解答:

Java 虚拟机默认有三个类加载器,它们之间是父子关系,遵循双亲委派模型:

  1. 启动类加载器(Bootstrap ClassLoader)

    • 负责加载 <JAVA_HOME>/lib 目录下的核心类库,如 rt.jar
    • 它不是 java.lang.ClassLoader 的子类,是用 C++ 实现的,所以无法在 Java 代码中直接获取它的引用。
  2. 扩展类加载器(Extension ClassLoader)

    • 负责加载 <JAVA_HOME>/lib/ext 目录下的扩展类库。
    • 它的父类加载器是启动类加载器。
  3. 应用程序类加载器(Application ClassLoader)

    • 负责加载用户类路径(CLASSPATH)上的所有类。
    • 它的父类加载器是扩展类加载器。

9. 什么是 Java 中的常量池?

解答:

常量池.class 文件中的一部分,用于存放编译期生成的各种字面量和符号引用。

分类:

  • 静态常量池.class 文件中的常量池。
  • 运行时常量池:当类加载到内存中后,静态常量池中的内容会被加载到方法区(JDK 1.8 后是元空间)的运行时常量池中。

主要内容:

  • 字面量:字符串字面量("hello")、final 变量等。
  • 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。

运行时常量池具有动态性,可以在运行时将新的常量放入池中,例如 String.intern() 方法。


10. 如何判断一个对象是否可以被回收?

解答:

主要有两种方式:

  1. 引用计数算法(Reference Counting)

    • 给每个对象添加一个引用计数器。当有一个地方引用它时,计数器加 1;引用失效时,计数器减 1。当计数器为 0 时,说明该对象可以被回收。
    • 缺点:无法解决对象之间循环引用的问题,导致内存泄漏。因此,JVM 不采用这种算法。
  2. 可达性分析算法(Reachability Analysis)

    • 从一系列被称为 GC Roots 的对象作为起点,向下搜索,形成一个引用链。
    • 如果一个对象无法从任何 GC Roots 节点到达,那么它就是不可达的,可以被回收。
    • GC Roots 包括
      • 虚拟机栈中的引用对象。
      • 方法区中的静态变量和常量。
      • 本地方法栈中的引用。
      • 正在运行的线程对象。

11. G1 垃圾回收器的工作原理是什么?

解答:

G1(Garbage First) 是一款面向大内存服务器的垃圾回收器。其核心思想是将整个 Java 堆划分为多个大小相等的独立区域(Region),每个区域可以是 Eden、Survivor 或者 Old。

工作流程:

  1. 初始标记(Initial Mark):标记所有 GC Roots 能直接访问到的对象。会产生短暂的 STW。
  2. 并发标记(Concurrent Mark):从 GC Roots 开始,对堆中的对象进行可达性分析,此阶段 GC 线程与应用线程并发执行。
  3. 最终标记(Final Mark):修正并发标记期间因应用线程活动导致的对象引用变化。会产生短暂的 STW。
  4. 筛选回收(Mixed GC):G1 会根据回收效率(垃圾最多)和预设的停顿时间,选择部分区域进行回收。它采用复制算法,将存活对象复制到空的区域,同时完成垃圾回收。

G1 的优点:

  • 可预测的停顿时间:可以设置期望的停顿时间(MaxGCPauseMillis),G1 会根据这个值来选择要回收的区域数量。
  • 并行与并发:充分利用多核 CPU,减少 STW。
  • 分代收集:G1 也是一种分代收集器,但它将新生代和老年代区域化,可以同时回收新生代和老年代的垃圾(Mixed GC)。
  • 无内存碎片:G1 主要采用复制算法,因此不会产生内存碎片。

12. JVM 中的 AOT 编译是什么?

解答:

AOT(Ahead-of-Time)编译,即预先编译。它是在程序运行之前,将 Java 字节码直接编译为本地机器码。这与 JIT 编译(运行时编译)是相对的。

AOT 的优点:

  • 启动速度快:由于代码在运行前已经编译好,省去了 JIT 编译和解释执行的开销,程序启动非常迅速。
  • 峰值性能高:由于在编译时可以进行更充分的优化,理论上可以达到更高的执行效率。

AOT 的缺点:

  • 牺牲跨平台性:生成的本地机器码依赖于特定的操作系统和 CPU 架构。
  • 动态性限制:无法在运行时进行一些动态优化,比如根据运行时信息调整代码执行路径。

13. JVM 中的本地方法接口(JNI)是什么?

解答:

JNI(Java Native Interface) 是 Java 平台标准的一部分,允许 Java 代码与运行在 JVM 上的其他语言(如 C、C++)代码进行交互。

主要作用:

  • 调用本地方法:Java 程序可以通过 JNI 调用用其他语言编写的函数。
  • 访问系统资源:Java 无法直接操作操作系统底层,可以通过 JNI 调用本地方法来访问操作系统资源,如文件系统、硬件设备等。

14. JVM 调优的步骤和思路是什么?

解答:

JVM 调优是一个系统性的过程,通常遵循以下步骤:

  1. 监控与分析

    • 使用 jstatjstackjmapVisualVM 等工具监控 JVM 状态,收集 GC 日志、堆栈信息等。
    • 分析应用程序的性能瓶颈,是 CPU 密集型还是 IO 密集型?是内存泄漏还是 GC 频繁?
  2. 确定目标

    • 调优的目的是什么?是减少 Full GC 次数?减少 GC 停顿时间?还是提高吞吐量?
  3. 参数调整

    • 根据分析结果,调整 JVM 启动参数,如 -Xms-Xmx-Xmn-XX:NewRatio 等。
    • 针对性地选择合适的垃圾回收器,例如:
      • 如果追求低延迟,选择 CMS 或 G1。
      • 如果追求高吞吐量,选择 Parallel Scavenge。
  4. 代码优化

    • 调优参数只是治标,根本原因在代码。
    • 检查代码是否存在内存泄漏,如未关闭的资源、未从集合中移除的对象。
    • 减少大对象的创建,尤其是频繁创建的临时大对象。
    • 优化算法和数据结构,减少不必要的对象创建。
  5. 持续观察

    • 每次调整后,重新进行监控,观察效果,不要一次调整太多参数。

15. JVM 的即时编译(JIT)和解释器有什么区别?

解答:

特性解释器JIT 编译器
执行方式逐行翻译字节码并执行将热点代码编译成机器码后执行
启动速度快,无需等待编译慢,需要时间编译
执行效率慢,每次执行都需翻译快,直接执行本地机器码
适用场景程序启动初期,或不频繁执行的代码频繁执行的热点代码
工作模式解释执行编译执行

现代 JVM 采用解释器和 JIT 编译器混合工作的模式,以平衡启动速度和运行效率。


16. 说说 Minor GC 的过程。

解答:

Minor GC 发生在新生代,主要回收 Eden 区和 Survivor 区的垃圾。

  1. 新对象分配:大多数新对象在 Eden 区中创建。
  2. Eden 区满:当 Eden 区空间不足时,触发 Minor GC。
  3. 标记存活对象:从 GC Roots 开始,标记所有 Eden 区和 From Survivor 区中存活的对象。
  4. 复制:将所有存活的对象复制到 To Survivor 区。
  5. 年龄递增:每经过一次 Minor GC 仍然存活的对象,其年龄(age)会加 1。
  6. 晋升老年代:当对象的年龄达到某个阈值(默认 15)时,它们会被移到老年代。
  7. 清空:清空 Eden 区和 From Survivor 区。
  8. 角色交换:将 From Survivor 和 To Survivor 的角色互换。

17. 什么是 JVM 的分代收集?为什么分代?

解答:

分代收集 是基于**弱分代假说(Weak Generational Hypothesis)**的垃圾回收策略。

  • 假说内容:绝大多数对象都是“朝生夕灭”的,存活时间很短。
  • 为什么分代:根据这个假说,可以将堆分为两个或多个区域,每个区域根据其对象的特点采用不同的 GC 策略。
    • 新生代:存放新对象,对象存活率低,适合使用复制算法,效率高。
    • 老年代:存放存活时间长的对象,对象存活率高,适合使用标记-整理标记-清除算法,减少移动开销。

这种分代策略使得 JVM 可以更高效地管理内存,显著减少 GC 停顿时间。

18. 如何判断 GC Roots?

解答:

GC Roots 是垃圾回收器进行可达性分析的起点。一个对象只要能通过引用链从任何一个 GC Roots 对象到达,它就是存活的。

常见的 GC Roots 对象包括:

  • 虚拟机栈中引用的对象:如栈帧中的局部变量表。
  • 本地方法栈中 JNI 引用的对象:即本地方法中使用的对象。
  • 方法区中类静态属性引用的对象:如 static 变量。
  • 方法区中常量引用的对象:如 String 常量池中的引用。
  • 所有处于活动状态的线程对象

19. JVM 的类加载器可以自定义吗?为什么?

解答:

可以自定义。

原因:

  • 动态加载:比如我们编写的 Web 服务器,需要动态加载和卸载应用,这时就需要自定义类加载器。
  • 隔离性:不同的应用程序可能依赖不同版本的同一个类库,自定义类加载器可以实现类之间的隔离,防止冲突。例如,Tomcat 服务器就是通过自定义类加载器来隔离不同 Web 应用的。
  • 加密:为了防止反编译,可以将 .class 文件进行加密,然后编写自定义类加载器,在加载时对文件进行解密。

自定义类加载器只需继承 java.lang.ClassLoader 类,并重写 findClass() 方法。

20. 什么是 JVM 的 safepoint?

解答:

safepoint(安全点) 是 JVM 垃圾回收时的一个重要概念。它是程序执行中的一个特定位置,在这个位置上,所有线程的状态都是已知的,并且可以安全地进行 GC。

为什么需要 safepoint

  • GC 线程需要一个稳定的环境来执行,不能在应用线程随意执行时进行。
  • 如果 GC 线程在应用线程执行一半时进行,可能会导致引用关系混乱,无法正确判断对象存活状态。

当 GC 发生时,JVM 会等待所有线程都运行到 safepoint,然后暂停所有线程(STW),进行 GC 操作。线程进入 safepoint 的方式有两种:

  • 抢占式中断:GC 线程先中断所有线程,然后检查它们是否在 safepoint 上。如果不在,就恢复它们执行,直到它们运行到 safepoint 再暂停。
  • 主动式中断:GC 线程在 safepoint 检查点上设置一个标志,每个线程在执行时,都会主动检查这个标志,如果被设置了,就主动挂起自己。HotSpot JVM 采用的是这种方式。
http://www.xdnf.cn/news/18147.html

相关文章:

  • 面试经验分享-某电影厂
  • 黎阳之光:以数字之力,筑牢流域防洪“智慧防线”
  • 图像采集卡与工业相机:机器视觉“双剑合璧”的效能解析
  • 【ASP.NET Core】ASP.NET Core中间件解析
  • 如何安全删除GitHub中的敏感文件?git-filter-repo操作全解析
  • PowerBI VS FineBI VS QuickBI实现帕累托分析
  • [WiFi]RealTek RF MP Tool操作说明(RTL8192ES)
  • 编排之神--Kubernetes中的认证授权详解
  • PyTorch数据加载利器:torch.utils.data 详解与实践
  • RNN深层困境:残差无效,Transformer为何能深层?
  • 【RustFS干货】RustFS的智能路由算法与其他分布式存储系统(如Ceph)的路由方案相比有哪些独特优势?
  • MySQL深分页性能优化实战:大数据量情况下如何进行优化
  • 阿里云参数配置化
  • C++入门自学Day14-- deque类型使用和介绍(初识)
  • 私有化部署全攻略:开源模型本地化改造的性能与安全评测
  • IPD流程执行检查表
  • 消费者API
  • Flink on Native K8S安装部署
  • 软件系统运维常见问题
  • 快手可灵招海外产品运营实习生
  • 51单片机拼接板(开发板积木)
  • 计算机毕设推荐:痴呆症预测可视化系统Hadoop+Spark+Vue技术栈详解
  • MySQL事务篇-事务概念、并发事务问题、隔离级别
  • Vibe 编码技巧与建议(Vibe Coding Tips and Tricks)
  • AAA服务器技术
  • Qt中使用QString显示平方符号(如²)
  • 搭建最新--若依分布式spring cloudv3.6.6 前后端分离项目--步骤与记录常见的坑
  • 【qml-5】qml与c++交互(类型单例)
  • 前端下载文件、压缩包
  • Java网络编程:TCP与UDP通信实现及网络编程基础