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

Java虚拟机面试题:类加载机制

🧑 博主简介:CSDN博客专家历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea

在这里插入图片描述


在这里插入图片描述

Java虚拟机面试题:类加载机制

1. 了解类的加载机制吗?

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

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

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

2. 类加载器有哪些?

类加载器(ClassLoader)用于动态加载 Java 类到 Java 虚拟机中。主要有四种类加载器:

①、启动类加载器(Bootstrap ClassLoader)负责加载 JVM 的核心类库,如 rt.jar 和其他核心库位于JAVA_HOME/jre/lib目录下的类。

②、扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader(或其它类似实现)实现。负责加载JAVA_HOME/jre/lib/ext目录下,或者由系统属性java.ext.dirs指定位置的类库。

③、应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader(或其它类似实现)实现。

负责加载系统类路径(classpath)上的类库,通常是我们在开发 Java 应用程序时的主要类加载器。

我们编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器。

④、用户自定义类加载器 (User-Defined ClassLoader),我们可以通过继承java.lang.ClassLoader类来创建自己的类加载器。

这种类加载器通常用于加载网络上的类、执行热部署(动态加载和替换应用程序的组件)或为了安全目的自定义类的加载方式。

3. 能说一下类的生命周期吗?

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)。

在这里插入图片描述

4. 类装载的过程知道吗?

类装载过程包括三个阶段:载入、链接(包括验证、准备、解析)、初始化。

①、载入:将类的二进制字节码加载到内存中。

②、链接可以细分为三个小的阶段:

  • 验证:检查类文件格式是否符合 JVM 规范
  • 准备:为类的静态变量分配内存并设置默认值。
  • 解析:将符号引用替换为直接引用。

③、初始化:执行静态代码块和静态变量初始化。

在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。

换句话说,初始化阶段是执行类的构造方法(javap中看到的 <clinit>() 方法)的过程。

载入过程JVM 会做什么?

三分恶面渣逆袭:载入

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

5. 什么是双亲委派模型?

双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。

在这里插入图片描述

  • 当一个类加载器需要加载某个类时,它首先会请求其父类加载器加载这个类。
  • 这个过程会一直向上递归,也就是说,从子加载器到父加载器,再到更上层的加载器,一直到最顶层的启动类加载器(Bootstrap ClassLoader)。
  • 启动类加载器会尝试加载这个类。如果它能够加载这个类,就直接返回;如果它不能加载这个类(因为这个类不在它的搜索范围内),就会将加载任务返回给委托它的子加载器。
  • 子加载器接着尝试加载这个类。如果子加载器也无法加载这个类,它就会继续向下传递这个加载任务,依此类推。
  • 这个过程会继续,直到某个加载器能够加载这个类,或者所有加载器都无法加载这个类,最终抛出 ClassNotFoundException。

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

①、避免类的重复加载:父加载器加载的类,子加载器无需重复加载。

②、保证核心类库的安全性:如 java.lang.* 只能由 Bootstrap ClassLoader 加载,防止被篡改。

7. 如何破坏双亲委派机制?

如果不想打破双亲委派模型,就重写 ClassLoader 类中的 fifindClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则需要重写 loadClass()方法。

8. 历史上有哪几次双亲委派机制的破坏?

双亲委派机制在历史上主要有三次破坏:

在这里插入图片描述

说说第一次破坏

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即 JDK 1.2 面世以前的“远古”时代。

由于双亲委派模型在 JDK 1.2 之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader 则在 Java 的第一个版本中就已经存在,为了向下兼容旧代码,所以无法以技术手段避免 loadClass()被子类覆盖的可能性,只能在 JDK 1.2 之后的 java.lang.ClassLoader 中添加一个新的 protected 方法 findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。

说说第二次破坏

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,如果有基础类型又要调用回用户的代码,那该怎么办呢?

例如我们比较熟悉的 JDBC:

各个厂商各有不同的 JDBC 的实现,Java 在核心包\lib里定义了对应的 SPI,那么这个就毫无疑问由启动类加载器加载器加载。

但是各个厂商的实现,是没办法放在核心包里的,只能放在classpath里,只能被应用类加载器加载。那么,问题来了,启动类加载器它就加载不到厂商提供的 SPI 服务代码。

为了解决这个问题,引入了一个不太优雅的设计:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI 服务使用这个线程上下文类加载器去加载所需的 SPI 服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。

说说第三次破坏

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。

OSGi 实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。

9. Tomcat 的类加载机制了解吗?

Tomcat 是主流的 Java Web 服务器之一,为了实现一些特殊的功能需求,自定义了一些类加载器。

Tomcat 类加载器如下:

在这里插入图片描述

Tomcat 实际上也是破坏了双亲委派模型的。

Tomact 是 web 容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖 hollis.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。

所以,Tomcat 破坏了双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每一个 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交 CommonClassLoader 加载,这和双亲委派刚好相反。

10. 你觉得应该怎么实现一个热部署功能?

实现一个热部署(Hot Deployment)功能通常涉及到类的加载和卸载机制,使得在不重启应用程序的情况下,能够动态替换或更新应用程序的组件。

第一步,使用文件监控机制(如 Java NIO 的 WatchService)来监控类文件或配置文件的变更。当监控到文件变更时,触发热部署流程。

class FileWatcher {public static void watchDirectoryPath(Path path) {// 检查路径是否是文件夹try {Boolean isFolder = (Boolean) Files.getAttribute(path, "basic:isDirectory", LinkOption.NOFOLLOW_LINKS);if (!isFolder) {throw new IllegalArgumentException("Path: " + path + " is not a folder");}} catch (IOException ioe) {// 文件 I/O 错误ioe.printStackTrace();}System.out.println("Watching path: " + path);// 我们获得文件系统的WatchService对象FileSystem fs = path.getFileSystem();try (WatchService service = fs.newWatchService()) {// 注册路径到监听服务// 监听目录内文件的创建、修改、删除事件path.register(service, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);// 开始无限循环,等待事件发生WatchKey key = null;while (true) {key = service.take(); // 会阻塞直到有事件发生// 对于每个发生的事件for (WatchEvent<?> watchEvent : key.pollEvents()) {WatchEvent.Kind<?> kind = watchEvent.kind();// 获取文件路径@SuppressWarnings("unchecked")WatchEvent<Path> ev = (WatchEvent<Path>) watchEvent;Path fileName = ev.context();System.out.println(kind.name() + ": " + fileName);}// 重置watchKeyboolean valid = key.reset();// 退出循环如果watchKey无效if (!valid) {break;}}} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {// 监控当前目录Path pathToWatch = Paths.get(".");watchDirectoryPath(pathToWatch);}
}

第二步,创建一个自定义类加载器,继承自java.lang.ClassLoader,重写findClass()方法,实现类的加载。

public class HotSwapClassLoader extends ClassLoader {public HotSwapClassLoader() {super(ClassLoader.getSystemClassLoader());}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 加载指定路径下的类文件字节码byte[] classBytes = loadClassData(name);if (classBytes == null) {throw new ClassNotFoundException(name);}// 调用defineClass将字节码转换为Class对象return defineClass(name, classBytes, 0, classBytes.length);}private byte[] loadClassData(String name) {// 实现从文件系统或其他来源加载类文件的字节码// ...return null;}
}

像 Intellij IDEA 就提供了热部署功能,当我们修改了代码后,IDEA 会自动编译,如果是 Web 项目,在 Chrome 浏览器中装一个 LiveReload 插件,一旦编译完成,页面就会自动刷新。对于测试或者调试来说,就非常方便。

11. 说说解释执行和编译执行的区别

先说解释和编译的区别:

  • 解释:将源代码逐行转换为机器码。
  • 编译:将源代码一次性转换为机器码。

一个是逐行,一个是一次性,再来说说解释执行和编译执行的区别:

  • 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
  • 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。

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

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

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

图片来源于美团技术博客


http://www.mrgr.cn/news/90028.html

相关文章:

  • ASP.NET Core 如何使用 C# 向端点发出 POST 请求
  • AI刷题-融合目标计算问题
  • DeepSeek结合Langchain的基本用法
  • unity学习33:角色相关2,碰撞检测,collider 和 rigidbody,测试一个简单碰撞爆炸效果
  • Verilog语言学习总结
  • 游戏引擎 Unity - Unity 启动(下载 Unity Editor、生成 Unity Personal Edition 许可证)
  • 深入理解Java三大特性:封装、继承和多态
  • 【STM32基础】STM32F4 USB通信之HID设备(基于CubeMX)
  • 51单片机俄罗斯方块计分函数
  • 位图的深入解析:从数据结构到图像处理与C++实现
  • 蚂蚁爬行最短问题
  • 【蓝桥杯嵌入式】UART(收发)
  • 计算机毕业设计Python+Vue.js游戏推荐系统 Steam游戏推荐系统 Django Flask 游 戏可视化 游戏数据分析 游戏大数据 爬虫
  • Centos Stream 10 根目录下的文件夹结构
  • 【HeadFirst系列之HeadFirstJava】第2天之类与对象-拜访对象村
  • OpenGL学习笔记(十二):初级光照:投光物/多光源(平行光、点光源、聚光)
  • Shapefile格式文件解析和显示
  • Office/WPS接入DeepSeek等多个AI工具,开启办公新模式!
  • 《Wiki.js知识库部署实践 + CNB Git数据同步方案解析》
  • 【算法】动态规划专题⑨ —— 二维费用背包问题 python
  • springboot简单应用
  • 【SQL教程|05】Mysql中Limit用法详解
  • 大疆前端开发面试题及参考答案(4万字长文)
  • 【R语言】卡方检验
  • HTML应用指南:利用GET请求获取全国盒马门店位置信息
  • 《Cherry Studio+DeepSeek+Whoosh:三剑合璧,打造高效AI知识库与全文搜索系统》