香雨站

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 109|回复: 1

JAVA类加载器就这么简单?

[复制链接]

2

主题

3

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-6-5 14:43:42 | 显示全部楼层 |阅读模式
一段代码如果要运行,必须要加载到内存中。那对于Java来说是由谁把代码加载到内存中的呢?具体是怎么加载的呢?这篇文章就来分析下这个问题。
什么是类加载器?
当写了一个Java代码后,会编译成.class文件,然后交给JVM执行。而把静态的.class文件加载到内存中的就是类加载器。
把一个类完全加载到JVM中共有几个过程?


加载:这个阶段就是类加载器负责的,主要进行三个动作
1.  通过一个类的全限定名来获取此类的二进制字节流
2.  将字节流代表的静态存储结构转换为方法区的运行时数据结构
3.  在内存中创建一个代表此类的java.lang.Class对象,作为方法区此类的各种数据的访问入口
小贴士:注意类加载器负责的仅仅是这个阶段,并不是完整的类加载过程

验证:主要校验一下.class文件是否符合规范
准备:为类的静态变量分配空间并初始化值 比如:
public static int num = 100;在准备阶段会为num开辟4个字节的内存,并设置num的初始值为0

解析:这个阶段可以理解为在类放到内存之后,把类中的一些字符串标记换成内存中的地址偏移量。解析之后,类名,方法名,字段名就会变成真实的内存地址
初始化:主要对类变量进行赋值 比如:
public static int num = 100; 在这一步num就会被赋值为100

触发类初始化的时机?
1. new对象2. 调用类的静态变量3. 调用类的静态方法4. 用反射包里的方法操作类5. 初始化类的时候如果父类没有初始化,就先初始化父类6. 虚拟机启动,先初始化主类
小贴士:Class.forName() 和 ClassLoader加载一个类有什么区别?这个问题就可以解释了。Class.forName()会触发初始化。ClassLoader只是加载完成。

Java中的类加载器有哪些呢?
BootstrapClassLoader:启动类加载器。负责加载JAVA_HOME\lib\下的类。位高权重,开发者不能直接使用这个类加载器
ExtensionClassLoader:扩展类加载器。负责加载jdk中JAVA_HOME\lib\ext下的类,开发者能直接使用这个类加载器
ApplicationClassLoader:应用类加载器。负责加载classpath下的类,默认的类加载器
自定义加载器:开发者继承ClassLoader自己实现的类加载器,主要用于定制化的类加载
ThreadContextClassLoader:线程上下文加载器,用于在一个线程中传递类加载器,默认里面放的是ApplicationClassLoader

把一个类加载到JVM中的流程是什么呢?


1. 默认首先由ApplicationClassLoader加载器先看下该类有没有被加载过,如果被加载过则直接返回被加载过的类对象。否则委托父加载器进行检查
2. ExtensionClassLoader加载器同样先看下该类有没有被加载过,如果被加载过则直接返回被加载过的类对象。否则委托父加载器进行检查
3. 直到BootstrapClassLoader加载器,同样先看下该类有没有被加载过,如果被加载过则直接返回被加载过的类对象。但是如果没加载过,就会看下自己能不能加载(该类有没有在自己的管辖范围),如果能加载就会直接加载并返回Class对象。如果不能加载就会委派下级加载器(ExtensionClassLoader)进行加载。4. ExtensionClassLoader就会看下自己能不能加载(该类有没有在自己的管辖范围),如果能加载就会直接加载并返回Class对象。如果不能加载就会委派下级加载器(ApplicationLoader)进行加载。
5. ApplicationLoader就会看下自己能不能加载(该类有没有在自己的管辖范围),如果能加载就会直接加载并返回Class对象。如果不能加载(找不到这个类)则报java.lang.ClassNotFoundException异常

小贴士:如果自定义的加载器遵守双亲委派,那就和上面流程一样。否则就是单独有一套加载逻辑,打破了双亲委派。
上面的流程正是传说中的双亲委派模型。

那为什么要用这个模型呢,从下向上找完还要从上往下,用得着这么麻烦吗?
这个做主要是解决以下两个问题:
1. 避免同一个类被多个加载器加载
从下向上委派,保证了被加载过的类不会再次走加载流程。而从上往下的委派,就保证了一个类只会被一个加载器加载。
2. 保证限定名一样的类优先被顶层的加载器(优先级高)加载,保证类加载的安全性
想象有一种情况是开发者自己写了一个类,限定名和核心包中的一个类完全一样,比如是String类。
如果没有从上向下的委派,就可能核心包的String类被BootstrapClassloader加载了,但是开发者自己写的String类被ApplicationClassloader加载了。
那么在代码中用到String类的时候,可能很多调用的就是开发者自己写的String类,必然会造成一些安全问题。

有的人会说不用上面的双亲委派流程我感觉也能实现相同的功能啊,直接从根判断有没有加载过,并且能不能加载不是也可以吗?
比如让BootstrapClassloader先判断有没有加载过这个类。如果加载过则直接返回Class对象。如果没有则看下自己能不能加载这个类,如果能就直接加载并返回。如果不能就委派给下级类加载器。每层的加载器都做同样的操作。
其实这也是一种实现方式。并且看起来比双亲委派的时间复杂度更低,更简洁。但是为什么JDK的开发者双亲委派的方式呢。

我认为主要有以下几个考虑:
1. 如果从最上层来向下委派的话,那么上层需要保存下层的类加载器引用。这就像数据表的一对多关系,把"多"的一方的ID放在"一"的一方,好不好处理不说,毕竟不优雅。
2. 极端一点的情况,如果ApplicationClassloader下定义了10个自定义加载器。那么直接从上向下委派的话,难不成还需要把这十个类加载器都检查一遍
通过以上的原因,应该就可以体会到双亲委派模型其实是一个非常优秀的设计,我认为也是一个递归算法的经典应用。

打破双亲委派模型是什么意思?
打破双亲委派是指在Java类加载器机制中,自定义类加载器在加载类时不再采用默认的双亲委派模型,而是通过重写类加载方法来实现自己的加载逻辑,绕过父类加载器的委派,直接由自定义类加载器来完成类的加载。

打破双亲委派模型的具体场景有哪些?
1. Tomcat:因为一个Tomcat中可以运行多个服务,多个服务中就可能出现多个限定名一样的类,因为是不同的服务,所以即使限定名一样也应该都加载。那么就和原来的双亲委派模型冲突了,所以就需要打破双亲委派才行。
2. 用自定义类加载器解决版本冲突:在项目开发的过程中可能服务A依赖了A.0.1.jar,中间件A和中间件B。并且中间件A和中间件B还依赖了A.0.2.jar,A.0.3.jar。



因为依赖的传递性,所以服务A中会有三个版本A的jar包。加载的时候服务A会选择一个进行加载。A.0.1版本距离服务A最近,所以A.0.1版本会被加载。但是因为A.0.1版本比较老,所以就有可能在B,C用到A的时候出现异常。
为了解决这个问题,一个方案就是打破类双亲委派机制。分别为服务A,中间件A和中间件B定义类加载器。这样三个版本的包都可以被加载并互不影响。

好多人说SPI破坏了双亲委派模型,果真如此吗?
先说结论,我认为并没有破坏。拿JDBC来说。JDBC定义了接口,具体实现类由各个厂商进行实现(比如MySQL)。根据类加载规则,如果一个类由类加载器A加载,那么这个类的依赖类也是由A加载。当使用JDBC获取链接的时候用的是如下方式:
Connection connection = DriverManager.getConnection("jdbc:mysql://xxxxxx/xxx", "xxxx", "xxxxx");DriverManager在java.sql包,应该由BootstrapClassloader来加载。但是Connection实现类是放在classpath路径下的,所以最终是通过那线程上下文类加载器(默认就是ApplicationClassloader)来加载的。可能有的人认为Connection应该由BootstrapClassloader加载,现在却一下跳到ApplicationClassloader加载了。其实我认为按照双亲委派模型来说,BootstrapClassloader加载不了就应该向下传递进行加载。只不过jdbc是直接从BootstrapClassloader开始加载的,没有办法向下传递,所以才从线程上下文拿出类加载器重新走了一遍类加载流程。我认为这其实是对双亲委派的补充,不能算是破坏了双亲委派。

如何自定义加载器?
public class CustomClassLoader extends ClassLoader {

    public CustomClassLoader() {
        super(); // 默认父加载器是系统类加载器
    }

    public CustomClassLoader(ClassLoader parent) {
        super(parent); // 使用指定的父加载器
    }

    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 在此方法中实现自定义类加载逻辑
        // ...
        throw new ClassNotFoundException(name);
    }

    public static void main(String[] args) {
        ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader().getParent(); // 获取系统类加载器的父加载器
        CustomClassLoader loader = new CustomClassLoader(parentClassLoader); // 使用系统类加载器的父加载器作为父加载器
        try {
            Class<?> clazz = loader.loadClass("com.example.MyClass");
            System.out.println("Loaded class: " + clazz.getName());
            System.out.println("Classloader of loaded class: " + clazz.getClassLoader());
        } catch (ClassNotFoundException e) {}
    }
}
小贴士:尤其要注意的是两个构造方法,无参数的默认上级加载器是系统加载器。有参数的可以自己设置。

如何获取类加载器?
1. 通过对象的getClassLoader()方法获取该对象的类加载器,例如:
ClassLoader classLoader = obj.getClass().getClassLoader();2. 通过当前线程的getContextClassLoader()方法获取当前线程的上下文类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();3. 通过类的getClassLoader()方法获取该类的类加载器
ClassLoader classLoader = MyClass.class.getClassLoader();
小思考题:加载一个类的时候是怎么确定用那个类加载的呢?



更多内容持续输出中
回复

使用道具 举报

5

主题

8

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2025-1-30 09:52:01 | 显示全部楼层
看起来不错
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|香雨站

GMT+8, 2025-3-15 13:38 , Processed in 0.811941 second(s), 23 queries .

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.. 技术支持 by 巅峰设计

快速回复 返回顶部 返回列表