类加载与双亲委派

我们回想一下最初学java写Hello World程序时是怎样的过程呢。

首先我们要写一段代码放在Hello.java的文本文件中,之后通过javac命令或是IDE build将Hello.java文件编译成Hello.class的字节码文件。至此,我们的硬盘上有两个文件,后缀名分别是java和class。最后我们通过java命令或是IDE运行main方法来执行获得我们想要的结果,而启动JVM后是如何将相关的.class文件加载到JVM内存中的呢,这就是我们要讨论的内容。

类加载与双亲委派

我们知道sun.misc.Launcher是程序的入口(这个包下的类,JDK中是没有源码的,有兴趣的可以去openjdk中查找),现在我们看一下Launcher是如果装配类加载器的。代码略多我们把安全检查和异常处理等等都删掉精简一下,只贴出核心部分。

 1 /**
 2  * This class is used by the system to launch the main application.
 3 Launcher */
 4 public class Launcher {
 5     private static URLStreamHandlerFactory factory = new Factory();
 6     private static Launcher launcher = new Launcher();
 7     private static String bootClassPath =System.getProperty("sun.boot.class.path");
 8     private ClassLoader loader;
 9 
10     public Launcher() {
11         // Create the extension class loader
12         ClassLoader extcl;
13         extcl = ExtClassLoader.getExtClassLoader();
14 
15         // Now create the class loader to use to launch the application
16         loader = AppClassLoader.getAppClassLoader(extcl);
17 
18         // Also set the context class loader for the primordial thread.
19         Thread.currentThread().setContextClassLoader(loader);
20     }
21 
22     /*
23      * The class loader used for loading installed extensions.
24      */
25     static class ExtClassLoader extends URLClassLoader {
26         /**
27          * create an ExtClassLoader. The ExtClassLoader is created
28          * within a context that limits which files it can read
29          */
30         public static ExtClassLoader getExtClassLoader() throws IOException
31         {
32             final File[] dirs = getExtDirs();
33             return new ExtClassLoader(dirs);
34         }
35          //Creates a new ExtClassLoader for the specified directories.
36         public ExtClassLoader(File[] dirs) throws IOException {
37             super(getExtURLs(dirs), null, factory);
38         }
39 
40         private static File[] getExtDirs() {
41             String s = System.getProperty("java.ext.dirs");
42             File[] dirs;
43             if (s != null) {
44                 StringTokenizer st =
45                     new StringTokenizer(s, File.pathSeparator);
46                 int count = st.countTokens();
47                 dirs = new File[count];
48                 for (int i = 0; i < count; i++) {
49                     dirs[i] = new File(st.nextToken());
50                 }
51             } else {
52                 dirs = new File[0];
53             }
54             return dirs;
55         }
56     }
57 
58     /**
59      * The class loader used for loading from java.class.path.
60      * runs in a restricted security context.
61      */
62     static class AppClassLoader extends URLClassLoader {
63         public static ClassLoader getAppClassLoader(final ClassLoader extcl)
64             throws IOException
65         {
66             final String s = System.getProperty("java.class.path");
67             final File[] path = (s == null) ? new File[0] : getClassPath(s);
68             return new AppClassLoader(urls, extcl);
69         }
70         //Creates a new AppClassLoader
71         AppClassLoader(URL[] urls, ClassLoader parent) {
72             super(urls, parent, factory);
73         }
74     }
75 }

在构造方法Launcher中我们发现了两个变量extcl和loader,并且调用了两个静态内部类ExtClassLoader和AppClassLoader,这两个静态内部类同样继承自URLClassLoader,我们进到URLClassLoader中可以看到,URLClassLoader继承自SecureClassLoader,

类加载与双亲委派

我们继续深入,SecureClassLoader继承了ClassLoader

类加载与双亲委派

我们跟踪到最后发现ClassLoader是一个抽象类

类加载与双亲委派

至此,主角终于出现,ClassLoader。我们先看一下刚才几个类之间的关系。

类加载与双亲委派

我们回到代码中,extcl和loader两个变量的类型是ClassLoader类,没有问题。ExtClassLoader和AppClassLoader两个类的构造方法都一样super了父类构造器,分别是super(getExtURLs(dirs), null, factory) 和super(urls, parent, factory) ,第一个参数是加载路径下的jar,第二个参数是设置它的父加载器。

类加载与双亲委派

类加载与双亲委派

我们先看怎么得到extdir的,System.getProperty("java.ext.dirs"),这是什么呢? 先测试一下

类加载与双亲委派

原来ExtClassLoader加载的是我们的扩展类,但它的父加载器为什么设置成null呢?(super(getExtURLs(dirs), null, factory))这个我们稍后会说。

再看AppClassLoader,同样我们打印下java.class.path

类加载与双亲委派

 得到的是我们项目下的引用的类。

我们现在返回看Launcher构造方法

   public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        extcl = ExtClassLoader.getExtClassLoader();

        // Now create the class loader to use to launch the application
        loader = AppClassLoader.getAppClassLoader(extcl);

        // Also set the context class loader for the primordial thread.
        Thread.currentThread().setContextClassLoader(loader);
    }

它做了哪几件事呢?

1.获取扩展类加载器ExtClassLoader,它的父加载器是Null

2.获取应用类加载器AppClassLoader,它的父加载器是ExtClassLoader

3.设置上下文类加载器是AppClassLoader

为什么ExtClassLoader父加载器是Null呢?应该是BootstrapClassLoader才对。

其实BootstrapClassLoader是由C/C++来实现的,加载如rt.jar等sun.boot.class.path下的文件。在Launcher类中我们同样能够发现静态变量bootClassPath。

好,我们梳理一下这三个加载器的关系。

类加载与双亲委派

现在我们已经进行了一半,那么接下来什么是双亲委派呢?

 先来看一下ClassLoader是怎么加载类的

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 先判断是否加载过
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
               // 递归调用父类加载器 c
= parent.loadClass(name, false); } else {
              // 父类加载器是空,找BootStrapClassLoader c
= findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); 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; } }

我们可以清楚的知道三种类加载器的调用关系

类加载与双亲委派

这就是双亲委派。

真正加载类的方法是findClass(),loadClass()主要来查看缓存中是否有要加载的类并实现双亲委托代理给父加载器。findClass()是ClassLoader中的抽象方法,如果我们要写一个自己的类加载器,需要实现findClass方法来读取自定义资源下的class并调用defineClass()把字节码文件转成类对象。

现在我们写一个自己的加载器

public class MyClassLoader extends ClassLoader{
    private String path;
    public MyClassLoader(String path){
        this.path=path;
    }
    @Override
    public Class<?> findClass(String name){
        byte[] data = getData(name);
        return defineClass(name, data, 0, data.length);
        
    }
    public byte[] getData(String name){
        String filePath = path + File.separatorChar+name.replace('.', File.separatorChar)+".class";
        File file = new File(filePath);
        try {
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream  bos = new ByteArrayOutputStream ();
            int len=0;
            while((len=is.read())!=-1){
                bos.write(len);
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return data;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return null;
    }
}

我们写一个Hello World程序,编译完成后放在E://下

package com.src;
public class Hello {
    public void hi(){
        System.out.println("Hello World");
    }
}

写一个执行类,用MyClassLoader来加载E:comsrc下的Hello.class

public class Test {
    public static void main(String[] args) {
        MyClassLoader mcl = new MyClassLoader("E:");
        try {
            Class c = mcl.loadClass("com.src.Hello");
            Object object = c.newInstance();
            c.getDeclaredMethod("hi", null).invoke(object, null);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

运行成功

类加载与双亲委派

更进一步 我们可以加载服务器上或者网络接收的文件,甚至为了安全可以加密文件,然后在自己的加载器上实现解密后加载。

我们知道了类是如何被加载的,甚至可以写一个简单的自定义的加载器,现在我们来思考一个问题,问什么要用双亲委托这种代理模式来加载类呢?

需要知道,同样的class文件被不同的加载器加载,JVM认为这是两个类。双亲委托保证了java核心类库的安全性和唯一性。比如我们用到的String类,如果被不同的加载器加载, 会生成多个版本的String类。

做一个有趣的实验,如果我们需要打印当前时间的话,经常会用到这行代码

System.out.println(new Date());

在Date类中,重写了toString()方法,让它返回当前时间。

我们写一个自己的Date类,放在java.util下

package java.util;
public class Date {
    @Override
    public String toString(){
        return "My Date";
    }
}

用我们之前写的自定义加载器来加载它

public class Test {
    public static void main(String[] args) {
        MyClassLoader mcl = new MyClassLoader("E:");
        try {
            Class c = mcl.loadClass("java.util.Date");
            Object object = c.newInstance();
            System.out.println(object);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

看下输出

类加载与双亲委派

看来加载的并不是我们自己的Date类。因为loadClass方法根本不会去找我们自己的Date类,但是如果MyClassLoader不去调用loadClass方法 直接用findClass跳过缓存查找和双亲委托呢?

Class c = mcl.findClass("java.util.Date");

运行结果

类加载与双亲委派

以java开始的包名是被禁止的,所以说我们想要写一个自己的Date类来取代核心类库中的Date类是不被允许的。

在实际应用中,双亲委派并不能完美解决所有问题。Java为了和第三方应用对接,提供了很多SPI(服务提供接口),如JDBC。在核心类库中声明了这些接口的定义,但是实现类并不在核心类库中,如我们mysql jar包是放在项目classpath下的,这就出现了一个问题,接口是有bootstrap Classloader来加载但是它本身并不能加载到实现类。为了解决这个问题,于是就有了上下文类加载器。在Launcher中我们定义了上下文类加载器是App classloader。