JVM类加载机制—类加载器和双亲委派机制详解

2024-08-25 07:12

本文主要是介绍JVM类加载机制—类加载器和双亲委派机制详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、概述

上篇我们介绍了JVM类加载机制—JVM类加载过程,类加载过程是类加载机制第一阶段,这一阶段主要做将类的字节码(class文件)加载JVM内存模型中,并转换为JVM内部的数据结构(如java.lang.Class实例),便于执行。其中类加载器是JVM用于加载类文件的一个子系统,主要是通过类的全限定名来定位和加载类的二进制文件。

二、类加载器

1、类加载器分类

JVM支持两种类型的类加载器,分别为JDK自带类加载器自定义类加载器。其中自带类加载器包括引导类加载器(启动类加载器/根类加载器)、扩展类加载器、应用程序类加载器。

引导类加载器(Bootstrap ClassLoader):也加启动类加载器或根类加载器。负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等。不继承java.lang.ClassLoader,是扩展类加载器的父加载器(不是父类)。底层有C++实现,在java代码中无法直接获取到引用,返回null。

扩展类加载器(Extension ClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包,是应用程序类加载器的父加载器。由java代码编写,继承ClassLoader类。

应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载ClassPath路径下的类包,主要就是加载自己写的那些类。由java代码编写,继承ClassLoader类,为程序中默认的类加载器。

自定义类加载器(User-Defined ClassLoader):负责加载用户自定义路径下的类包,支持一些个性化的功能。自定义类加载器只需继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。代码如下

import java.io.FileInputStream;/*** 自定义类加载器*/
public class MyClassLoaderTest {static class MyClassLoader extends ClassLoader {private String classPath;public MyClassLoader(String classPath) {this.classPath = classPath;}//根据类名从磁盘上将类文件 读取到byte[]数组中private byte[] loadByte(String className) throws Exception {className = className.replace("\\.", "/");FileInputStream fis = new FileInputStream(classPath + "/" + className + ".class");int length = fis.available();byte[] bytes = new byte[length];fis.read(bytes);fis.close();return bytes;}//根据类名 加载Class类protected Class<?> findClass(String name) throws ClassNotFoundException {byte[] bytes= null;try {bytes = loadByte(name);//defineClass方法将一个字节数组转换为Class对象,这个字节数组是class文件读取后最终的字节数组return defineClass(name, bytes, 0, bytes.length);} catch (Exception e) {throw new ClassNotFoundException();}}}
}

引导类加载器是扩展类加载器的父加载器,扩展类加载器是应用程序类加载器的父加载器,但三者之间没有继承或实现的关系。

通过下面示例可以测试下类所使用的具体类加载器

/*** 类加载器过程*/
public class TestJDKClassLoader {public static void main(String[] args) {//核心程序类 支撑JVM运行 位于lib目录下  引导类加载器System.out.println("引导类加载器:" +String.class.getClassLoader());//扩展程序类 支撑JVM运行 位于lib目录中ext下的包 扩展类加载器System.out.println("扩展类加载器:" +DESKeyFactory.class.getClassLoader());//自己编写的程序 运行自身业务流程  应用程序类加载器System.out.println("应用程序类加载器:" +TestJDKClassLoader.class.getClassLoader());}
}

运行结果

通过上面结果我们可以看出 引导类加载器底层由于是C++实现,在java中无法获取,而扩展类加载器和应用程序类加载器 都是sun.misc.Launcher类下的内部类。

2、类加载初始化过程

通过类加载全过程图(加载过程)知道,类加载会创建JVM的sun.misc.Launcher类实例的启动器。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。通过Launcher类的getLauncher()方法来获取类自己的加载器ClassLoader。

getLauncher()源码

public class Launcher {private static URLStreamHandlerFactory factory = new Factory();//单例模式设计,保证一个JVM中只有一个Launcher实例private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;//获取Launcher类实例public static Launcher getLauncher() {return launcher;}//Launcher类构造方法public Launcher() {ExtClassLoader var1;try {//构造扩展类加载器,在构造的过程中将其父加载器设置为nullvar1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {//构造应用程序类加载器,在构造过程中将其父加载器设置为ExtClassLoader//Launcher类的loader属性只是AppClassLoader,一般都是用这个类加载器来加载我们写的代码this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}Thread.currentThread().setContextClassLoader(this.loader);String var2 = System.getProperty("java.security.manager");//。。。。。省略一部分不关注的代码}
}

三、JVM类加载机制种类

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。


双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。


缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

四、双亲委派机制

JVM为了避免类的重复加载,确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。JVM会采用双亲委派模型进行加载,双亲委派模型要求除了引导类加载器外,其余的类加载器都应当有自己的父加载器。

1、工作原理

当类加载器收到加载一个类的请求时,它首先不会尝试自己去加载这个类,而是为先委托父加载器寻找这个类,找不到就会在委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。简单来说先找父加载器加载,不行再由子加载器自己加载。加载流程如下:

我们来看下应用程序类加载器APPClassLoader加载类的双亲委派机制的源码。APPClassLoader加载器的loadClass方法最终会调用其父类ClassLoader的loadClass方法。该方法大体逻辑如下:

1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。

2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false));或者是调用bootstrap类加载器来加载。

3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。

ClassLoader.loadClass()源码

//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded//首先,检查当前类加载器起是否已经加载了该类,若当前加载器集合中有该类 直接返回//第一次加载该类 肯定为空Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {//如果当前类加载器的父加载器不为空,则委托父加载器加载该类//AppClassLoader 父加载器为ExtClassLoader,直接执行还是到这个loadClass方法ExtClassLoader加载器的父加载器为null(引导类加载器是C++实现,所以为null)if (parent != null) {c = parent.loadClass(name, false);} else {//如果当前加载器的父加载器为空,则委托引导类加载器加载该类c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}//当该类通过类加载器加载为空时,通过findClass方法加载类//ExtClassLoader无findClass方法,通过父类URLClassLoader.findClass方法加载类if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();//通过父类URLClassLoader.findClass方法加载类c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

URLClassLoader.findClass方法源码

//类加载器通过findClass方法尝试加载类
protected Class<?> findClass(final String name)throws ClassNotFoundException{final Class<?> result;try {result = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {public Class<?> run() throws ClassNotFoundException {//通过类的name 获取类的路径//如com.test.jvm.Math 获取path = com/text/jvm/Math.classString path = name.replace('.', '/').concat(".class");//通过类路径从磁盘上拿到类文件Resource res = ucp.getResource(path, false);if (res != null) {try {//通过defineClass方法将类装载到JVM中//ExtClassLoader加载器是装载失败的,因为ExtClassLoader加载的是ext包下的路径,返回为null//再会执行AppClassLoader加载器的defineClass进行加载,才会装载JVM成功return defineClass(name, res);} catch (IOException e) {throw new ClassNotFoundException(name, e);}} else {return null;}}}, acc);} catch (java.security.PrivilegedActionException pae) {throw (ClassNotFoundException) pae.getException();}if (result == null) {throw new ClassNotFoundException(name);}return result;}

2、双亲委派机制意义

沙箱安全机制:确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。如自己写的java.lang.String.class类是不会被加载的

类的唯一性:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,避免重复加载,保证被加载类的唯一性

上一篇: JVM类加载机制—类加载过程

这篇关于JVM类加载机制—类加载器和双亲委派机制详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1104892

相关文章

Spring Boot集成/输出/日志级别控制/持久化开发实践

《SpringBoot集成/输出/日志级别控制/持久化开发实践》SpringBoot默认集成Logback,支持灵活日志级别配置(INFO/DEBUG等),输出包含时间戳、级别、类名等信息,并可通过... 目录一、日志概述1.1、Spring Boot日志简介1.2、日志框架与默认配置1.3、日志的核心作用

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

破茧 JDBC:MyBatis 在 Spring Boot 中的轻量实践指南

《破茧JDBC:MyBatis在SpringBoot中的轻量实践指南》MyBatis是持久层框架,简化JDBC开发,通过接口+XML/注解实现数据访问,动态代理生成实现类,支持增删改查及参数... 目录一、什么是 MyBATis二、 MyBatis 入门2.1、创建项目2.2、配置数据库连接字符串2.3、入

Springboot项目启动失败提示找不到dao类的解决

《Springboot项目启动失败提示找不到dao类的解决》SpringBoot启动失败,因ProductServiceImpl未正确注入ProductDao,原因:Dao未注册为Bean,解决:在启... 目录错误描述原因解决方法总结***************************APPLICA编

深度解析Spring Security 中的 SecurityFilterChain核心功能

《深度解析SpringSecurity中的SecurityFilterChain核心功能》SecurityFilterChain通过组件化配置、类型安全路径匹配、多链协同三大特性,重构了Spri... 目录Spring Security 中的SecurityFilterChain深度解析一、Security

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

SpringBoot多环境配置数据读取方式

《SpringBoot多环境配置数据读取方式》SpringBoot通过环境隔离机制,支持properties/yaml/yml多格式配置,结合@Value、Environment和@Configura... 目录一、多环境配置的核心思路二、3种配置文件格式详解2.1 properties格式(传统格式)1.

Apache Ignite 与 Spring Boot 集成详细指南

《ApacheIgnite与SpringBoot集成详细指南》ApacheIgnite官方指南详解如何通过SpringBootStarter扩展实现自动配置,支持厚/轻客户端模式,简化Ign... 目录 一、背景:为什么需要这个集成? 二、两种集成方式(对应两种客户端模型) 三、方式一:自动配置 Thick

Python标准库之数据压缩和存档的应用详解

《Python标准库之数据压缩和存档的应用详解》在数据处理与存储领域,压缩和存档是提升效率的关键技术,Python标准库提供了一套完整的工具链,下面小编就来和大家简单介绍一下吧... 目录一、核心模块架构与设计哲学二、关键模块深度解析1.tarfile:专业级归档工具2.zipfile:跨平台归档首选3.

Spring WebClient从入门到精通

《SpringWebClient从入门到精通》本文详解SpringWebClient非阻塞响应式特性及优势,涵盖核心API、实战应用与性能优化,对比RestTemplate,为微服务通信提供高效解决... 目录一、WebClient 概述1.1 为什么选择 WebClient?1.2 WebClient 与