欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > JVM 面经

JVM 面经

2025/4/3 11:45:15 来源:https://blog.csdn.net/qq_45801780/article/details/146608582  浏览:    关键词:JVM 面经

1、什么是 JVM?

JVM 就是 Java 虚拟机,它是 Java 实现跨平台的基石。程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,交给对应的操作系统执行。这样就实现了 Java 一次编译,处处运行的特性。
在这里插入图片描述

1.1 说说 JVM 的其他特性?

  1. JVM 可以自动管理内存,通过垃圾回收器回收不再使用的对象并释放内存空间。
  2. JVM 包含一个即时编译器 JIT,它可以在运行时将热点代码缓存到 codeCache 中,下次执行的时候不用再一行一行的解释,而是直接执行缓存后的机器码,执行效率会大幅提高。
  3. 任何可以通过 Java 编译的语言,比如说 Groovy、Kotlin、Scala 等,都可以在 JVM 上运行。

1.2 为什么要学习 JVM ?

学习 JVM 可以帮助我们开发者更好地优化程序性能、避免内存问题。比如:了解 JVM 的内存模型和垃圾回收机制,可以帮助我们更合理地配置内存、减少 GC 停顿。掌握 JVM 的类加载机制可以帮助我们排查类加载冲突或异常。JVM 还提供了很多调试和监控工具,可以帮助我们分析内存和线程的使用情况,从而解决内存溢出内存泄露等问题。

2、说说 JVM 的组织架构

JVM 大致可以划分为三个部分:类加载器、运行时数据区和执行引擎。
在这里插入图片描述

  1. 类加载器:负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
  2. 运行时数据区:JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域按照 Java 虚拟机规范可以划分为方法区、堆、虚拟机栈、本地方法栈和程序计数器。
  3. 执行引擎:JVM 的心脏,负责执行字节码。它包括解释器、JIT 编译器和垃圾回收器。

3、能说一下 JVM 的内存区域吗?

按照 Java 虚拟机规范,JVM 的内存区域可以细分为方法区、堆、虚拟机栈、本地方法栈和程序计数器。其中方法区和堆是线程共享的,虚拟机栈、本地方法栈和程序计数器是线程私有的。

3.1 介绍一下方法区?

方法区并不真实存在,是 Java 虚拟机规范中的一个逻辑概念,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。在 HotSpot 虚拟机中,方法区的实现称为永久代 PermGen,但在 Java 8 及之后的版本中,已经被元空间 Metaspace 所替代。

3.2 介绍一下 Java 堆?

堆是 JVM 中最大的一块内存区域,被所有线程共享,在 JVM 启动时创建,主要用来存储 new 出来的对象。Java 中几乎所有的对象都会在堆中分配,堆也是垃圾收集器管理的目标区域。
在这里插入图片描述
从内存回收的角度来看,由于垃圾收集器大部分都是基于分代收集理论设计的,所以堆又被细分为新生代、老年代、Eden空间、From Survivor空间、To Survivor空间等。
在这里插入图片描述
从 JDK 7 开始,JVM 默认开启了逃逸分析,意味着如果某些方法中的对象引用没有被返回或者没有在方法体外使用,也就是未逃逸出去,那么对象可以直接在栈上分配内存。

3.3 介绍一下 Java 虚拟机栈?

Java 虚拟机栈的生命周期与线程相同。当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入虚拟机栈中。当方法执行完毕后,栈帧会从虚拟机栈中移除。

3.4 介绍一下本地方法栈?

本地方法栈与虚拟机栈相似,区别在于虚拟机栈是为 JVM 执行 Java 编写的方法服务的,而本地方法栈是为 Java 调用本地 native 方法服务的,通常由 C/C++ 编写。

3.5 介绍一下本地方法栈的运行场景?

当 Java 应用需要与操作系统底层或硬件交互时,通常会用到本地方法栈。比如调用操作系统的特定功能,如内存管理、文件操作、系统时间、系统调用等。
在这里插入图片描述
在这里插入图片描述

3.6 native 方法解释一下?

native 方法是在 Java 中通过 native 关键字声明的,用于调用非 Java 语言,如 C/C++ 编写的代码。Java 可以通过 JNI,也就是 Java Native Interface 与底层系统、硬件设备、或者本地库进行交互。

3.7 介绍一下程序计数器?

程序计数器也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码行号指示器。

3.8 变量存在堆栈的什么位置?

对于局部变量,它存储在当前方法栈帧中的局部变量表中。当方法执行完毕,栈帧被回收,局部变量也会被释放。对于静态变量来说,它存储在 Java 虚拟机规范中的方法区中,在 Java 7 中是永久代,在 Java 8 及以后是元空间。

4、说一下 JDK 1.6、1.7、1.8 内存区域的变化?

  1. JDK 1.6 使用永久代来实现方法区。
  2. JDK 1.7 依然是永久带,但是将字符串常量池、静态变量存放到了堆上。
  3. JDK 1.8 直接在内存中划出了一块区域,叫元空间,来取代之前放在 JVM 内存中的永久代,并将运行时常量池、类常量池都移动到了元空间。

5、为什么使用元空间替代永久代?

因为永久代受到 JVM 内存大小的限制,会导致 Java 应用程序出现内存溢出的问题。

6、对象创建的过程了解吗?

当我们使用 new 关键字创建一个对象时,JVM 首先会检查 new 指令的参数是否能在常量池中定位到类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,就先执行类加载。如果已经加载,JVM 会为对象分配内存完成初始化,比如数值类型的成员变量初始值是 0,布尔类型是 false,对象类型是 null。接下来,会设置对象头,里面包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。最后,JVM 会执行构造方法 完成赋值操作,将成员变量赋值为预期的值,比如 int age = 18,这样一个对象就创建完成了。
在这里插入图片描述

6.1 对象的销毁过程了解吗?

当对象不再被任何引用指向时,就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。垃圾收集器通过标记清除、标记复制、标记整理等算法来回收内存,释放对象占用的内存空间。

7、堆内存是如何分配的?

在堆中为对象分配内存时,主要使用两种策略:指针碰撞和空闲列表。指针碰撞适用于管理简单、碎片化较少的内存区域,如年轻代;而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景如老年代。
在这里插入图片描述

  1. 指针碰撞:假设堆内存是一个连续的空间,被分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。在分配内存时,Java 虚拟机会维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动一段距离,如果没有发生碰撞,就将这段内存分配给对象实例。
  2. 空闲列表:JVM 会维护一个列表,记录堆中所有未占用的内存块,每个内存块都记录有大小和地址信息。当有新的对象请求内存时,JVM 会遍历空闲列表,寻找足够大的空间来存放新对象。分配后,如果选中的内存块未被完全利用,剩余的部分会作为一个新的内存块加入到空闲列表中。

8、new 对象时,堆会发生抢占吗?

会发生抢占。new 对象时,指针会向右移动一个对象大小的距离,假如一个线程 A 正在给 String 对象 分配内存,另一个线程 B 同时为 ArrayList 对象分配内存,两个线程就发生了抢占。
在这里插入图片描述

8.1 JVM 怎么解决堆内存分配的竞争问题?

为了解决堆内存分配的竞争问题,JVM 为每个线程保留了一小块内存空间,被称为 TLAB,也就是线程本地分配缓冲区,用于存放该线程分配的对象。当线程需要分配对象时,直接从 TLAB 中分配。只有当 TLAB 用尽或对象太大需要直接在堆中分配时,才会使用全局分配指针。
在这里插入图片描述

9、能说一下对象的内存布局吗?

对象的内存布局是由 Java 虚拟机规范定义的,拿我们常用的 HotSpot 来说吧。对象在内存中包括三部分:对象头、实例数据和对齐填充。
在这里插入图片描述

9.1 说说对象头的作用?

对象头是对象存储在内存中的元信息,包含了:Mark Word、类型指针等信息。Mark Word 存储了对象的运行时状态信息,包括锁、哈希值、GC 标记等。在 64 位操作系统下占 8 个字节。类型指针指向对象所属类的元数据,也就是 Class 对象,用来支持多态、方法调用等功能。在开启压缩指针的情况下占 4 个字节,否则占 8 个字节。除此之外,如果对象是数组类型,还会有一个额外的数组长度字段。占 4 个字节。

9.2 实例数据了解吗?

实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声明的顺序存储。JVM 会对这些数据进行对齐 / 重排,以提高内存访问速度。

9.3 对齐填充了解吗?

由于 JVM 的内存模型要求对象的起始地址是 8 字节对齐(64 位 JVM 中),因此对象的总大小必须是 8 字节的倍数。如果对象头和实例数据的总长度不是 8 的倍数,JVM 会通过填充额外的字节来对齐。比如说,如果对象头 + 实例数据 = 14 字节,则需要填充 2 个字节,使总长度变为 16 字节。

9.4 为什么非要进行 8 字节对齐呢?

因为 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,CPU 的访问效率就会降低。8 字节对齐,是一种以空间换时间的方案。
在这里插入图片描述

9.5 new Object() 对象的内存大小是多少?

一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位的 JVM 上,new Object()的大小是 16 字节(12 字节的对象头 + 4 字节的对齐填充)。
在这里插入图片描述
假如 MyObject 对象有三个成员变量,分别是 int、long 和 byte 类型,那么它们占用的内存大小分别是 4 字节、8 字节和 1 字节。考虑到对齐填充,MyObject 对象的总大小为 12 + 4 + 8 + 1 + 7(填充)= 32 字节。

9.6 对象的引用(类型指针)大小了解吗?

在 64 位 JVM 上,未开启压缩指针时,对象引用占用 8 字节;开启压缩指针时,对象引用会被压缩到 4 字节。HotSpot 虚拟机默认是开启压缩指针的。

10、JVM 怎么访问对象的?

主流的方式有两种:句柄和直接指针。句柄是通过中间的句柄表来定位对象的,优点是对象被移动时只需要修改句柄表中的指针,而不需要修改对象引用本身。直接指针是通过引用直接存储对象的内存地址的,因为对象的实例数据和类型信息都存储在堆中固定的内存区域。直接指针访问的优点是访问速度更快,因为少了一次句柄的寻址操作。缺点是如果对象在内存中移动,引用需要更新为新的地址。HotSpot 虚拟机主要使用直接指针来进行对象访问。

11、说一下对象有哪几种引用?

四种,分别是强引用、软引用、弱引用和虚引用。

  1. 强引用:Java 中最常见的引用类型。使用 new 关键字赋值的引用就是强引用,只要强引用关联对象,垃圾收集器就不会回收这部分对象,即使内存不足。
  2. 软引用:用于描述一些非必须对象,通过 SoftReference 类实现。软引用的对象在内存不足时会被回收。
  3. 弱引用:用于描述一些短生命周期的非必须对象,如 ThreadLocal 中的 Entry,就是通过 WeakReference 类实现的。弱引用的对象会在下一次垃圾回收时被回收,不论内存是否充足。
  4. 虚引用:用于跟踪对象被回收的过程,通过 PhantomReference 类实现。虚引用的对象在任何时候都可能被回收。

12、Java 堆的内存分区了解吗?

了解。Java 堆被划分为新生代和老年代两个区域。
在这里插入图片描述
新生代又被划分为 Eden 空间和两个 Survivor 空间(From 和 To)。

新创建的对象会被分配到 Eden 空间。当 Eden 区填满时,会触发一次 Minor GC,清除不再使用的对象。存活下来的对象会从 Eden 区移动到 Survivor 区。

对象在新生代中经历多次 GC 后,如果仍然存活,会被移动到老年代。当老年代内存不足时,会触发 Major GC,对整个堆进行垃圾回收。

13、说一下新生代的区域划分?

新生代的垃圾收集主要采用标记复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高。

基于这种算法,虚拟机将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。
在这里插入图片描述

14、对象什么时候会进入老年代?

对象通常会在新生代中分配,随着时间的推移和垃圾收集的进程,某些满足条件的对象会进入到老年代中,比如长期存活的对象。
在这里插入图片描述

14.1 长期存活的对象如何判断?

JVM 会为对象维护一个年龄计数器,记录对象在新生代中经历 Minor GC 的次数。每次 GC 未被回收的对象,其年龄会加 1。当超过一个特定阈值,默认值是 15,就会被认为老对象了,需要重点关照。

  1. 如果应用中的对象存活时间较短,可以适当调大这个值,让对象在新生代多待一会儿。
  2. 如果对象存活时间较长,可以适当调小这个值,让对象更快进入老年代,减少在新生代的复制次数。

14.2 大对象如何判断?

大对象是指占用内存较大的对象,如大数组、长字符串等。在 G1 垃圾收集器中,大对象会被直接分配到 Humongous 区域。当对象大小超过一个 Region 容量的 50% 时,会被认为是大对象。
在这里插入图片描述

int[] array = new int[1000000];
String str = new String(new char[1000000]);

14.3 动态年龄判定了解吗?

如果 Survivor 区中所有对象的总大小超过了一定比例,通常是 Survivor 区的一半,那么年龄较小的对象也可能会被提前晋升到老年代。

这是因为:如果年龄较小的对象在 Survivor 区中占用了较大的空间,会导致 Survivor 区中的对象复制次数增多,影响垃圾回收的效率。

15、STW 了解吗?

了解。JVM 进行垃圾回收的过程中,会涉及到对象的移动,为了保证对象引用在移动过程中不被修改,必须暂停所有的用户线程,像这样的停顿,我们称之为 Stop The World。简称 STW。

15.1 如何暂停线程呢?

JVM 会使用安全点机制来确保线程能够被安全地暂停,其过程包括四个步骤:

  1. JVM 发出暂停信号。
  2. 线程执行到安全点后,挂起自身并等待垃圾收集完成。
  3. 垃圾回收器完成 GC 操作。
  4. 线程恢复执行。

15.1 什么是安全点?

安全点是 JVM 的一种机制,用于垃圾回收的 STW 操作,用于让线程在执行到某些特定位置时,可以被安全地暂停。通常位于方法调用、循环跳转、异常处理等位置,以保证线程暂停时数据的一致性。
在这里插入图片描述

16、对象一定分配在堆中吗?

不一定。默认情况下,Java 对象是在堆中分配的,但 JVM 会进行逃逸分析,来判断对象的生命周期是否只在方法内部,如果是的话,这个对象可以在栈上分配。

举例来说,下面的代码中,对象 new Person() 的生命周期只在方法内部,因此 JVM 会将这个对象分配在栈上。

public void testStackAllocation() {Person p = new Person();p.name = "沉默王二是只狗";p.age = 18;System.out.println(p.name);
}

16.1 什么是逃逸分析?

逃逸分析是一种 JVM 优化技术,用来分析对象的作用域和生命周期,判断对象是否逃逸出方法或线程。逃逸分析通过分析对象的引用流向,判断对象是否被方法返回、赋值到全局变量、传递到其他线程等,来确定对象是否逃逸。如果对象没有逃逸,就可以进行栈上分配、同步消除、标量替换等优化,以提高程序的性能。

16.2 逃逸具体是指什么?

根据对象逃逸的范围,可以分为方法逃逸和线程逃逸。

  1. 方法逃逸:当对象被方法外部的代码引用,生命周期超出了方法的范围,那么对象就必须分配在堆中。
  2. 线程逃逸:当对象被另外一个线程引用,生命周期超出了线程的范围,那么对象就必须分配在堆中。

16.3 逃逸分析会带来什么好处?

主要有三个:

  1. 如果确定一个对象不会逃逸,就可以考虑栈上分配,使对象占用的内存随栈帧出栈销毁,减少了垃圾收集的压力。
  2. 线程同步需要加锁,加锁就要占用系统资源,如果逃逸分析能够确定一个对象不会逃逸出线程,那么该对象就不用加锁,减少了线程同步的开销。
  3. 如果对象的字段在方法中独立使用,JVM 可以将对象分解为标量变量,避免对象分配。

17、内存溢出和内存泄漏了解吗?

  1. 内存溢出:也就是 OOM,是指当程序请求分配内存时,由于没有足够的内存空间,从而抛出 OutOfMemoryError。通常是因为堆栈内存不足导致的。可以通过优化内存配置、减少对象分配来解决。
  2. 内存泄漏:通常是因为长期对象持有短期对象的引用,又没有释放,从而导致短期对象无法被回收而导致的。

18、能手写内存溢出的例子吗?

class HeapSpaceErrorGenerator {public static void main(String[] args) {// 第一步,创建一个大的容器List<byte[]> bigObjects = new ArrayList<>();try {// 第二步,循环写入数据while (true) {// 第三步,创建一个大对象,一个大约 10M 的数组byte[] bigObject = new byte[10 * 1024 * 1024];// 第四步,将大对象添加到容器中bigObjects.add(bigObject);}} catch (OutOfMemoryError e) {System.out.println("OutOfMemoryError 发生在 " + bigObjects.size() + " 对象后");throw e;}}
}

19、内存泄漏可能由哪些原因导致呢?

  1. 静态的集合中添加的对象越来越多但没有及时清理;静态变量的生命周期与应用程序相同,如果静态变量持有其他对象的引用,这些对象将无法被 GC 回收。
  2. 单例模式下对象持有的外部引用没有及时清理;单例对象的生命周期与应用程序相同,如果单例对象持有其他对象的引用,这些对象将无法被 GC 回收。
  3. 数据库、IO、Socket 等连接资源没有及时关闭。
  4. ThreadLocal 的引用没有清理,线程退出后仍然持有对象引用。

20、有没有处理过内存泄漏问题?

有,当时在做牛券项目的时候,由于 ThreadLocal 没有及时清理导致出现了内存泄漏问题。我用可视化的监控工具 VisualVM,配合 JDK 自带的 jstack 等命令行工具进行了排查。大致的过程我回想了一下,主要有 7 个步骤:

第一步,使用 jps -l 查看运行的 Java 进程 ID。
第二步,使用 top -p [pid] 查看进程使用 CPU 和内存占用情况。
第三步,使用 top -Hp [pid] 查看进程下的所有线程占用 CPU 和内存情况。
第四步,jstack 抓取线程栈,看看有没有线程死锁、死循环或长时间等待。
第五步,使用 jstat 输出 GC 信息,查看 YGC 和 Full GC 次数。
第六步,jmap 生成 dump 文件,然后借助可视化工具分析哪个对象非常多,基本就能定位到问题根源了。
第七步,使用图形化工具分析,我用的是自带的 VisualVM,从菜单 > 文件 > 装入 dump 文件。

然后在结果观察内存占用最多的对象,找到内存泄漏的源头。

21、有没有处理过内存溢出问题?

之前有一次做项目由于上传的文件过大,没有正确处理,导致一下子撑爆了内存,程序直接崩溃了。记得是通过导出堆转储文件进行分析发现的。也是通过 jmap 生成 dump 文件,然后使用 JProfiler 工具分析,查看内存中的对象占用情况。后面我通过增大堆内存大小解决了这个问题 (Xmx4g 参数)。

22、什么情况下会发生栈溢出?

栈溢出发生在程序调用栈的深度超过 JVM 允许的最大深度时。栈溢出的本质是因为线程的栈空间不足,导致无法再为新的栈帧分配内存。
在这里插入图片描述
当一个方法被调用时,JVM 会在栈中分配一个栈帧,用于存储该方法的执行信息。如果方法调用嵌套太深,栈帧不断压入栈中,最终会导致栈空间耗尽,抛出 StackOverflowError。最常见的栈溢出场景就是递归调用,尤其是没有正确的终止条件下,会导致递归无限进行。如果方法中定义了特别大的局部变量,栈帧会变得很大,栈空间更容易耗尽。

23、讲讲 JVM 的垃圾回收机制

垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。JVM 在 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、标记复制算法、标记整理算法、分代收集算法等。我的项目使用的 JDK 8,采用 CMS 垃圾收集器。

23.1 垃圾回收的过程是什么?

Java 的垃圾回收过程主要分为标记存活对象、清除无用对象、以及内存压缩/整理三个阶段。不同的垃圾回收器在执行这些步骤时会采用不同的策略和算法。

24、如何判断对象仍然存活?

Java 通过可达性分析算法来判断一个对象是否存活。通过一组名为 GC Roots 的根对象进行递归扫描,无法从根对象到达的对象就是垃圾,可以被回收。这也是 G1、CMS 等主流垃圾收集器使用的主要算法。
在这里插入图片描述

24.1 什么是引用计数法?

每个对象有一个引用计数器,记录引用它的次数。当计数器为零时,对象可以被回收。引用计数法无法解决循环引用的问题。例如,两个对象互相引用,但不再被其他对象引用,它们的引用计数都不为零,因此不会被回收。
在这里插入图片描述

24.2 做可达性分析的时候,应该有哪些前置性的操作?

在进行垃圾回收之前,JVM 会暂停所有正在执行的应用线程。这是因为可达性分析过程必须确保在执行分析时,内存中的对象关系不会被应用线程修改。如果不暂停应用线程,可能会出现对象引用的改变,导致垃圾回收过程中判断对象是否可达的结果不一致,从而引发严重的内存错误或数据丢失。

25、Java 中可作为 GC Roots 的引用有哪几种?

所谓的 GC Roots,就是一组必须活跃的引用,它们是程序运行时的起点,是一切引用链的源头。在 Java 中,GC Roots 包括以下几种:

  1. Java 虚拟机栈中的引用(方法的参数、局部变量等)。
  2. 本地方法栈中 JNI 的引用。(本地 native 方法通常由 C/C++ 编写)。
  3. 类静态变量。(JDK 7 及之前存在堆的方法区中,JDK 8 及之后存在堆中,而不是用元空间实现的方法区)。
  4. 运行时常量池中的常量(String 或 Class 类型)。
    在这里插入图片描述

25.1 说说虚拟机栈中的引用?

虚拟机栈的局部变量表中存储了方法参数和局部变量,其中:

​1. 基本数据类型​(如 int、float)直接存储值。
2. ​对象引用​(reference类型)存储的是堆中对象的地址或句柄,而非对象本身。当方法执行完成后,局部变量的作用域结束,不再被认为是 GC Roots,因此将被作为垃圾回收掉。

25.2 说说本地方法栈中 JNI 的引用?

当调用 Java 方法时,虚拟机会创建一个栈帧并压入虚拟机栈。而当调用本地方法时,虚拟机会通过动态链接直接调用指定的本地方法。JNI 引用是在 Java 本地接口代码中创建的引用,这些引用可以被认为是 GC Roots。一旦 JNI 方法执行完毕,除非这个引用是全局的,否则它指向的对象将会被作为垃圾回收掉。

25.3 说说类静态变量?

只要类静态变量所属类未被卸载,这个类静态遍历都不会被作为垃圾回收掉。

25.4 说说运行时常量池中的常量?

只要包含这些常量的类未被卸载,这些常量引用的对象就不会被作为垃圾回收掉。

class ConstantPoolReference {public static final String CONSTANT_STRING = "Hello, World"; // 常量,存在于运行时常量池中public static final Class<?> CONSTANT_CLASS = Object.class; // 类类型常量public static void main(String[] args) {System.out.println(CONSTANT_STRING);System.out.println(CONSTANT_CLASS.getName());}
}

26、finalize()方法了解吗?

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被标记,随后进行筛选。筛选的条件是对象是否有必要执行 finalize()方法。如果对象在 finalize() 中重新与引用链上的任何一个对象建立关联,那么在清理时它就逃过一劫,不会被回收掉。

27、垃圾收集算法了解吗?

垃圾收集算法主要有三种,分别是标记清除算法、标记复制算法和标记整理算法。

27.1 说说标记-清除算法?

标记清除算法分为两个阶段:

  1. 标记:标记所有需要回收的对象。
  2. 清除:回收所有被标记的对象。

在这里插入图片描述
优点是实现简单,缺点是回收过程中会产生内存碎片。

27.2 说说标记-复制算法?

标记复制算法可以解决标记清除算法的内存碎片问题,因为它将内存空间划分为两块,每次只使用其中一块。当一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后清理掉一块。
在这里插入图片描述
缺点是浪费了一半的内存空间。

27.3 说说标记-整理算法?

标记整理算法是标记清除复制算法的升级版,它不再划分内存空间,而是将存活的对象向内存的一端移动,然后清理边界以外的内存。
在这里插入图片描述
缺点是移动对象的成本比较高。

27.4 说说分代收集算法?

分代收集算法是目前主流的垃圾收集算法,根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。
在这里插入图片描述
新生代用标记复制算法,因为大部分对象生命周期短。老年代用标记整理算法,因为对象存活率较高。

27.5 为什么要用分代收集呢?

分代收集算法的核心思想是根据对象的生命周期优化垃圾回收。新生代的对象生命周期短,使用标记复制算法可以快速回收。老年代的对象生命周期长,使用标记整理算法可以减少移动对象的成本。

27.6 标记复制的标记过程和复制过程会不会停顿?

在标记复制算法中,标记阶段和复制阶段都会触发 STW。

  1. 标记阶段停顿是为了保证对象的引用关系不被修改。
  2. 复制阶段停顿是防止对象在复制过程中被修改。

28、Minor GC、Major GC、Mixed GC、Full GC 都是什么意思?

  1. Minor GC:也称为 Young GC,是指发生在年轻代的垃圾收集。年轻代包含 Eden 区以及两个 Survivor 区。
  2. Major GC:也称为 Old GC,是指发生在老年代的垃圾收集。是 CMS (JDK 8) 的特有行为。
  3. Mixed GC:是 G1 垃圾收集器特有的一种 GC 类型,它在一次 GC 中同时清理年轻代和部分老年代。
  4. Full GC:是最彻底的垃圾收集,涉及整个 Java 堆和方法区。它是最耗时的 GC,通常在 JVM 压力很大时发生。

28.1 Full GC 是怎么清理的?

Full GC 会从 GC Roots 出发,标记所有可达的对象。新生代使用标记复制算法,清空 Eden 区。老年代使用标记整理算法,回收对象并消除碎片。Full GC 停顿时间较长,会影响系统响应性能。

29、Young GC 什么时候触发?

如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。

30、什么时候会触发 Full GC?

在进行 Young GC 的时候,如果发现老年代可用的连续内存空间 < 新生代历次 Young GC 后升入老年代的对象总和的平均大小,说明本次 Young GC 后升入老年代的对象大小可能超过了老年代当前可用的内存空间,就会触发 Full GC。

30.1 空间分配担保是什么?

空间分配担保是指在进行 Minor GC 前,JVM 会确保老年代有足够的空间存放从新生代晋升的对象。如果老年代空间不足,可能会触发 Full GC。

31、知道哪些垃圾收集器?

JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC。
在这里插入图片描述
CMS:JDK 1.5 时引入,第一个关注 GC 停顿时间的垃圾收集器。
G1: JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。
ZGC:JDK 11 时引入,低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间才 1.68 ms,性能远胜于 CMS 和 G1。

31.1 说说 Serial 收集器?

Serial 收集器是最基础、历史最悠久的收集器。如同它的名字(串行),是单线程工作的收集器,使用一个处理器或一条收集线程去完成垃圾收集工作。进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。即:STW。

31.2 说说 ParNew 收集器?

ParNew 收集器实质上是 Serial 收集器的多线程并行版本,使用多条线程进行垃圾收集。

31.3 说说 Parallel Scavenge 收集器?

Parallel Scavenge 收集器是一款新生代收集器,基于标记复制算法实现,也能够并行收集。和 ParNew 有些类似,但 Parallel Scavenge 主要关注的是垃圾收集的吞吐量,也就是 CPU 运行用户代码的时间和总消耗时间的比值,比值越大,说明垃圾收集的占比越小。根据对象存活周期的不同会将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

31.4 说说 Serial Old 收集器?

Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

31.5 说说 Parallel Old 收集器?

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,基于标记整理算法实现,使用多条 GC 线程在 STW 期间同时进行垃圾回收。

31.6 说说 CMS 收集器?

CMS 在 JDK 1.5 时引入,采用标记清除算法,分为初始标记、并发标记、重新标记和并发清除四个阶段,优点是垃圾回收线程和应用线程同时运行,停顿时间短,适合延迟敏感的应用,但容易产生内存碎片,可能触发 Full GC。

31.7 说说 G1 收集器?

G1 在 JDK 1.7 时引入,是一种面向大内存、高吞吐场景的垃圾收集器,将堆划分为多个小的 Region,通过标记整理算法,避免了内存碎片问题。优点是停顿时间可控,适合大堆场景,但调优较复杂。

31.8 说说 ZGC 收集器?

ZGC 在 JDK 11 时引入,最大特点是将垃圾收集的停顿时间控制在 10ms 以内,即使在 TB 级别的堆内存下也能保持较低的停顿时间。它通过并发标记和重定位来避免大部分 STW 停顿,主要依赖指针染色来管理对象状态。

  1. 标记对象的可达性:通过在指针上增加标记位,不需要额外的标记位即可判断对象的存活状态。
  2. 重定位状态:在对象被移动时,可以通过指针染色来更新对象的引用,而不需要等待全局同步。

适用于需要超低延迟的场景,比如金融交易系统、电商平台。

31.9 垃圾回收器的作用是什么?

垃圾回收器的核心作用是自动管理 Java 应用程序的运行时内存。负责识别哪些内存是不再被应用程序使用的,并释放这些内存以便重新使用。这一过程减少了程序员手动管理内存的负担,降低了内存泄漏和溢出错误的风险。

32、能详细说一下 CMS 的垃圾收集过程吗?

CMS 使用标记清除算法进行垃圾收集,分 4 大步:

  1. 初始标记:标记所有从 GC Roots 直接可达的对象,这个阶段需要 STW,但速度很快。
  2. 并发标记:从初始标记的对象出发,遍历所有对象,标记所有可达的对象。这个阶段是并发进行的。
  3. 重新标记:完成剩余的标记工作,包括处理并发阶段遗留下来的少量变动,这个阶段通常需要短暂的 STW 停顿。
  4. 并发清除:清除未被标记的对象,回收它们占用的内存空间。

32.1 你提到了 remark,那它 remark 具体是怎么执行的?三色标记法?

是的,remark 阶段通常会结合三色标记法来执行,确保在并发标记期间所有存活对象都被正确标记。目的是修正并发标记阶段中可能遗漏的对象引用变化。在 remark 阶段,垃圾收集器会停止应用线程,以确保这个阶段不会有引用关系的进一步变化。这种暂停通常很短暂。remark 阶段主要包括以下操作:

  1. 处理写屏障记录的引用变化:在并发标记阶段,应用程序可能会更新对象的引用(比如一个黑色对象新增了对一个白色对象的引用),这些变化通过写屏障记录下来。在 remark 阶段,GC 会处理这些记录,确保所有可达对象都正确地标记为灰色或黑色。
  2. 扫描灰色对象:再次遍历灰色对象,处理它们的所有引用,确保引用的对象正确标记为灰色或黑色。
  3. 清理:确保所有引用关系正确处理后,灰色对象标记为黑色,白色对象保持不变。这一步完成后,所有存活对象都应当是黑色的。

32.2 什么是三色标记法?

三色标记法用于标记对象的存活状态,它将对象分为三类:

白色:尚未访问的对象。垃圾回收结束后,仍然为白色的对象会被认为是不可达的对象,可以回收。
灰色:已经访问到但未标记完其引用的对象。灰色对象是需要进一步处理的。
黑色:已经访问到并且其所有引用对象都已经标记过。黑色对象是完全处理过的,不需要再处理。

三色标记法的工作流程:

  1. 初始标记:从 GC Roots 开始,标记所有直接可达的对象为灰色。
  2. 并发标记:在此阶段,标记所有灰色对象引用的对象为灰色,然后将灰色对象自身标记为黑色。这个过程是并发的,和应用线程同时进行。此阶段的一个问题是,应用线程可能在并发标记期间修改对象的引用关系,导致一些对象的标记状态不准确。
  3. 重新标记:重新标记阶段的目标是处理并发标记阶段遗漏的引用变化。为了确保所有存活对象都被正确标记,remark 需要在 STW 暂停期间执行。
  4. 使用写屏障:捕捉并发标记阶段应用线程对对象引用的更新。通过遍历这些更新的引用来修正标记状态,确保遗漏的对象不会被错误地回收。

33、G1 垃圾收集器了解吗?

G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。G1 把 Java 堆划分为多个大小相等的独立区域 Region,每个区域都可以扮演新生代或老年代的角色。同时,G1 还有一个专门为大对象 (超过 50% Region 大小) 设计的 Region,叫 Humongous 区。这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。

G1 收集器的运行过程大致可划分为这几个步骤:

  1. 并发标记:找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。
  2. 混合收集:在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),再优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。
  3. 可预测的停顿:G1 在垃圾回收期间仍需 STW。不过,G1 在停顿时间上添加了预测机制,用户可以在 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

34、有了 CMS,为什么还要引入 G1?

CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。

35、你们线上用的什么垃圾收集器?

采用了设计比较优秀的 G1 垃圾收集器,不仅能满足低停顿的要求,还解决了 CMS 的浮动垃圾问题、内存碎片问题。G1 非常适合大内存、多核处理器的环境。可以通过以下命令查看当前 JVM 的垃圾收集器:

java -XX:+PrintCommandLineFlags -version

UseParallelGC = Parallel Scavenge + Parallel Old,表示新生代用Parallel Scavenge收集器,老年代使用Parallel Old 收集器。

因此你也可以这样回答:

我们系统的业务相对复杂,但并发量不是特别高,所以我们选择了适用于多核处理器、能够并行处理垃圾回收任务,且能提供高吞吐量的Parallel GC。

但这个说法不讨喜,你也可以回答:

我们系统采用的是 CMS 收集器,能够最大限度减少应用暂停时间。

35.1 工作中项目使用的什么垃圾回收算法?

我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,G1 采用的是分区式标记整理算法,将堆划分为多个区域,按需回收,适用于大内存和多核环境,能够同时考虑吞吐量和暂停时间。

或者:

我们系统采用的是 CMS 收集器,CMS 采用的是标记清除算法,能够并发标记和清除垃圾,减少暂停时间,适用于对延迟敏感的应用。

再或者:

我们系统采用的是 Parallel 收集器,Parallel 采用的是年轻代使用标记复制算法,老年代使用标记整理算法,适用于高吞吐量要求的应用。

36、垃圾收集器应该如何选择?

  1. 如果应用程序只需很小的内存空间(大约 100 MB),或者对停顿时间没有特殊的要求,可以选择 Serial 收集器。
  2. 如果考虑应用程序的峰值性能,并且没有时间要求,可以接受 1 秒或更长的停顿时间,可以选择 Parallel 收集器。
  3. 如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内,可以选择 CMS / G1 收集器。
  4. 如果响应时间是高优先级的,或者堆空间比较大,可以选择 ZGC 收集器。

37、用过哪些性能监控的命令行工具?

  1. 操作系统层面,我用过 top、vmstat、iostat、netstat 等命令,可以监控系统整体的资源使用情况,比如说内存、CPU、IO 使用情况、网络使用情况。
  2. JDK 自带的命令行工具层面,我用过 jps、jstat、jinfo、jmap、jhat、jstack、jcmd 等,可以查看 JVM 运行时信息、内存使用情况、堆栈信息等。

37.1 你一般都怎么用jmap?

  1. 我一般会使用 jmap -heap 查看堆内存摘要,包括新生代、老年代、元空间等。
  2. 或者使用 jmap -histo 查看对象分布。

38、了解哪些可视化的性能监控工具?

  1. JConsole:JDK 自带的监控工具,可以用来监视 Java 应用程序的运行状态,包括内存使用、线程状态、类加载、GC 等。
  2. VisualVM:一个基于 NetBeans 的可视化工具,是 Oracle 官方主推的故障处理工具。集成了多个 JDK 命令行工具的功能,非常友好。
  3. Java Mission Control:JMC 最初是 JRockit VM 中的诊断工具,但在 Oracle JDK7 Update 40 以后,就绑定到了 HotSpot VM 中。不过后来又被 Oracle 开源出来作为了一个单独的产品。

38.1 用过哪些第三方的工具?

  1. MAT:一个 Java 堆内存分析工具,主要用于分析和查找 Java 堆中的内存泄漏和内存消耗问题;可以从 Java 堆转储文件中分析内存使用情况,并提供丰富的报告,如内存泄漏疑点、最大对象和 GC 根信息;支持通过图形界面查询对象,以及检查对象间的引用关系。
  2. GChisto:GC 日志分析工具,可以帮助我们优化垃圾收集行为和调整 GC 性能。
  3. JProfiler:一个全功能的商业化 Java 性能分析工具,提供 CPU、 内存和线程的实时分析。
  4. arthas:阿里巴巴开源的 Java 诊断工具,主要用于线上的应用诊断;支持在不停机的情况下进行诊断;可以提供包括 JVM 信息查看、监控、Trace 命令、反编译等功能。
  5. async-profiler:一个低开销的性能分析工具,支持生成火焰图,适用于复杂性能问题的分析。

39、JVM 的常见参数配置知道哪些?

39.1 配置堆内存大小的参数有哪些?

  1. -Xms:初始堆大小。
  2. -Xmx:最大堆大小。
  3. -XX:NewSize=n:年轻代大小。
  4. -XX:NewRatio=n:年轻代和年老代的比值。
  5. -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。

39.2 配置 GC 收集器的参数有哪些?

  1. -XX:+UseSerialGC:设置串行收集器。
  2. -XX:+UseParallelGC:设置并行收集器。
  3. -XX:+UseParalledlOldGC:设置并行老年代收集器。
  4. -XX:+UseConcMarkSweepGC:设置并发收集器。

39.3 配置并行收集的参数有哪些?

  1. -XX:MaxGCPauseMillis=n:设置最大垃圾回收停顿时间。
  2. -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的比例。
  3. -XX:+CMSIncrementalMode:设置增量模式,适合单 CPU 环境。
  4. -XX:ParallelGCThreads=n:设置并行收集器的线程数。

39.4 打印 GC 回收的过程日志信息的参数有哪些?

  1. -XX:+PrintGC:输出 GC 日志
  2. -XX:+PrintGCDetails:输出 GC 详细日志
  3. -XX:+PrintGCTimeStamps:输出 GC 的时间戳(以基准时间的形式)。
  4. -Xloggc:filename:日志文件的输出路径。

40、做过 JVM 调优吗?

JVM 调优是一个复杂的过程,调优的对象包括堆内存、垃圾收集器和 JVM 运行时参数等。
在这里插入图片描述
如果堆内存设置过小,可能会导致频繁的垃圾回收。项目中,我在启动 JVM 的时候配置了 -Xms 和 -Xmx 参数,让堆内存最大可用内存为 2G(我用的丐版服务器)。在项目运行期间,我会使用 JVisualVM 定期观察和分析 GC 日志,如果发现频繁的 Full GC,我会特意关注一下老年代的使用情况。接着,通过分析 Heap dump 寻找内存泄漏的源头,看看是否有未关闭的资源,长生命周期的大对象等。之后进行代码优化,比如说减少大对象的创建、优化数据结构的使用方式、减少不必要的对象持有等。

41、CPU 占用过高怎么排查?

在这里插入图片描述

42、内存飙高问题怎么排查?

  1. 先观察垃圾回收的情况,可以通过 jstat -gc PID 1000 查看 GC 次数和时间。
  2. 通过 jmap 命令 dump 出堆内存信息。
  3. 使用可视化工具分析 dump 文件,比如说 VisualVM,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。

43、频繁 Minor GC 怎么办?

频繁的 Minor GC 通常意味着新生代中的对象频繁地被垃圾回收,可能是因为新生代空间设置的过小,或者是因为程序中存在大量的短生命周期对象(如临时变量)。

可以使用 GC 日志进行分析,查看 GC 的频率和耗时,找到频繁 GC 的原因。或者使用监控工具查看堆内存的使用情况,特别是新生代的使用情况。如果是因为新生代空间不足,可以通过 -Xmn 增加新生代的大小,减缓新生代的填满速度。

44、频繁 Full GC 怎么办?

频繁的 Full GC 通常意味着老年代中的对象频繁地被垃圾回收,可能是因为老年代空间设置的过小,或者是因为程序中存在大量的长生命周期对象。

我一般会使用 JDK 的自带工具,包括 jmap、jstat 等。或者使用一些可视化的工具,比如 VisualVM、JConsole 等,查看堆内存的使用情况。

45、了解类的加载机制吗?

了解,JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,对数据进行校验、解析和初始化,最终转化成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。

其中最重要的三个概念就是:类加载器、类加载过程和双亲委派模型。

  1. 类加载器:负责加载类文件,将类文件加载到内存中,生成 Class 对象。
  2. 类加载过程:包括加载、验证、准备、解析和初始化等步骤。
  3. 双亲委派模型:当一个类加载器接收到类加载请求时,它会把请求委派给父类加载器去完成,依次递归,直到最顶层的类加载器,如果父类加载器无法完成加载请求,子类加载器才会尝试自己去加载。

46、类加载器有哪些?

主要有四种:

  1. 启动类加载器,负责加载 JVM 的核心类库,如 rt.jar 和其他核心库位于 JAVA_HOME/jre/lib 目录下的类。
  2. 扩展类加载器,负责加载 JAVA_HOME/jre/lib/ext 目录下,或者由系统属性 java.ext.dirs 指定位置的类库。
  3. 应用程序类加载器,负责加载 classpath 的类库,由sun.misc.Launcher$AppClassLoader实现。我们编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器。
  4. 用户自定义类加载器,通常用于加载网络上的类、执行热部署,或者为了安全考虑,从不同的源加载类。通过继承java.lang.ClassLoader类来实现。

47、能说一下类的生命周期吗?

一个类从加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 、验证、准备、解析、初始化、使用和卸载。
在这里插入图片描述

48、类装载的过程知道吗?

类装载过程包括三个阶段:载入、链接和初始化。

  1. 加载:将类的二进制字节码加载到内存中。
  2. 连接可以细分为三个小的阶段:
  • 验证:检查类文件格式是否符合 JVM 规范
  • 准备:为类的静态变量分配内存并设置默认值。
  • 解析:将符号引用替换为直接引用。
  1. 初始化:执行静态代码块和静态变量初始化。

在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。比如说 static int a = 1;,在准备阶段,a 的值为 0,在初始化阶段,a 的值为 1。换句话说,初始化阶段是在执行类的构造方法。

48.1 加载过程 JVM 会做什么?

在这里插入图片描述

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的访问入口。

49、什么是双亲委派模型?

双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。
在这里插入图片描述
这个过程会一直向上递归,也就是说,从子加载器到父加载器,再到更上层的加载器,一直到最顶层的启动类加载器。启动类加载器会尝试加载这个类。如果它能够加载这个类,就直接返回;如果它不能加载这个类,就会将加载任务返回给委托它的子加载器。子加载器尝试加载这个类。如果子加载器也无法加载这个类,它就会继续向下传递这个加载任务,依此类推。直到某个加载器能够加载这个类,或者所有加载器都无法加载这个类,最终抛出 ClassNotFoundException。

50、为什么要用双亲委派模型?

  1. 避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
  2. 保证核心类库的安全性:如 java.lang.* 只能由 Bootstrap ClassLoader 加载,防止被篡改。

51、如何破坏双亲委派机制?

重写 ClassLoader 的 loadClass() 方法。如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法,那些无法被父类加载器加载的类最终会通过这个方法被加载。

52、有哪些破坏双亲委派模型的典型例子?

我了解的有两种:

  1. SPI 机制:SPI 是 Java 的一种扩展机制,用于加载和注册第三方类库,常见于 JDBC、JNDI 等框架。双亲委派模型会优先让父类加载器加载类,而 SPI 需要动态加载子类加载器中的实现。根据双亲委派模型,java.sql.Driver 类应该由父加载器加载,但父类加载器无法加载由子类加载器定义的驱动类,如 MySQL 的 com.mysql.cj.jdbc.Driver。那么只能使用 SPI 机制通过 META-INF/services 文件指定服务提供者的实现类
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Enumeration<Driver> drivers = ServiceLoader.load(Driver.class, cl).iterator();

DriverManager 使用了线程上下文类加载器来加载 SPI 的实现类,从而允许子类加载器加载具体的 JDBC 驱动。
2. 热部署:热部署是指在不重启服务器的情况下更新应用程序代码,需要替换旧版本的类,但旧版本的类可能由父加载器加载。如 Spring Boot 的 DevTools 通常会自定义类加载器,优先加载新的类版本。

53、Tomcat 的类加载机制了解吗?

Tomcat 基于双亲委派模型进行了一些扩展,主要的类加载器有:

  1. Bootstrap ClassLoader:加载 Java 的核心类库;
  2. Catalina ClassLoader:加载 Tomcat 的核心类库;
  3. Shared ClassLoader:加载共享类库,允许多个 Web 应用共享某些类库;
  4. WebApp ClassLoader:加载 Web 应用程序的类库,支持多应用隔离和优先加载应用自定义的类库(破坏了双亲委派模型)。
    在这里插入图片描述

54、说说解释执行和编译执行的区别?

先说解释和编译的区别:

  1. 解释:将源代码逐行转换为机器码。

  2. 编译:将源代码一次性转换为机器码。
    一个是逐行,一个是一次性,再来说说解释执行和编译执行的区别:

  3. 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。

  4. 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。
    Java 一般被称为“解释型语言”,因为 Java 代码在执行前,需要先将源代码编译成字节码,然后在运行时,再由 JVM 的解释器“逐行”将字节码转换为机器码,然后执行。

这也是 Java 被诟病“慢”的主要原因。

但 JIT 的出现打破了这种刻板印象,JVM 会将热点代码(即运行频率高的代码)编译后放入 CodeCache,当下次执行再遇到这段代码时,会从 CodeCache 中直接读取机器码,然后执行。

因此,Java 的执行效率得到了大幅提升。
在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词