记一次ClassLoader发现之旅(详细)

简介

debug看到sun.misc.Launcher$AppClassLoader@63961c42,不明白AppClassLoader是什么东西,故查阅并记录此次探索之旅。

参考文章:

blog.csdn.net/u013412772/… blog.csdn.net/mggwct/arti… blog.csdn.net/briblue/art… blog.csdn.net/fuzhongmin0… www.jianshu.com/p/a18aecaec…

一、什么是ClassLoader


大家都知道,当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,会调用该程序的一个入口函数来调用系统的相关功能.而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。

而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的

其中具体加载过程为:JVM加载.class字节码到内存,而.class文件时怎么被加载到JVM中的就是Java ClassLoader需要做的事情.

那JVM什么时候加载.class文件?

  1. 当执行new操作时候
  2. 当执行Class.forName(“包路径 + 类名”)\ Class.forName(“包路径 + 类名”, ClassLoader)\ ClassLoader.loadClass(“包路径 + 类名”)

以上情况都会触发类加载器去类加载对应的路径去查找对应的.class文件,并创建Class对象.

不过(2)方式加载字节码到内存后生产的只是一个Class对象,要得到具体的对象实例还需要使用Class对象的newInstance()方法来创建具体实例.

再引用一段话来说明什么是类加载器:

  1. 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

  2. 类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足Java Applet的需求而开发出来的。虽然目前Java Applet技术基本上已经“死掉”,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了Java技术体系中一块重要的基石,可谓是失之桑榆,收之东隅。

  3. 类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。


1.1 AppClassLoader

AppClassLoader应用类加载器,又称为系统类加载器,负责在JVM启动时加载来自命令java中的classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。

public class AppClassLoaderTest  {
    public static void main(String[] args) {
        System.out.println(ClassLoader.getSystemClassLoader());
    }
}
复制代码

输出结果如下:

sun.misc.Launcher$AppClassLoader@63961c42
复制代码

以上结论可以说明调用ClassLoader.getSystemClassLoader()可以获得AppClassLoader类加载器。再深入一点,我们看看getSystemClassLoader是怎么调用AppClassLoader的:

    //初始化系统类加载器
    initSystemClassLoader();
复制代码

那么我们再进去看看:

private static synchronized void initSystemClassLoader() {

    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
}
复制代码

总结一下就是getSystemClassLoader() -> initSystemClassLoader() -> Launcher(),前面两个都是过度,真正有用的是Launcher()

下面详细解释一下Launcher():

public Launcher() {
    Launcher.ExtClassLoader var1;
    
    try {
        //1.初始化ExtClassLoader
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }
    try {
        //2.初始化AppClassLoader,将ExtClassLoader置为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);
    ...
}
复制代码

Launcher类初始化时,先初始化了个ExtClassLoader,然后又初始化了个AppClassLoader,然后把ExtClassLoader作为AppClassLoader的父loader。

同时,我们知道,在没有特定说明的情况下,用户自定义的任何类加载器都将java.lang.ClassLoader作为自定义类加载器的父加载器.

在上面我们刚开始的main函数的类的加载就是使用AppClassLoader加载器进行加载的,也可以通过执行下面的代码得出这个结论:

public class AppClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
    }
    
    private static class Test {

    }
}
复制代码

执行结果如下:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742
复制代码

从上面的运行结果可以得知AppClassLoader的父加载器是ExtClassLoader,接下来继续说一下ExtClassLoader类加载器.


1.2 ExtClassLoader

ExtClassLoader称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制).

ExtClassLoader的类扫描路径通过执行下面代码来看一下:

C:\Program Files\Java\jdk1.8.0_77\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
复制代码

其中C:\Java\jdk1.8.0_101\jre\lib\ext路径下内容为:

从上面的路径中随意选择一个类,来看看他的类加载器是什么:

sun.misc.Launcher$ExtClassLoader@27fa135a
null
复制代码

从上面的程序运行结果可知ExtClassLoader的父加载器为null.


1.3 BootstrapClassLoader

称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
    System.out.println(url.toExternalForm());
}
复制代码

执行结果如下:

file:/C:/Java/jdk1.8.0_101/jre/lib/resources.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/rt.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/sunrsasign.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/jsse.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/jce.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/charsets.jar
file:/C:/Java/jdk1.8.0_101/jre/lib/jfr.jar
file:/C:/Java/jdk1.8.0_101/jre/classes
复制代码

从rt.jar中选择String类,看一下String类的类加载器是什么

ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
复制代码

执行结果如下:

null
复制代码

其实除了String,byte/char/int等八大基本类型都是由BootstrapClassLoader加载的,那么可以装箱拆箱的Char类型呢,是否类加载也是null呢?

ClassLoader cl = Char.class.getClassLoader();
System.out.println(cl);
复制代码

执行结果如下:

null
复制代码

因为它们也都是在rt.jar包之中的,所以也一同被BootStrapClassLoader加载了。

但是为什么ClassLoader是null呢?

是因为BootstrapClassLoader对Java不可见,所以返回了null,我们也可以通过某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassLoader进行加载的依据.另外上面提到ExtClassLoader的父加载器返回的是null,那是否说明ExtClassLoader的父加载器是BootstrapClassLoader?

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象


小结

你能用一句话总结下列问题吗:

  1. ClassLoader是什么?ClassLoader是类加载器,是用来动态的将.class文件载入内存中供JVM使用。
  2. BootstrapClassLoader加载哪些类?/jre/lib下的jar包(不包括文件夹内的jar包)
  3. ExtClassLoader加载哪些类?/jre/lib/ext下面的jar包
  4. AppClassLoader加载哪些类?除去bootstrapClassLoader和ExtClassLoader之外的类。比如说maven的pom.xml里面的dependencies标签引入的类。

讲到这里,ClassLoader就基本结束了,上面就是ClassLoader的全部内容。下面的一些扩展,是在上面基础上的一些详细描述和扩展应用,感觉有用的可以看看,没用的就跳过。


ClassLoader扩展(一):双亲委派模型


双亲委派模型(Parents Dlegation Model),因为国外的parents翻译过来是双亲,所以有了这个名字,其实哪里是双亲,明明是单亲委派模型,不信的话可以看下图:

图上哪有什么双亲,只有不断向上查找的父亲。所谓的双亲委派模型就是要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

我们来看一下双亲模型的关键代码:

loadClass()
public abstract class ClassLoader {
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先, 检查类是否已被加载
            Class<?> c = findLoadedClass(name);
            // 父类查找
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {// bootstrapClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    ...
                }
    
                if (c == null) {
                    // 父类没有找到,在自己要加载的类中查找是否存在
                    c = findClass(name);
                }
            }
            return c;
        }
    }
}
复制代码

loadClass()方法,就是双亲委派模型,从代码中可以看出,它的查找流程是:

  1. 先检查类是否已经被加载过,若没有则递归调用父加载器的loadClass()方法,直到parent==null,也就是父类加载器为BootstrapClassLoader。
  2. 父类加载器没找到,那么逐层退出递归,不断的让子加载器去尝试加载这个类,直到加载成功。

双亲委派的具体逻辑就实现在ClassLoader类的loadClass()方法之中,大家可以自己去看,JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

一个小问题

如果我们自己写了一个类加载器要加载String类,而Bootstrap加载器在启动时也会加载String类,最后到底由谁来加载?加载成功还是会报一个冲突的错误?

答案很明显:加载成功,由Bootstrap加载器加载。从代码中可以看出,一开始子类不自己加载,一直让父类加载,而父类加载完成后,findLoadedClass()方法返回不为空,则子类永远不再加载该类。


ClassLoader扩展(二):破坏双亲委派模型


所谓破坏双亲委派模型,解释起来一点都不复杂:

  • 破坏双亲委派模型,其实就是重写 loadClass(String name,boolean resolve) 方法

  • 破坏双亲委派模型,其实就是重写 loadClass(String name,boolean resolve) 方法

  • 破坏双亲委派模型,其实就是重写 loadClass(String name,boolean resolve) 方法

2.1 如何打破双亲委派模型

本篇将围绕一个问题展开叙述,讲述一下我是怎么一步步实现双亲委派模型的。

问题:一个类的静态块是否可能被执行两次?

我们知道类加载的初始化阶段会自动收集类中所有类变量的赋值动作与静态语句块中的语句生成一个方法,这个方法只会被执行一次。

根据双亲委派模型的代码,不管你怎么新建ClassLoader,只要用到了原有的loadClass方法加载类,那么这个类就会被缓存,再次加载的时候直接使用缓存,所以静态代码块永远不会执行第二次。

下面,来说说怎么破坏双亲委派模型:

public class FBreakParentClassLoader extends  ClassLoader{
    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        if(name.indexOf("MyStaticUtil")>-1){
            return findClass(name);
        }
        return super.loadClass(name,resolve);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(mLibPath,fileName);
        try {
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            System.out.println(name.substring(index+1)+".class");
            return name.substring(index+1)+".class";
        }
    }
}
复制代码

对,就是这么简单,新建一个FBreakParentClassLoader类,并重写loadClassfindClass方法即可。

开始测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ClassLoaderTest {
    @Test
    public void addClass(){
        //新建两个类加载器
        FBreakParentClassLoader fBreakParentClassLoader = new FBreakParentClassLoader("F:\\");
        FBreakParentClassLoader sBreakParentClassLoader = new FBreakParentClassLoader("F:\\");
        try {
            //第一次加载
            fBreakParentClassLoader.loadClass("MyStaticUtil").newInstance();
            System.out.println("----------------------------------");
            //第二次加载
            sBrParentClassLoader.loadClass("MyStaticUtil").newInstance();
            System.out.println("----------------------------------");
            
        } catch (...) {
        }
    }
}
复制代码

最后,查看结果时看到,static代码块被执行了两次:


ClassLoader扩展(三):Tomcat是怎么破坏双亲委派模型的

3.1 Tomcat类加载关系简介

上图可以看到Tomcat的类加载关系, Common ClassLoader作为 Catalina ClassLoaderShared ClassLoader的parent,而 Shared ClassLoader又可能存在多个children类加载器 WebApp ClassLoader,一个 WebApp ClassLoader实际上就对应一个Web应用,那Web应用就有可能存在Jsp页面,这些Jsp页面最终会转成class类被加载,因此也需要一个Jsp的类加载器,就是图中的 JasperLoder

3.1.1 从代码角度分析:Common ClassLoader和Catalina/Shared ClassLoader是怎么关联的

Tomcat的启动入口在Bootstrap.class中

⬇️我们看一下bootstrap.init()中初始化了什么⬇️

⬇️在初始化Tomcat的类加载器的时候做了以下三件事:⬇️

  1. 初始化加载commonLoader
  2. 初始化加载catalinaLoader,并将commonLoader置为父加载器
  3. 初始化加载sharedLoader,并将commonLoader置为父加载器

从上面的代码我们已经将第一幅图Common ClassLoaderCatalina ClassLoader/Shared ClassLoader关联上了。下面我们看看Common ClassLoader怎么和Application ClassLoader关联起来。

3.1.2 Common ClassLoader和Application ClassLoader是怎么关联的

我们想一下,Common ClassLoader要和Application ClassLoader建立关联,只可能是在加载时,也就是上面的createClassLoader("common",null)方法里。

    /**
     * 根据配置默认值和指定的目录路径创建并返回新的类加载器
     */
    public static ClassLoader createClassLoader(List<Repository> repositories,
                final ClassLoader parent)throws Exception {
        
        // 注释了部分代码
        
        // 下面的return是重点
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null)
                            //返回 URLClassLoader(urls);
                            return new URLClassLoader(array);
                        else
                            return new URLClassLoader(array, parent);
                    }
                });
    }
复制代码

上述代码可以看到,返回的是URLClassLoader(array)【array是要加载的urls】。array是什么并不重要,因为我们要关注的new URLClassLoader中并没有用到。

事实上URLClassLoader在构造器上不断的super(),也就是说它调用的是ClassLoader的构造方法。我们来看如下关系图:

再看下面的代码:

public URLClassLoader(URL[] urls) {
    super();
}
复制代码

⬇️所以我们直接找到了super()的源头,ClassLoader的构造器,请看下面的代码:⬇️

sun.misc.Launcher.class类的loader成员变量,通过 getClassLoader()获取的就是应用类加载器(系统类加载器),也就是Application ClassLoader。

实例化的具体代码如下:

至此,Common ClassLoader和Application ClassLoader就关联上了。

3.1.3 Shared ClassLoader和WebApp ClassLoader是怎么关联的

Bootstrap中我们看到,它先用SharedLoader初始化了Catalina类。

然后在main()方法中调用 Catalina.start()方法。

public static void main(String args[]) {
    //...
    } else if (command.equals("start")) {
        daemon.setAwait(true);
        daemon.load(args);
        daemon.start(); //这里调用Catalina.start()方法
    }
    //...
复制代码

Catalina.start()做了什么?

答:其实它调用了生命周期实现方法LifeCycleBase.start()方法。而LifeCycleBasestart()中的startInternal()StandardContext.startInternal重写。我们的WebAppClassLoader就在这里被关联。

我们来看代码:

StandardContext.startInternal中,将webappClassLoader和SharedClassLoader关联上了。


3.2 Tomcat是怎么破坏双亲委派的

1.loadClass实现

Springboot中Tomcat破坏双亲委派模型是由TomcatEmbeddedWebappClassLoader重写loadClass来完成的。java类装载过程(看过深入理解JVM虚拟机的同学应该很熟悉这张图):

其实它的实现就是:

第一步、查询缓存
Class<?> result = findExistingLoadedClass(name);
复制代码
第二步、加载类

doLoadClass的代码,可分为3点:

上述可以分解成下面代码,上下代码是等价的:

//1.判断是否为特定的类,delegate默认为false
boolean delegateLoad = delegate || filter(name, true);

//2.特定类交于双亲委派(loadFormParent)
if (delegateLoad) {
    // 用Class.forName有个好处是可以指定classLoader()
    return Class.forName(name, false, this.parent);//parent -> AppClassLoader
}

//3.若delegate被修改为true,执行findClassIgnoringNotFound(name)
return findClass(name);
复制代码

还有一个filter,其实filter(name,true)里硬编码写死了一些类要交于双亲加载:

  • javax开头的javax.el.*,javax.servlet.*,javax.websocket.*,javax.security.auth.message.*
  • org.apache开头的几个类,包含下图的一些类

2.findClass实现

findClass的关键作用其实就是找到并且加载类。它是由WebAppClassLoaderBase类来实现的。

findClassInternal将类加载进来


拓展:OSGi

转载于:https://juejin.im/post/5c7a41b0e51d4550690476b3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值