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

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.lryc.cn/news/626071.html

相关文章:

  • JVM对象创建和内存分配
  • SpringAI接入openAI配置出现的问题全解析
  • 今日行情明日机会——20250819
  • Java开发面试实战:Spring Boot微服务与数据库优化案例分析
  • 星图云开发者平台新功能速递 | 微服务管理器:无缝整合异构服务,释放云原生开发潜能
  • 微服务如何集成swagger3
  • 微服务-08.微服务拆分-拆分商品服务
  • UE5 使用RVT制作地形材质融合
  • idea如何设置tab为4个空格
  • CSS backdrop-filter:给元素背景添加模糊与色调的高级滤镜
  • Day08 Go语言学习
  • Ansible 中的文件包含与导入机制
  • 常见 GC 收集器与适用场景:从吞吐量到亚毫秒停顿的全景指南
  • NestJS 依赖注入方式全解
  • TDengine IDMP 运维指南(3. 使用 Ansible 部署)
  • 【上升跟庄买入】副图/选股指标,动态黄色线由下向上穿越绿色基准线时,发出买入信号
  • day32-进程与线程(5)
  • Ubuntu 下面安装搜狗输入法debug记录
  • Ubuntu一键安装harbor脚本
  • WSL虚拟机(我的是ubuntu20.04)将系统文件转移到E盘
  • 机器学习之决策树:从原理到实战(附泰坦尼克号预测任务)
  • LINUX819 shell:for for,shift ,{} ,array[0] array[s] ,declare -x -a
  • 中科米堆CASAIM提供机加工件来料自动化测量尺寸方案
  • 中国互联网医院行业分析
  • Linux下Mysql命令,创建mysql,删除mysql
  • 基于多级缓存架构的Redis集群与Caffeine本地缓存实战经验分享
  • 原牛:一站式自媒体工具平台
  • 【LeetCode题解】LeetCode 153. 寻找旋转排序数组中的最小值
  • [优选算法专题二——找到字符串中所有字母异位词]
  • 工业4.0时代,耐达讯自动化Profibus转光纤如何重构HMI通信新标准?“