目 录CONTENT

文章目录
JVM

双亲委派机制延伸-ClassLoader

MWSFOT
2024-02-05 / 0 评论 / 0 点赞 / 281 阅读 / 3,100 字

先抛出问题

  1. ClassLoader是什么
  2. 什么是双亲委派机制
  3. Java是如何由源代码->执行结果的
  4. JVM按什么顺序加载.class文件的
  5. ClassLoader 如何保证每个类只会加载一次
  6. 同名同包类最终加载的结果是什么, 是都会被加载还是只会加载其中一个.如果都会被加载那为什么呢? 如果只会被加载其中一个那又是为什么?
  7. 同名不同包类最终加载的结果是什么, 是都会被加载还是只会加载其中一个.如果都会被加载那为什么呢? 如果只会被加载其中一个那又是为什么?

ClassLoader是什么

ClassLoader是虚拟机负责装载类的对象。类ClassLoader是一个抽象类。给定类的二进制名称,类加载器应该尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”即.class

什么是双亲委派机制

为了实现双亲委派机制. 在Java中,类加载器负责加载类的字节码并创建对应的Class对象。而双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。

这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库,它的父级为null, 其他子类加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。

这种机制的好处是可以避免类的重复加载,同事提高了类加载的效率和系统类的安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现自己的加载策略

Java是如何由源代码->执行结果的

java源代码->执行结果中间分为2大部分.
1 编译=>结果生成java虚拟机能够接受的.class文件
2. 类加载执行class文件=>得到运行结果
其中编译阶段分为: 词法分析->语法分析->语义分析->编译class文件
而虚拟机执行流程分为 加载.class文件(状态->连接->初始化)阶段得到系统指令码->执行阶段发送指令码进行相关操作
如上的委托父级去加载. 是在已经获取到class文件后才会去执行的

JVM按什么顺序去加载.class文件

  1. 本地类路径:JVM会首先在本地类路径中查找并加载类。本地类路径一般包括当前工程的编译输出目录和依赖的第三方库。
  2. 扩展类路径:如果在本地类路径中没有找到类,则JVM会在扩展类路径中查找并加载类。扩展类路径一般包括JVM安装目录下的jre/lib/ext目录和java.ext.dirs系统属性指定的目录。
  3. 系统类路径:如果在扩展类路径中仍然没有找到类,则JVM会在系统类路径中查找并加载类。系统类路径一般包括JVM启动时指定的类路径,可以通过java -classpath或java -cp参数来指定。
    **注意:**在上述加载顺序中,当JVM找到第一个与类名匹配的类时,就会停止搜索,不再继续查找

ClassLoader怎样保证同一时间每个类只会加载一次

ClassLoader设计的大概实现和目的上述已经描述清了, 接下来看看如何去保证每个类同时只会加载一次

源码分析

/**
ClassLoader.loadClass();
* 根据名称去加载类, 这点可以自定义
* 整体的类加载实现逻辑在这个方法里就可以看到
*/
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已经被加载过. 有就直接使用,没有就跳过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                //逐级向上递, 所有的ClassLoader实现类都会实现/使用该方法.除非自定义破坏
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //只有BootstrapLoader 父类才会为null. 运行到这里也就是到了顶层了
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                
                }
                //如果父类加载都为空, 那么尝试自己去实现
                if (c == null) {
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    //// 多余代码已删除
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

重点关注下这里的synchronized (getClassLoadingLock(name)) synchronized关键字应该都清楚, 放在这里锁的是代码块, 锁的是getClassLoadingLock(name)返回的这个对象. 保证返回相同的这个对象统一时间只会被执行一次 再观察下这个方法

private final ConcurrentHashMap<String, Object> parallelLockMap;
/**
* 根据className去获取对象. 这里的parallelLockMap对象为ConcurrentHashMap. 这属于是一个线程安全的Map. 感兴趣可以额外去了解下. 这里不再赘述
*/
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
        // 创建一个新Object对象
            Object newLock = new Object();
       // 当map.putIfAbsent() put成功时, 返回null,当前对象, 反之,已经存在则返回当前对象
            lock = parallelLockMap.putIfAbsent(className, newLock);
       // 注意如果put成功才会为null. 也就是返回的lock 是当前classLoader对象
            if (lock == null) {
      // put失败, 才会返回一个新的对象
                lock = newLock;
            }
        }
        return lock;
    }

整理流程

拿这里举几个例子.依次整理下思路
自定义MyString(com.mwsfot包下) 和 MyString(com.mwsfot包下) 从包的概念理解上, 它就应该被加载两次吧. 那我自定义一个String(java.lang). 然后在main方法中输出下自身的加载器和类加载器, 结果应该是什么? 按照我们预期的结果

第一行结果是AppClassLoader
第二行则是ExtClassLoader

package java.lang;

/**
* 自定义String类
 * @author MC
 * @description String
 * @date 2024/2/5 11:03
 */
public class String {
    private int a;
    public String(Integer a){
        a = a;
    }
    public String(){
        
    }
}

输出类

public class Test{
    public static void main(String[] args) {
    	String s = new String("123");
        byte[] bytes = s.getBytes();
        System.out.println(bytes);
        System.out.println("自己的类加载器: "+ s.getClass().getClassLoader());
        System.out.println("父类的类加载器: " + s.getClass().getClassLoader().getParent());
    }

以上,都编译通过

image-1707102905058
想好了么, 揭晓答案了. 答案是会报错. 可以看到这里main方法. 我们用的是真正的java官方提供的java.lang.String. 只有它才拥有getBytes方法. 为什么会报错呢. 这里没有使用到我们自己编写的String类, 使用的官方的java.lang.String了. 而官方的Stringmain方法的入口参数. 同一个类中只会引入一个类型.
而报错原因也很简单, String是rt.jar包里的, 是通过BootstrapClassLoader加载的. 使用的是C++底层. 所以不存在类加载器, 也就是null
image-1707103730318
既然同包名无法执行,那我们换个包名区分下

public class String {
    private int a;
    public String(int a){
        a = a;
    }
}

image-1707103902088
ok 可以看到, 编译通过了, 我们换个包名就被识别到了, 现在再想想执行结果是什么
image-1707104049885
揭晓答案, 答案是找不到主类.
原因很简单.这里的main方法不是我们熟知的java入口了, 这里的String[] args是我们自己编写的String
注意不是因为类加载系统的问题, 上面说了这里只是编译阶段, 还没有到类加载的阶段!

ok我们再改写下, 指定下java.lang.String如下:

public static void main(java.lang.String[] args) {
        String s = new String(1);
        System.out.println("自己的类加载器: "+ s.getClass().getClassLoader());
        System.out.println("父类的类加载器: " + s.getClass().getClassLoader().getParent());
    }

现在再最后想想执行结果是什么. 第一行结果是AppClassLoader吗. 第二行是ExtClassLoader

揭晓答案, 正如我们猜想的一样. 自定义的类使用AppClassLoader加载. 同时与String不冲突.
image-1707104452148

结论

我们现在将最后一步的自定义的String类代入ClassLoader的当中.
image-1707108220674
我们的name叫做com.mwsfot.parent.String. 继续往下走进入getClassLoadingLock方法
image-1707108309280
可以看到存在的是叫做java.lang.String. 实际上在map中存储的不是同一个String类
image-1707108372654
返回为null也可以得出.
image-1707108429266
此时的两个String类, 不是同一个对象. 我们的String就相当于普通的类. 会被AppClassLoader加载也就不奇怪了. 这也就回答上面提出的问题.因为返回的value不是同一个对象, 我们的synchronized (getClassLoadingLock(name))也就当然锁不住了.
这也就是为何每个类同时只会被加载一次的原因. 而同名同包类本身编译阶段都无法通过. java 就是通过这样去保证每个类同时只会被加载一次的.

同名同包类和同名不同包类的结果

虽然上面解释了, 保证每个类同时只会加载一次的问题. 但是又引申出了新的问题. 那加载呢? 这里只是控制了顺序, 具体的加载那怎么办?
loadClass中存在一个findLoadedClass方法, 就是去缓存中获取已经加载过的class信息.

Class<?> c = findLoadedClass(name);

如果没有加载过走双亲委派去找,上述都失败为null的情况. 才去自身的findClass方法查找. 默认抛出异常信息. 由实现类自行去实现. 一般实现defineClass方法去网络或者本地去查找class信息.最终加载进来.
以上就是是加载的流程.
考虑到我们标题提的问题, 这里就需要去扩展思考下了,前提都是没有自定义ClassLoader

  1. 假如真的有两个同名同包的java.lang.String 通过了编译阶段. 第一个是正版, 第二个盗版.
    1). 第一个正版String存入了Map当中, 锁的是value值, 即new Object().
    2). 第二个盗版String存入Map. 发现存在, 于是返回对象ClassLoader.
    3). 锁的是ClassLoader这个对象. 当然没锁住, 但是锁住了类. 此时的class缓存必然存在结果, 要么已经存在要么不存在.
    4). 此时去缓存中根据名称去获取判断, 存在就直接返回, 不存在就走双亲委派机制去找到当前class. 如果不存在委派去父类加载, 父类层层委派.最终在BootstrapClassLoader返回正版的String. 我们的盗版替代正版的希望又轮空了.
    5). 再假设我们又通过了委派机制(自定义),返回的就是我们自定义的类, 后续又有一个方法叫做defineClass 用于返回class定义, 这里又逃逸不过去, 会校验名称, java.lang.String java开通头的名称, 必须由BootstrapClassLoader去加载. 至此形成了闭环. 只会返回正版的String
  2. 还是上面的情况, 只不过我们目标现在没那么远大了, 我们就想替换某个三方包的类, 比如com.a.Acom.a .B A是正版, B是我们想替换的盗版, 再代入进去, A 和 B任意一个, 先被加载存入Map.不存在同时, 因为有synchronized关键字锁value值, 同时ComcurrentHashMap存返值. 所以两个加载必然被保留先后顺序. 最终结果只会加载第一个同名文件.
  3. 同名不同包就很简单理解了, 不同的全限定名就是不同的Map对象. 存在先后顺序, 找到的class文件当然就是不同的class
0

评论区