当前位置:首页 » JAVA技术教程

Java的类加载器ClassLoader

2018-02-10 16:01 本站整理 浏览(1)

前言

 

ClassLoader java的类加载器,其作用就是把编译好的class文件或者jar包中对应的类的元数据加载到jvm的方法区,在堆中创建一个Class对象并返回,调用这个这个Class对象的newInstance()方法就可以创建一个指定类对应的对象。熟悉反射的朋友应该很清楚,如果再在前面加一段通过Class.forName()方法来获取Class对象的话,这其实就是java“反射”获取对象的实现过程。

 

Class.forName()方法会调用forName0方法,这是一个jvm实现的native方法,里有一个重要的参数就是ClassLoader对象。Class.forName()方法是显示的获取一个Class对象的方法,jvm会调用根据参数里的ClassLoader对象来查询并加载一个Class对象。

 

Class.forName()方法是一种显示的获取Class对象的方法。这种方式一般是通过反射来创建java对象。我们平时用得更多的是使用new关键字来创建java对象,这个过程一个隐式创建java对象的过程:首先jvm会找到当前上下文的ClassLoader对象,通过这个ClassLoader对象来Load并创建一个Class对象;再通过Class对象的newInstance()方法实例化一个类对象。这个过程完全就jvm自己完成,所以称为“隐式加载”。通过这个过程,我们还可以发现,jvm是按需加载的,也就是说在new关键字被调用后,才会去加载对应的Class对象。否则如果一上来就把所有的jar包都加载到内存,势必一种内存空间的浪费;同时程序启动时间也会变长。

 

java自带的ClassLoader对象的创建过程

 

提示:本文中贴出的源码都是基于JDK1.8。

ClassLoader在java中定义的是一个抽象类,也说真实的ClassLoader对象是其子类对象。ClassLoader类中定义了一些通用的方法,比如需要一个ClassLoader时可以通过getSystemClassLoader()方法:

public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader(); //初始化ClassLoader对象
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {//权限检查
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }
 

 

这个方法没什么好说的关键就是调用initSystemClassLoader()方法 对ClassLoader对象进行初始化。initSystemClassLoader这方法不多说,其中核心部分是创建了一个Launcher对象:

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

 

java自带的类加载器对象就是在Launcher类中创建的。下面我们主要来看下Launcher类,这个类从整体上来看运用了一个 “类单例模式”,大致结构如下:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private ClassLoader loader;
    //省略其他成员变量
 
    public static sun.misc.Launcher getLauncher() {
        return launcher;
    }
    //公有的构造方法,与典型的单例模式有点差别
    public Launcher() {
        //省略方法体
}
//省略其他方法
}
 

 

这是一种典型的饿汉式“单例模式”(除了构造方法是public),在ClassLoad的initSystemClassLoader()方法中就是通过getLauncher()方法来获取Launcher对象的,可以看到通过这个方法获取的对象,每次都是同一个。

 

Launcher的构造方法做了三件事:创建ExtClassLoader类加载器对象;创建AppClassLoader类加载器对象;创建SecurityManager安全策略对象。关于java的安全策略这里就不展开了,感兴趣的可以异步到这里https://www.cnblogs.com/yiwangzhibujian/p/6207212.html。下面是该构造方法源码:

 

public Launcher() {
        //创建ExtClassLoader类加载器对象
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
 
        //创建AppClassLoader类加载器对象
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //把AppClassLoader类加载器对象设置到上线文
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
       
        //创建SecurityManager安全策略
        if(var2 != null) {
            SecurityManager var3 = null;
            if(!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                    ;
                } catch (InstantiationException var6) {
                    ;
                } catch (ClassNotFoundException var7) {
                    ;
                } catch (ClassCastException var8) {
                    ;
                }
            } else {
                var3 = new SecurityManager();
            }
 
            if(var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }
 
            System.setSecurityManager(var3);
        }
 
    }
 

 

可以看到ExtClassLoader、AppClassLoader是二者都是Launcher的内部类,并且都继承自URLClassLoader;而URLClassLoader又都继承自前面提到的抽象类ClassLoader。二者的区别是AppClassLoader的父加载器是ExtClassLoader,而ExtClassLoader的父加载器为空(AppClassLoader的构造方法需要一个ExtClassLoader对象)。

 

Launcher的构造方法首次执行后,Launcher的单例对象就创建完成。下面继续返回ClassLoader抽象类。

 

当需要加载一个Class对象时:

1、首先通过ClassLoader的getSystemClassLoader()方法获取到到当前的ClassLoader对象(AppClassLoader对象);

2、然后调用该ClassLoader对象的LoadClass方法loadClass()方法进行加载。

下面就来看LoadClass方法的实现流程:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查该Class对象是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果有父加载器就交给父加载器加载
                    //否则就交给BootstrapClassLoader 加载器加载
                    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
                }
                //如果父加载器 以及BootstrapClassLoader加载器都没有加载,就自己加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //自己加载,如果加载不到就抛出ClassNotFoundException异常。
                    //该方式收抽象方法,交给子类实现
                    c = findClass(name);
 
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
 

 

LoadClass方法的核心流程如下:

1、首先通过findLoadedClass方法,检查自己是否已经加载过该class对象。最终调用的是native方法findLoadedClass0进行检查,由于是jvm实现的看不到源码。可以猜测到已经该类加载器 加载过的class对象,应该是被缓存起来了。这里还可以猜测到“类加载器”和class对象之前相互持有对方的引用,这也就是为什么class对象 以及方法区很难被“垃圾回收”的原因,除非首先想办法回收掉“类加载器”或者切断二者之间的引用关系(要自己去实现很复杂,OSGI在回收Bundle时,应该就是这么做的)。

 

2、类加载器,首先不自己加载,而是交给自己的父类加载器加载。从前面的讲解我们可以得知getSystemClassLoader()方法获取的真实的ClassLoader是AppClassLoader,其父类加载器是ExtClassLoader;父加载器ExtClassLoader又会重复步骤1检查自己是否已经加载过,否则到步骤2,此时ExtClassLoader的父加载器为空,就会交给BootstrapClassLoader加载器加载。

 

3、如果步骤2中,父加载器ExtClassLoader、以及BootstrapClassLoader加载器都没有加载,则自己调用findClass方法进行加载。

 

在上述三个步骤中提到的三个类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader,下一节再详细说明这三者的关系。先来看下findClass方法,在ClassLoader中是抽象方法,在UrlClassLoader中进行了实现,内容就比粘贴了,大致就是URI路径找到class文件,然后调用自己defineClass方法进行取去文件内容到ByteBuffer中,最后再调用父类ClassLoader中的defineClass方法进行加载,该方式是native的,由jvm实现。简易序列图如下:


 

简单的理解就是 loadClass方法-->调用findClass方法-->调用defineClass方法

 

findClass方法一般交由子类子类字节去实现从哪里找“class”文件,UrlClassLoader的实现是在一个目录下找。当然也可以在网络中去找,只要把文件内容读取到ByteBuffer中就行。

 

至此关于类加载器如何加载一个Class对象的流程就分析完毕。

 

java中自带的三个类加载器

 

在上一节中,可以看到java中自带的三个类加载器:BootstrapClassLoader、ExtClassLoader、AppClassLoader。准确的说BootstrapClassLoader应该是JVM自带的类加载器,ExtClassLoader、AppClassLoader是jdk api中定义的类加载器,他们都是ClassLoader的间接子类,并且是Launcher的内部类。

 

三个类加载器的最大区别就是 他们各自加载类的路径不同,也就是我们常说的ClassPath。它们的类路径在Launcher类中都可以看到:

 

BootstrapClassLoader:读取系统配置sun.boot.class.path,默认配置路径为:System.out.println(System.getProperty("sun.boot.class.path"));

 

ExtClassLoader:读取系统配置java.ext.dirs,默认配置路径为:System.out.println(System.getProperty("java.ext.dirs"));

 

AppClassLoader:读取系统配置java.class.path,默认配置路径为:System.out.println(System.getProperty("java.class.path"));

 

执行上面的System.out.println就可以看到你电脑中,三个类加载器的加载路径。简单的理解:

BootstrapClassLoader加载的是java的核心包,比如rt.jar。

ExtClassLoader加载的是扩展包 $JAVA_HOME/ lib/ext

AppClassLoader加载的是自己应用开发的jar包(或者class文件)。

 

java 类加载器的父优先原则

 

从第二节中,我们还可以看到java类加载器的父优先原则 即:在需要加载一个Class对象时,AppClassLoader判断自己是否加载过,如果没有,自己先不加载,而是交给ExtClassLoader;ExtClassLoader判断自己是否加载过,如果没有就交给BootstrapClassLoader进行加载。

 

反过来,如果BootstrapClassLoader发现这个class文件不在自己的类路径下,就交给ExtClassLoader;ExtClassLoader尝试在自己的类路径下找,如果还是没有找到,就交给AppClassLoader;AppClassLoader尝试在自己的类路径下找,如果还是没有找到,就抛出ClassNotFoundException。在上述三步中的任意一步中 如果找到,就由该类加载器加载,并被缓存起来。这就是java 类加载器的父优先原则(也有称做:双亲委派模型)。

 

为什么java的类加载要采用父优先原则呢?其中一个最重要的目的就是为了防止我们重新java中定义的类。比如:你在自己的项目中定义了一个java.lang.String类,在运行时是永远都加载不到你自己这个类的,因为每次加载时都由BootstrapClassLoader类加载器 加载自己类路径下的java.lang.String类。

 

自定义类加载器

 

通过前面几节的讲解,相信都对java的类加载器有了大致的了解,如果我们想定义自己的类加载器也是件很容易的时。无非就是继承ClassLoader类,重新其findClass方法,也就是指定类加载器到哪里去读Class文件。当然还有一个跟简单的方法就是,仿照ExtClassLoader和AppClassLoader,直接继承UrlClassLoader。这里我们采用后者来展示一个最简单的示例:

public class MyClassLoader extends URLClassLoader{
    public MyClassLoader(URL[] urls,ClassLoader parent) {
        super(urls,parent);
    }
}
 
写个main方法测试下:
public static void main(String[] args) throws Exception{
        URL temp = new URL("file:///D:/test/");
        URL [] urls = {temp};
        MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent());
        Class c1 = myClassLoader.loadClass("com.sky.main.Hello");
        Object obj1 = c1.newInstance();
        System.out.println("obj1类名:"+obj1.getClass().getName());
        Method sayhello = c1.getMethod("sayHello");
        sayhello.invoke(obj1);//调用该对象的sayhello方法
 
        System.out.println("##################");
 
        Hello obj2 = new Hello();
        System.out.println("obj2类名:"+obj2.getClass().getName());
 
        if(obj1 instanceof Hello){
            System.out.println("obj1是com.sky.main.Hello类型");
        }else{
            System.out.println("obj1不是com.sky.main.Hello类型");
        }
 
    }
 

 

这里指定了自定义MyClassLoader的类路径为“D:/test/”目录,同时设定了他的父类加载器为ExtClassLoader。另外还创建了一个Hello类,这里首先使用ExtClassLoader进行显式的加载;然后是又在mian方法中创建了一个Hello类的对象,这里会使用默认的AppClassLoader再次加载Hello类。Hello类很简单:

package com.sky.main;
 
/**
 * Created by gantianxing on 2018/2/10.
 */
public class Hello {
    public void sayHello(){
        System.out.println("hello,xiaoming");
    }
}
 

 

我们把编译好的Hello.class放到“D:\test\com\sky\main\”目录下,即可运行main方法了,打印信息为:

obj1类名:com.sky.main.Hello
hello,xiaoming
##################
obj2类名:com.sky.main.Hello
obj1不是com.sky.main.Hello类型

是不是发现了一个很奇怪的问题:使用我们自定义的ClassLoader加载的obj1对象的类名是com.sky.main.Hello,但instanceof检测时却发现不是com.sky.main.Hello类型。这是因为这里分别使用了两个类加载器,加载Hello类。

 

感兴趣的朋友还可以把上面测试代码的MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent());这行代码改为:

MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader());

 

即把MyClassLoader的父类加载器设置为AppClassLoader,再运行一次,运行结果会不同。具体为什么呢?答案就是java类加载器的父优先原则,自己摸索下。

 

这里的自定义ClassLoader很简单,当然你还可以根据自己的需要重写findClass方法。

 

总结

 

本文首先讲解了什么是类加载器,以及java的类加载器是如何创建的,并对整个创建过程,以及类加载过程源码进行了分析。然后讲解了java自带类加载器的父优先原则。最后展示了如何创建自己的类加载器。

 

创建自定义类加载器的,可以在运行时实时的加载新的class对象,并且可以通过一定的手段卸载指定的class对象,从而实现class对象的热更新。但要实现对一个jar包的热更新,使用自定义ClassLoader来实现就显得非常复杂,因为你不知道哪些Class已经加载,也就不知道要卸载哪些Class对象,除非把整个ClassLoader先卸载。这是一个复杂的过程,庆幸的是OSGI已经帮我们做了这些事情,如果想实现对jar包的热更新,选择OSGI就行了。