关于JVM面试题汇总

JVM是如何运行的?

JVM的执行流程如下:

  1. 程序再执行之前先要把Java代码转换成字节码(class文件),JVM首先需要把字节码通过一定的方式类加载器 (ClassLoader)把文件加载到内存中运行时数据区(Runtime Data Area)
  2. 但是字节码文件是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器,也就是JVM的执行引擎会将字节码翻译成底层系统指令再交由CPU去执行
  3. 在执行的过程中,也需要调用其他语言接口,如**通过调用本地库接口(Native Interface)**来实现整个程序的运行

所以整体来看,JVM主要是通过以下四个部分来执行Java程序的:

  1. 类加载器
  2. 运行时数据区
  3. 执行引擎
  4. 本地库接口

Java是编译性语言还是解释性语言?

编译型语言和解释性语言区别如下:

  • 编译性语言:在程序执行之前,先经过编译器的处理,将源代码转换为目标及其可以执行的二进制机器码,然后直接执行。因此编译过程只需要进行一次,生成可执行文件可以重复运行
    • 优点:执行效率高
    • 缺点:编译时间长跨平台能力有限
  • 解释性语言:不需要事先编译成机器码,而是在运行时由解释器江源带哦吗逐行解释执行
    • 优点:跨平台性比较好,无需编译
    • 缺点:执行效率低,不宜保护源代码

而Java语言既不属于编译性语言也而不属于解释性语言,又不完全属于解释性语言。Java是半编译语言,也叫编译-解释型语言,其执行过程包括编译和解释两个阶段:

  1. 编译阶段:Java源代码(.Java文件)通过Java编译器编译成字节码文件(.class文件)。字节码是一种中间语言,它具有平台无关性,可以在任何支持Java虚拟机(JVM)平台上运行
  2. 解释阶段:达昂程序运行时,Java虚拟机会加载字节码,并且对其进行解释或即使编译执行。现代JVM普遍采用JIT技术,会根据代码热点将频繁执行的字节码动态编译成本地机器指令以提高性能

为什么需要将 java 代码编译成字节码(.class)?

Java需要编译成字节码(.class文件)的原因主要是有以下几点:

  1. 跨平台执行:将Java代码编译成字节码,可以使不同平台下的Java虚拟机(JVM)识别,从而根据平台特性,从而生成不同平台的二进制机器码进行执行,这样就可以实现跨平台执行
  2. 代码检查:编译器在编译阶段会对代码进行类型检查,确保代码的类型安全性,让我们提前发现一些潜在的错误,例如类型不匹配、缺少方法等问题
  3. 动态加载和扩展:将Java代码编译成字节码可以动态扩展一些功能,例如Lombok插件的@Getter和@Setter方法就是编译器进行字节码生成的
  4. 高效执行:字节码是一种中间表示形式,相比于原始的源代码,可以提供更高效的执行和优化。JVM会通过即使编译等技术将字节码转换为机器码,以提高程序的执行速度
  5. 代码保护:编译后的字节码是一种经过转换的相信那个是,与原始的源代码相比,更难以直接理解。这可以提供一定程度的代码保护,使得源代码的逻辑和实现细节难以被逆向工程或恶意更改

说一下JVM的内存布局?

通常所说的JVM内存布局,通常是指JVM运行时数据区,也就是当字节码被类加载器加载后的执行区域。JVM运行时数据区主要分为以下几个部分:

  1. 程序计数器:当存储当前线程执行的字节码指令地址,在多线程环境之中,程序计数器用于实现线程切换,保证线程恢复执行时能后继续从正确的位置执行代码
  2. Java虚拟机栈:用于存储方法调用和局部变量(方法内部定义的变量),在方法调用和返回时,虚拟机栈用于保存方法的调用帧,包括方法的局部变量、操作数栈、方法返回地址等
  3. 本地方法栈:与薰妮基栈类似,本地方法找用于执行本地方法
  4. Java堆:JVM中的最大一块内存存储区域,用于存储对象实例,所有对象都在堆中分配内存
  5. 方法区:用于存储类的元数据信息,包括类的结构、字段、方法、静态变量、常量池等

Java虚拟机规范和Java虚拟机有什么关系

它们是一个JVM规范,以一个是针对规范的实现产品,具体来说:

  • **Java 虚拟机规范(**JVM Specification)是 Sun Microsystems 公司(现为 Oracle 公司)制定的一套详细的文档,它定义了 Java 虚拟机的内部工作原理、结构、指令集、数据类型、内存区域、垃圾收集、类文件格式、加载和执行机制等具体规则,这些规则是 Java 平台实现兼容性和可移植性的基础
  • Java 虚拟机(Java Virtual Machine,JVM)则是根据上述规范实现的具体软件系统,它是一个实际运行在物理硬件上的程序,负责装载并执行 Java 字节码。任何符合 Java 虚拟机规范的 JM 都可以正确解释和执行标准的 Java 字节码,从而确保 Java 代码的"一次编写,到处运行"的特性

因此,Java 虚拟机规范与 Java 虚拟机的关系可以理解为规范与实现的关系。规范描述了所有 Java 虚拟机应遵循的标准和约定,而各种不同的 Java 虚拟机则是按照该规范进行设计和实现的具体产品。例如,HotSpot JVM 就是由 Oracle 开发的一款广泛使用的 Java 虚拟机的默认实例

Java中的引用类型有哪些?这些引用类型对应的使用场景有哪些?为什么要有这么多的引用类型?

在Java中,引用是指向对象在内存中存储位置的指针,引用类型主要是分为四种:强引用、软引用、弱引用、虚引用

  1. 强引用:强引用指的是在程序代码之中普遍存在的,类似Object obj = new Object()这类引用,只要强引用还存在,垃圾回收器就不会回收掉被引用的对象实例
    1. 使用场景:日常使用的new XXX()创建的对象都是强引用1
  2. 软引用:软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联的对象,在系统中将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收 。人如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用
    1. 使用场景:软引用通常是用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足的时候清理掉,这样就保证了使用缓存的同时,不会耗尽内存
  3. 弱引用:弱引用也是用来描述非必须的对象的,它的强度要弱于软引用。被弱引用关联的对象之恶能生存到下一次垃圾回收之前。当垃圾回收器开始工作的时候,无论当前内存是否够用,都会回收掉弱引用关联的对象 。在JDK 1.2之后提供了WeakReference类来实现弱引用
    1. 使用场景:维护一种非强制性的映射关系,如果试图获取对象还存在,就使用它,否则就重新实例化。例如ThreadLocal中的ThreaddLocal中的ThreadLocalMap使用的就是弱引用
  4. 虚引用:虚引用也被称为幽灵引用或者幻影引用,不能通过它访问对象。幻想引用仅仅是提供一种确保对象被finalize以后,做某些事情的机制
    1. 使用场景:监控对象的创建和销毁

为什么要有这么多引用类型

这些引用类型为Java程序提供一种更加精细的内存管理手段,使得开发者可以根据应用程序的具体需求来调整对象的生命周期,特别是在处理大量数据缓存、资源管理和防止内存泄露等场景尤其重要

Java中的垃圾回收算法有哪些?它们各自有哪些优缺点?

垃圾回收是指自动管理程序中不再需要的内存的过程。在Java中,垃圾回收负责识别和释放不再适用对象所占用的内存空间,以便将其重新分配给新的对象使用。Java中常见的垃圾回收算法重要是有以下几种:

  1. 标记-清除算法 :标记-清除算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,如下图:

    • 优点:实现简单且执行效率相当高效
    • 缺点:会产生内存碎片问题,在标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大的对象时,无法找到足够连续内存而不得不提前出发另一次垃圾收集
  2. 标记-整理算法 :标记-整理算法也是分为两个阶段标记和整理,其中标记与上述相同,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉

    • 优点:无内存碎片化问题
    • 缺点:执行效率比较低
  3. 复制算法 :复制算法是为了解决标记-整理的效率问题,它将可用内存按照容量分为大小相等的两块,每次只是用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次性清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片化问题等复杂情况,只需要移动堆顶指针,按顺序分配即可。简单高效,运行高效

    • 优点:执行效率高
    • 缺点:空间利用率低
  4. 分代算法 :分代算法是通过区域划分的,实现不同区域和不同的垃圾回收策略,从而更好的垃圾回收

    • 优点:分而治之,不同场景实现不同的算法,整体的性能更高且空间效率高
    • 缺点:实现复杂度比较高

JVM中的常见垃圾回收器有哪些?

  1. Serial/Serial Old

    • 使用的是标记-整理(Mark-Compact)算法。
    • 在新生代中,使用标记-复制(Mark-Copy)算法。
    • 这些算法是基于停止-复制或者停止-整理的方法,在垃圾回收过程中会暂停所有应用程序线程。
  2. ParNew

    • ParNew是Serial的多线程版本,因此在新生代中也使用标记-复制(Mark-Copy)算法。
    • ParNew垃圾回收器可以与CMS垃圾回收器一起工作,在CMS旧代回收时使用。
  3. Parallel Scavenge/Parallel Old

    • Parallel Scavenge在新生代中使用标记-复制(Mark-Copy)算法,目标是达到一个可控的吞吐量。
    • Parallel Old在老年代中使用的是标记-整理(Mark-Compact)算法。
  4. CMS (Concurrent Mark-Sweep)

    • 使用的是并发标记和并发清除算法。
    • CMS垃圾回收器在尽量减少停顿时间的前提下进行垃圾回收,通过与应用程序并发执行标记和清除操作来实现。
  5. G1 (Garbage-First)

    • 使用的是分代垃圾回收算法,结合了标记-整理(Mark-Compact)和标记-清除(Mark-Sweep)的特性。
    • G1将堆内存划分为多个区域,并使用标记-整理和标记-清除操作来进行垃圾回收,以尽量减少停顿时间。

新生代指的是新创建的对象内存区域/老年代适用于存放长时间存活对象的内存区域

CMS垃圾回收器有什么优缺点?它使用了什么算法?

CMS(并发标记清理)回收器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,已给用户带来更好的体验,CMS收集器就非常符合这类应用的需求

优点:低延迟、并发收集效率高

缺点

产生内存碎片:因为CMS使用的是标记-清除算法,所以会产生内存碎片

CPU资源敏感:因为垃圾回收时虽然不会STW(停止),但是占用用户线程的CPPU资源,如果用户线程本身的CPU资源已经很吃紧了,那么此时在使用CMS无疑是雪山加霜

CMS如何解决内存碎片问题?它能使用标记-整理算法吗?为什么?

CMS并不能直接解决内存碎片的问题,因为CMS时使用的是标记-清除算法。但是当内存碎片比较多的时候(连续内存不足以申请大对象时),CMS会借助Serial Old垃圾回收执行内存碎片的回收工作,因Serial Old使用的是标记-整理算法,所以内存碎片问题就会得到解决

CMS可以使用标记-整理算法呢

不能,这是因为CMS最后一个阶段是并发清楚阶段,此阶段CMS垃圾回收回和用户线程并发执行,如果使用并发-整理算法需要移动内存位置,而此时用户线程只在执行,所以不能使用并发-整理算法

说一下CMS执行流程?它需要全局停顿(STW)几次?

CMS执行流程总共分为以下四个阶段:

  1. 初始标记(STW):GC Roots能直接关联的对象,执行速度很快
  2. 并发标记(和用户线程并发执行):GC Roots直接关联的对象继续往下(一直)遍历和标记,耗时会比较长
  3. 重新标记(STW):对上一步的并发标记阶段,因为用户线程执行而导致变动的对象进行修正标记
  4. 并发清除(和用户线程并发执行):使用并发-清除算法将垃圾对象进行删除

由此可以看出CMS执行的时候,一共有两次停顿

Old GC和Full GC有什么区别?JVM中的垃圾回收类型还有哪些?

Old GC是老年代垃圾回收,而Full GC是全局垃圾回收,区别如下:

Old GC(老年代垃圾回收):Old GC又被称为Major GC。它主要是针对老年代内存区域进行垃圾回收,当老年代内存空间不足,或从新生代晋升的对象过多导致老年代无法容纳的时候,会发生Old GC。Old GC的发生频率是低于新生代GC,但是其执行时间通常是比新生代GC长的多

Full GC(全局垃圾回收会泽和完全垃圾回收):Full GC是对整个堆内存(年轻代和老年代)、方法去进行全面的垃圾回收。发生Full GC的情况是较为复杂的,例如:老年代空间不足、元空间不足、System.gc(显式调用(不推荐)、CMS 并发标记失败后 fallback 到 Serial 0ld 收集器等。Full GC 的开销非常大,因为它涉及到了IVM 几乎所有的内存区域,因此应尽量避免不必要的 Ful GC,以减少系统停顿时间

所以说:Full GC=Minor GC+Major Gc+方法区 GC

JVM有哪些优化手段?说说JIT和逃逸分析?

JVM优化手段主要分为以下几种:

  1. JIT(即时编译):是一种在程序运行时将部分热点代码(频发使用的代码)编译成机器代码的技术,以提高程序的执行性能的机制
  2. 逃逸分析 :用于确定对象动态作用域是否超过当前方法或线程,通过逃逸分析,编译器可以决定一个对象的作用范围,从而进行相应的优化,但是确定对象没有逃逸时,可以进行以下优化:
    1. 栈上分配:如果编译器可以确定一个对象不会逃逸出方法,它可以将对象分配到栈上而不是堆上。在栈上的分配对象在方法返回后会自动销毁,不会进行垃圾回收,提高了程序执行效率
    2. 锁消除:如果对象只在单线程中使用,那么同步锁可能会被消除,提高程序性能
    3. 标量替换:将原本需要分配在堆上的对象拆分成若干基础数据类型存储在栈上,进一步减少堆空间的使用
  3. 字符串池优化:JVM通过共享字符串常量,重用字符串对象,以减少内存占用和提升字符串操作性能

G1是如何分区的?

G1总共分为以下四个区域:

  1. Eden(伊甸园区):新创建的对象都会存储在此区域
  2. Survivor(存活区):eden经过GC(垃圾回收)之后存活的对象就会移动到此区域
  3. Old(老年代):经过n次GC之后还存活的对象
  4. Humongous(巨型区):用来存放大对象的,大对象会直接存放到此区域,当一个对象的大小超过 Region(内存) 的一半时(50%),则该对象定义为大对象

G1和CMS有什么区别?

G1和CMS垃圾回收器都是HotSpot虚拟机最终的最常用的垃圾回收器。区别如下:

  1. 回收算法
    1. G1 :使用的是分代垃圾回收算法,结合了标记-整理(Mark-Compact)和标记-清除(Mark-Sweep)的特性。G1将堆内存划分为多个区域,并使用标记-整理和标记-清除操作来进行垃圾回收,以尽量减少停顿时间
    2. CMS :使用的是标记-清除算法。CMS垃圾回收器在尽量减少停顿时间的前提下进行垃圾回收,通过与应用程序并发执行标记和清除操作来实现
  2. 停顿时间
    1. G1:G1回收器的设计目标之一是尽量减少垃圾回收造成的停顿时间,它通过在整个堆内存中采用并发标记、并发整理和并发清理等技术来实现。由于G1使用了区域化的垃圾回收策略,因此可以更好地控制垃圾回收的停顿时间
    2. CMS:CMS也致力于减少垃圾回收造成的停顿时间,但由于其并发清除阶段并不是完全并发的,可能会导致一些额外的停顿时间
  3. 内存占用
    1. G1:G1的设计目标之一是减少内存碎片,通过区域化的垃圾回收策略和压缩空间的操作来提高内存的利用率,从而减少堆内存的占用。
    2. CMS:CMS的设计目标之一是最小化停顿时间,因此在内存利用率方面可能没有G1优秀。此外,由于CMS回收器在并发清除阶段可能会产生大量的空间碎片,可能会影响到堆内存的分配和使用

方法区、永久代和元空间有什么区别?

方法区、永久区和元空间是Java虚拟机用于存储类信息的区域,它们在不同的Java虚拟机版本有所不同:

  1. 方法区:方法去是一块用于存储类的结构信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域。在早期的Java虚拟机版本中,方法区被永久实现为永久代。但是在Java 8及其以后版本中,方法去被移除,取而代之的是元空间
  2. 永久代:永久代是Java虚拟机中的一个特定的方法区,用于存储类的元数据新消息,如类名、方法名、字段名等。永久代的大小是固定的,并且在默认的情况下比较小。由于永久代的大小是有限的,当加载的类过多或者动态生成的类过多的时候,就容易出现永久代溢出的情况
  3. 元空间:元空间是Java 8及其以后版本中取代永久代的新方法区实现。元空间并不在虚拟机内存中,而是使用本地内存来存储类的元数据信息。元空间的大小可以根据需要进行调整,并且默认的情况下是不限制大小的。元空间的好处是可以避免永久代的溢出问题,并且可以更好的利用系统资源

所以说,方法区是一个规范层面的概念,而永久代是早期 HotSpot JVM(JVM的实现) 对方法区的具体实现方式,现已废弃;而元空间则是后来 HotSpot 为了改进内存管理,和解决永久代带来的内存溢出问题所采用的新的实现机制

为什么要使用元空间替代永久代?元空间有什么优点?

  • 降低内存溢出:当使用永久代实现方法区时,永久代的最大容量受制于 PermSize 和 MaxPermSize 参数设置的大小,而这两个参数的大小又很难确定,因为在程序运行时需要加载多少类是很难估算的,如果这两个参数设置的过小就会频繁的触发FullGC 和导致 OOM(Out of Memory,内存溢出)。但是,当使用元空间替代了永久代之后,出现 00M 的几率就被大大降低了,因为元空间使用的是本地内存,这样元空间的大小就只和本地内存的大小有关了,从而大大降低了OOM 的问题
  • 降低运维成本:因为元空间使用的是本地内存,这样就无需运维人员再去专门设置和调整元空间的大小了

常量池和字符串常量池有什么区别?字符串常量池是如何实现的?

常量池和自负床常量池都是Java中用于优化性能和内存使用的机制,但是它们的应用场景和存储内容有所不同:

常量池:是指存储在.class文件中的一组常量的集合,包括字符串常量、数字常量、类和接口的名称以及其他一些常量。常量池是Java中的一种优化机制,用于避免重复存储相同的常量值,从而减小类文件的大小

字符串常量池:也是常量池中的一种,专门用于存储字符串常量的

二者区别如下:

  1. JDK 1.8之后,字符串常量池存储在堆上,而常量池在元空间本地内存中
  2. 常量池包含很多的内容,如类、方法、字段你等常量都是存储在常量池中,而字符串常量池只是用于存储字符串常量对象

字符串常量池的实现

自负床常量池是由C++中的HashMap实现的,它的key是字符串的字面量,value是字符串对象的引用

什么叫做堆溢出?实际工作中哪些情况会导致堆溢出?

堆溢出通常是指堆内存中的对象过多,无法被垃圾回收导致的内存溢出出错。以下是常见的导致堆溢出的场景和原因:

  1. 内存泄露:最常见的情况就是内存泄漏,即对象被创建后不再被使用,但是没有被释放。这会导致之堆中的对象数量逐渐增加,直到堆溢出。例如ThreadLocal使用不当,使用完成之后未调用remove方法导致内存泄漏,以及忘记释放各种连接,也会导致内存泄漏,如数据库连接、网络连接和IO连接等
  2. 无限递归创建大量对象:无限递归调用一个方法可能会导致栈溢出,但是如果当调用中创建了大量对象并持续递归,也可能导致堆溢出
  3. 创建大量的大对象:创建大量大对象,尤其是数组或集合,可能导致堆溢出。如果没有足够的连续内存来存储大对象,堆溢出会发生
  4. 未合理设置堆大小:如果未合理设置 Java 虚拟机的堆大小参数(如 -Xmx 和 -Xms),可能导致堆溢出
  5. Excel 导入和导出:如果有大的 excel 要进行导入和导出的情况下,因为其操作都是在内存中拼接和组织数据的,如果 excel过大,很容易就会造成堆溢出

什么叫做栈溢出?导致栈溢出的原因是啥?

栈溢出是指在程序运行的时候,当栈空间中可用内存大小被超出所能容纳的大小的时候,导致发成异常或错误。以下是常见的导致栈溢出的场景和原因:

  1. 递归调用:在递归算法中,如果递归的深度过大,每次递归都会在栈上生成一个函数调用的帧,当栈空间内不足以容纳这些帧的时候,就会发生栈溢出
  2. 无限递归:在某些情况下,由于代码逻辑错误或循环调用,可能会导致无限递归的情况发生,从而导致栈溢出
  3. 大规模数据结构使用:当使用大规模的数据结构(如大数组、大集合等)时,如果栈空间不足以容纳这些数据,就会导致栈溢出。解决方法时尽量使用堆空间存储的答案规模数据结构,或者增加栈的大小
  4. 深度嵌套函数调用:当函数调用过于深度嵌套的时候,每次调用都会在栈中生成一个新的函数帧,如果嵌套层过多,就会导致栈溢出

说一下类加载机制?Loading和Class Loading有什么区别?

类加载机制是Java虚拟机在运行Java程序的时候用来加载文件的过程。类加载机制包括以下几个步骤:

  1. 加载:将类的字节码文件加载到内存中,并创建一个对应的Class对象,用这个来表示类。加载阶段是通过类加载器来实现的
  2. 验证:确保加载的类文件符合Java虚拟机的规范不会造成安全问题
  3. 准备:为类的静态变量分配内存,并初始化为默认值
  4. 解析:解析阶段是JVM将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。这个过程会涉及到三个概念:
    1. 符号引用:类文件中的一个抽象引用方式,它并不涉及具体的内存地址或者对象实例。符号引用包括三方面的信息:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。这些信息足以唯一的确定一个类、字段或者方法,但是类被加载到JVM之前,并没有实际的内存布局关联
    2. 直接引用:一种可以直接指向目标对象、类、字或者方法在JVM内存中的物理位置的引用方式。例如:指针、偏移量等。一但是有了直接引用,就可以直接访问目标实体,而无需经过其他的查找过程
    3. 替换过程:当JVM解析阶段需要对某个符号进行解析的时候,会根据类加载的结果生成对应的直接引用。比如,当一个类引用了另一个类的方法或者字段的时候,解析阶段会确保被引用的目标类已经被加载,并会计算出被饮用方法或字段在内存中的准确位置,然后用这个位置信息替换掉原来的符号引用
  5. 初始化:执行类初始化代码,包括静态变量的复制和静态代码块的执行。初始化阶段是类加载的最后一步

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

双亲委派模型是Java类加载机制的一种设计模式,用于保证类加载的安全性和一致性。在双亲为欸配模型中,类加载器在尝试加载一个类时,会先将加载的请求派给其父类加载器,如果父类加载器无法加载该类(即父类加载器的搜索范围内找不到对应的类),则子类加载器才会尝试加载

自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器,如下图所示:

  • 启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即 $JAVA_HOME/lib 目录。
  • 扩展类加载器:加载 lib/ext 目录下的类
  • 应用程序类加载器:加载我们写的应用程序
  • 自定义类加载器:根据自己的需求定制类加载器

双亲委派模型的主要目的就是确保Java类的安全性和一致性,具体的表现在以下几个方面:

  1. 防止重复加载:比如A类和B类都有一个父类C类,那么当A启动的时候就会将C类加载起来,那么在B类加载的时候就不需要进行重复加载C类
  2. 更安全:使用双亲委派模型也可以保证Java的核心API不会被串改,如果没有使用双亲委派模型,而是每个类加载自己的话就会出现一点问题,比如我们编写一个称为java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的Object类,而有些Object类又是用户自己提供的因此安全性就不能得到保证

有哪些打破双亲委派模型的场景?为什么要打破双亲委派模型?

打破双亲委派模型的主要场景主要是以下两种:

  1. Java中自带的SPI机制
  2. Tomcat

SPI机制:SPI是JDK内置的一种服务提供发现机制。例如,数据库驱动就是SPI的典型实现。在Java中,数据库驱动就是一个典型的SPI使用场景。不同的数据库厂商都提供了自己的数据库驱动实现,这些实现都是实现了同一个JDBC接口。JVM在运行时可以动态加载适合的数据库驱动,使得开发者可以在不修改代码的情况下切换不同的数据库

双亲委派模型是从下往上加载,而 SPI机制是从上往下加载

Tomcat:Tomcat 也要打破了双亲委派模型,因为一个外置 Tomcat 中要部署多个应用,多个 Web 应用程序在同一个omcat 实例中独立运行,而不会相互干扰或导致类冲突,所以 Tomcat 需要打破双亲委派模型来实现类隔离、热部署和解决类库版本冲突等问题

  1. 类隔离:应用服务器通常需要在同一JVM 中运行多个不同的 Web 应用程序,每个应用程序都可能依赖于不同版本的类库。为了保持这些应用程序的隔离性,Tomcat 需要使用自定义的类加载器来加载各个 Web 应用程序的类。这样可以确保每个 Web 应用程序都不会干扰其他应用程序的类加载
  2. 热部署和热加载:Tomcat 支持热部署和热加载,允许在应用程序运行时替换类文件而不需要重新启动整个应用服务器。为了实现这一功能,Tomcat 需要自己的类加载器,以便能够动态加载新的类定义
  3. 类库版本冲突:有时 Web 应用程序需要使用自己的类库版本,而不是应用服务器提供的全局类库版本。这可能会导致类库版本冲突,为了解决这个问题,Tomcat 可以使用自定义的类加载器来加载应用程序的类,而不受全局类库的影响

判断死亡对象的算法有哪些?

判断对象是否存活的常见算法主要有以下两种:

  1. 引用计数算法
  2. 可达性分析算法

引用计数算法

引用计数器的实现思路是,给对象增加一个引用计数器,没达昂有一个地方引用它时,计数器就+1;当引用失效的时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已死

优点:实现简单,判定效率比较高

缺点:引用计数无法解决对象循环引用的问题

可达性分析算法

可达性分析算法是通过一系列为GC Roots的对象作为起点,从这些接电脑开始向下搜索搜索走过的路径称为引用链,达昂一个对象到GC Roots没有任何的引用链相连时(从GC Roots 到这个对象不可达)时,证明此对象是不可用的,也就是死亡对象

对象 Object5-0bject7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象

目前主流的Java虚拟机都是使用可达性分析算法来判断死亡对象的

什么对象可以作为GC Roots?为什么它们能作为GC Roots?

在Java中,可作为GC Roots的对象有以下几种:

  1. Java虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 本地方法栈中(Native方法)引用的对象
  3. 方法区中的类静态属性引用的对象
  4. 方法区中常量引用的对象

其中,Java 虚拟机栈和本地方法栈中的引用对象,是目前线程正在执行时用的对象,所以它们不能被回收,因此它们可以作为 GC Roots。而方法区中的静态属性和常量对象与类本身相关联,而类已经被加载到程序中了,所以类属于系统的一部分了,因此类所关联的静态属性和常量也就是系统的一部分了,所以它们可以作为 GC Roots

相关推荐
秃头佛爷42 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
阿伟*rui43 分钟前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
待磨的钝刨43 分钟前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei3 小时前
java的类加载机制的学习
java·学习
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端