一看你就懂,超详细java中的ClassLoader详解

本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布html

ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的很少,但对于某些框架开发者来讲却很是常见。理解ClassLoader的加载机制,也有利于咱们编写出更高效的代码。ClassLoader的具体做用就是将class文件加载到jvm虚拟机中去,程序就能够正确运行了。可是,jvm启动的时候,并不会一次性加载全部的class文件,而是根据须要去动态加载。想一想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。java

备注:本文篇幅比较长,但内容简单,你们不要恐慌,安静地耐心翻阅就是web

Class文件的认识

咱们都知道在Java中程序是运行在虚拟机中,咱们日常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如咱们编写一个简单的程序HelloWorld.java编程

public class HelloWorld{

	public static void main(String[] args){
		System.out.println("Hello world!");
	}
}

如图:
这里写图片描述
而后,咱们须要在命令行中进行java文件的编译bootstrap

javac HelloWorld.java

这里写图片描述
能够看到目录下生成了.class文件api

咱们再从命令行中执行命令:缓存

java HelloWorld

这里写图片描述

上面是基本代码示例,是全部入门JAVA语言时都学过的东西,这里从新拿出来是想让你们将焦点回到class文件上,class文件是字节码格式文件,java虚拟机并不能直接识别咱们日常编写的.java源文件,因此须要javac这个命令转换成.class文件。另外,若是用C或者PYTHON编写的程序正确转换成.class文件后,java虚拟机也是能够识别运行的。更多信息你们能够参考这篇安全

了解了.class文件后,咱们再来思考下,咱们日常在Eclipse中编写的java程序是如何运行的,也就是咱们本身编写的各类类是如何被加载到jvm(java虚拟机)中去的。微信

你还记得java环境变量吗?

初学java的时候,最惧怕的就是下载JDK后要配置环境变量了,关键是当时不理解,因此战战兢兢地照着书籍上或者是网络上的介绍进行操做。而后下次再弄的时候,又忘记了并且是必忘。当时,内心的想法很气愤的,想着是–这东西一点也不人性化,为何非要本身配置环境变量呢?太不照顾菜鸟和新手了,不少菜鸟就是由于卡在环境变量的配置上,遭受了太多的挫败感。网络

由于我是在Windows下编程的,因此只讲Window平台上的环境变量,主要有3个:JAVA_HOMEPATHCLASSPATH

JAVA_HOME

指的是你JDK安装的位置,通常默认安装在C盘,如

C:\Program Files\Java\jdk1.8.0_91

PATH

将程序路径包含在PATH当中后,在命令行窗口就能够直接键入它的名字了,而再也不须要键入它的全路径,好比上面代码中我用的到javacjava两个命令。
通常的

PATH=%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;%PATH%;

也就是在原来的PATH路径上添加JDK目录下的bin目录和jre目录的bin.

CLASSPATH

CLASSPATH=.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

一看就是指向jar包路径。
须要注意的是前面的.;.表明当前目录。

环境变量的设置与查看

设置能够右击个人电脑,而后点击属性,再点击高级,而后点击环境变量,具体不明白的自行查阅文档。

查看的话能够打开命令行窗口

echo %JAVA_HOME%

echo %PATH%

echo %CLASSPATH%

好了,扯远了,知道了环境变量,特别是CLASSPATH时,咱们进入今天的主题Classloader.

JAVA类加载流程

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外须要注意的是能够经过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。好比java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。咱们能够打开个人电脑,在上面的目录下查看,看看这些jar包是否是存在于这个目录。
  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还能够加载-D java.ext.dirs选项指定的目录。
  • Appclass Loader也称为SystemAppClass 加载当前应用的classpath的全部类。

咱们上面简单介绍了3个ClassLoader。说明了它们加载的路径。而且还提到了-Xbootclasspath-D java.ext.dirs这两个虚拟机参数选项。

加载顺序?

咱们看到了系统的3个类加载器,但咱们可能不知道具体哪一个先行呢?
我能够先告诉你答案

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

为了更好的理解,咱们能够查看源码。
sun.misc.Launcher,它是一个java虚拟机的入口应用。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }

    /* * Returns the class loader used to launch the main application. */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /* * The class loader used for loading installed extensions. */
    static class ExtClassLoader extends URLClassLoader {}

/** * The class loader used for loading from java.class.path. * runs in a restricted security context. */
    static class AppClassLoader extends URLClassLoader {}

源码有精简,咱们能够获得相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并无看见BootstrapClassLoader,但经过System.getProperty("sun.boot.class.path")获得了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

咱们能够先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

获得的结果是:

C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes

能够看到,这些全是JRE目录下的jar包或者是class文件。

ExtClassLoader源码

若是你有足够的好奇心,你应该会对它的源码感兴趣

/* * The class loader used for loading installed extensions. */
    static class ExtClassLoader extends URLClassLoader {

        static {
            ClassLoader.registerAsParallelCapable();
        }

        /** * create an ExtClassLoader. The ExtClassLoader is created * within a context that limits which files it can read */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) {
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) {
                    dirs[i] = new File(st.nextToken());
                }
            } else {
                dirs = new File[0];
            }
            return dirs;
        }
 
......
    }

咱们先前的内容有说过,能够指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里咱们经过能够编写测试代码。

System.out.println(System.getProperty("java.ext.dirs"));

结果以下:

C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext

AppClassLoader源码

/** * The class loader used for loading from java.class.path. * runs in a restricted security context. */
    static class AppClassLoader extends URLClassLoader {


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

     
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        ......
    }

能够看到AppClassLoader加载的就是java.class.path下的路径。咱们一样打印它的值。

System.out.println(System.getProperty("java.class.path"));

结果:

D:\workspace\ClassLoaderDemo\bin

这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。

好了,自此咱们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.pathjava.ext.dirsjava.class.path来加载资源文件的。

接下来咱们探讨它们的加载顺序,咱们先用Eclipse创建一个java工程。
这里写图片描述
而后建立一个Test.java文件。

public class Test{}

而后,编写一个ClassLoaderTest.java文件。

public class ClassLoaderTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
	
		ClassLoader cl = Test.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
	}

}

咱们获取到了Test.class文件的类加载器,而后打印出来。结果是:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93

也就是说明Test.class文件是由AppClassLoader加载的。

这个Test类是咱们本身编写的,那么int.class或者是String.class的加载是由谁完成的呢?
咱们能够在代码中尝试

public class ClassLoaderTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
	
		ClassLoader cl = Test.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
		cl = int.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
	}

}

运行一下,却报错了

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" java.lang.NullPointerException
	at ClassLoaderTest.main(ClassLoaderTest.java:15)

提示的是空指针,意思是int.class这类基础类没有类加载器加载?

固然不是!
int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,咱们首先得知道一个前提。

每一个类加载器都有一个父加载器

每一个类加载器都有一个父加载器,好比加载Test.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,怎么样获取呢?很简单,经过getParent方法。好比代码能够这样编写:

ClassLoader cl = Test.class.getClassLoader();
		
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());

运行结果以下:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742

这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?

System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString());

运行若是:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
java.lang.NullPointerException
	at ClassLoaderTest.main(ClassLoaderTest.java:13)

又是一个空指针异常,这代表ExtClassLoader也没有父加载器。那么,为何标题又是每个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,咱们还须要看下面的一个基础前提。

父加载器不是父类

咱们先前已经粘贴了ExtClassLoader和AppClassLoader的代码。

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

能够看见ExtClassLoader和AppClassLoader一样继承自URLClassLoader,但上面一小节代码中,为何调用AppClassLoader的getParent()代码会获得ExtClassLoader的实例呢?先从URLClassLoader提及,这个类又是什么?
先上一张类的继承关系图
这里写图片描述

URLClassLoader的源码中并无找到getParent()方法。这个方法在ClassLoader.java中。

public abstract class ClassLoader {

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    return parent;
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    return scl;
}

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //经过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
}

咱们能够看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个状况:

  1. 由外部类建立ClassLoader时直接指定一个ClassLoader为parent。
  2. getSystemClassLoader()方法生成,也就是在sun.misc.Laucher经过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader建立时若是没有指定parent,那么它的parent默认就是AppClassLoader。

咱们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,咱们上面已经粘贴过Launcher的部分代码。

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
	    //将ExtClassLoader对象实例传递进去
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

public ClassLoader getClassLoader() {
        return loader;
    }
static class ExtClassLoader extends URLClassLoader {

        /** * create an ExtClassLoader. The ExtClassLoader is created * within a context that limits which files it can read */
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            //ExtClassLoader在这里建立
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }


        /* * Creates a new ExtClassLoader for the specified directories. */
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
           
        }
        }
 }

咱们须要注意的是

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

代码已经说明了问题AppClassLoader的parent是一个ExtClassLoader实例。

ExtClassLoader并无直接找到对parent的赋值。它调用了它的父类也就是URLClassLoder的构造方法并传递了3个参数。

public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);   
}

对应的代码

public  URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
     super(parent);
}

答案已经很明了了,ExtClassLoader的parent为null。

上面张贴这么多代码也是为了说明AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null。这符合咱们以前编写的测试代码。

不过,细心的同窗发现,仍是有疑问的咱们只看到ExtClassLoader和AppClassLoader的建立,那么BootstrapClassLoader呢?

还有,ExtClassLoader的父加载器为null,可是Bootstrap CLassLoader却能够当成它的父加载器这又是为什么呢?

咱们继续往下进行。

Bootstrap ClassLoader是由C++编写的。

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的现象。具体是什么缘由,很快就知道答案了。

双亲委托

双亲委托。
咱们终于来到了这一步了。
一个类加载器查找class和resource时,是经过“委托模式”进行的,它首先判断这个class是否是已经加载成功,若是没有的话它并非本身进行查找,而是先经过父加载器,而后递归下去,直到Bootstrap ClassLoader,若是Bootstrap classloader找到了,直接返回,若是没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫作双亲委托。
整个流程能够以下图所示:
这里写图片描述
这张图是用时序图画出来的,不过画出来的结果我却本身都以为不理想。

你们能够看到2根箭头,蓝色的表明类加载器向上委托的方向,若是当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不必定是父类)进行操做,而后以此类推。直到Bootstrap ClassLoader。若是Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,若是查找成功则返回,若是没有查找成功则交给子类加载器,也就是ExtClassLoader,这样相似操做直到终点,也就是我上图中的红色箭头示例。
用序列描述一下:

  1. 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,不然委托给父加载器。
  2. 递归,重复第1部的操做。
  3. 若是ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,若是没有找到的话,就去找本身的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器本身去找。
  4. Bootstrap ClassLoader若是没有查找成功,则ExtClassLoader本身在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
  5. ExtClassLoader查找不成功,AppClassLoader就本身查找,在java.class.path路径下查找。找到就返回。若是没有找到就让子类找,若是没有子类会怎么样?抛出各类异常。

上面的序列,详细说明了双亲委托的加载流程。咱们能够发现委托是从下向上,而后具体查找过程倒是自上至下。

我说过上面用时序图画的让本身不满意,如今用框图,最原始的方法再画一次。
这里写图片描述

上面已经详细介绍了加载过程,但具体为何是这样加载,咱们还须要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。

重要方法

loadClass()

JDK文档中是这样写的,经过指定的全限定类名加载class,它经过同名的loadClass(String,boolean)方法。

protected Class<?> loadClass(String name,
                             boolean resolve)
                      throws ClassNotFoundException

上面是方法原型,通常实现这个方法的步骤是

  1. 执行findLoadedClass(String)去检测这个class是否是已经加载过了。
  2. 执行父加载器的loadClass方法。若是父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  3. 若是向上委托父加载器没有加载成功,则经过findClass(String)查找。

若是class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 咱们能够从源代码看出这个步骤。

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) {
                    	//父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                    	//父加载器为空则调用Bootstrap Classloader
                        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();
                    //父加载器没有找到,则调用findclass
                    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()
                resolveClass(c);
            }
            return c;
        }
    }

代码解释了双亲委托。

另外,要注意的是若是要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()方法,而不要直接改写loadClass()方法。
另外

if (parent != null) {
	//父加载器不为空则调用父加载器的loadClass
    c = parent.loadClass(name, false);
} else {
	//父加载器为空则调用Bootstrap Classloader
    c = findBootstrapClassOrNull(name);
}

前面说过ExtClassLoader的parent为null,因此它向上委托时,系统会为它指定Bootstrap ClassLoader。

自定义ClassLoader

不知道你们有没有发现,无论是Bootstrap ClassLoader仍是ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。若是在某种状况下,咱们须要动态加载一些东西呢?好比从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容而后再进行加载,这样能够吗?

若是要这样作的话,须要咱们自定义一个classloader。

自定义步骤

  1. 编写一个类继承自ClassLoader抽象类。
  2. 复写它的findClass()方法。
  3. findClass()方法中调用defineClass()

defineClass()

这个方法在编写自定义classloader的时候很是重要,它能将class二进制内容转换成Class对象,若是不符合要求的会抛出各类异常。

注意点:

**一个ClassLoader建立时若是没有指定parent,那么它的parent默认就是AppClassLoader。 **

上面说的是,若是自定义一个ClassLoader,默认的parent父加载器是AppClassLoader,由于这样就可以保证它能访问系统内置加载器加载成功的class文件。

自定义ClassLoader示例之DiskClassLoader。

假设咱们须要一个自定义的classloader,默认加载路径为D:\lib下的jar包和资源。

咱们写编写一个测试用的类文件,Test.java

Test.java

package com.frank.test;

public class Test {
	
	public void say(){
		System.out.println("Say Hello");
	}

}

而后将它编译过年class文件Test.class放到D:\lib这个路径下。

DiskClassLoader

咱们编写DiskClassLoader的代码。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;


public class DiskClassLoader extends ClassLoader {
	
	private String mLibPath;
	
	public DiskClassLoader(String path) {
		// TODO Auto-generated constructor stub
		mLibPath = path;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		
		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) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return super.findClass(name);
	}

	//获取要加载 的class文件名
	private String getFileName(String name) {
		// TODO Auto-generated method stub
		int index = name.lastIndexOf('.');
		if(index == -1){ 
			return name+".class";
		}else{
			return name.substring(index+1)+".class";
		}
	}
	
}

咱们在findClass()方法中定义了查找class的方法,而后数据经过defineClass()生成了Class对象。

测试

如今咱们要编写测试代码。咱们知道若是调用一个Test对象的say方法,它会输出"Say Hello"这条字符串。但如今是咱们把Test.class放置在应用工程全部的目录以外,咱们须要加载它,而后执行它的方法。具体效果如何呢?咱们编写的DiskClassLoader能不能顺利完成任务呢?咱们拭目以待。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
	
		//建立自定义classloader对象。
		DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
		try {
			//加载class文件
			Class c = diskLoader.loadClass("com.frank.test.Test");
			
			if(c != null){
				try {
					Object obj = c.newInstance();
					Method method = c.getDeclaredMethod("say",null);
					//经过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

}

咱们点击运行按钮,结果显示。

这里写图片描述

能够看到,Test类的say方法正确执行,也就是咱们写的DiskClassLoader编写成功。

回首

讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 不少同窗可能以为前面有些啰嗦,但我按照本身的思路,我以为仍是有必要的。由于我是围绕一个关键字进行讲解的。

关键字是什么?

关键字 路径

  • 从开篇的环境变量
  • 到3个主要的JDK自带的类加载器
  • 到自定义的ClassLoader

它们的关联部分就是路径,也就是要加载的class或者是资源的路径。
BootStrap ClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包。若是咱们要突破这种限制,实现本身某些特殊的需求,咱们就得自定义ClassLoader,自已指定加载的路径,能够是磁盘、内存、网络或者其它。

因此,你说路径能不能成为它们的关键字?

固然上面的只是我我的的见解,可能不正确,但现阶段,这样有利于本身的学习理解。

自定义ClassLoader还能作什么?

突破了JDK系统内置加载路径的限制以后,咱们就能够编写自定义ClassLoader,而后剩下的就叫给开发者你本身了。你能够按照本身的意愿进行业务的定制,将ClassLoader玩出花样来。

玩出花之Class解密类加载器

常见的用法是将Class文件按照某种加密手段进行加密,而后按照规则编写自定义的ClassLoader进行解密,这样咱们就能够在程序中加载特定了类,而且这个类只能被咱们自定义的加载器进行加载,提升了程序的安全性。

下面,咱们编写代码。

1.定义加密解密协议

加密和解密的协议有不少种,具体怎么定看业务须要。在这里,为了便于演示,我简单地将加密解密定义为异或运算。当一个文件进行异或运算后,产生了加密文件,再进行一次异或后,就进行了解密。

2.编写加密工具类

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;


public class FileUtils {
	
	public static void test(String path){
		File file = new File(path);
		try {
			FileInputStream fis = new FileInputStream(file);
			FileOutputStream fos = new FileOutputStream(path+"en");
			int b = 0;
			int b1 = 0;
			try {
				while((b = fis.read()) != -1){
					//每个byte异或一个数字2
					fos.write(b ^ 2);
				}
				fos.close();
				fis.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

咱们再写测试代码

FileUtils.test("D:\\lib\\Test.class");

这里写图片描述
而后能够看见路径D:\\lib\\Test.class下Test.class生成了Test.classen文件。

编写自定义classloader,DeClassLoader

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


public class DeClassLoader extends ClassLoader {
	
	private String mLibPath;
	
	public DeClassLoader(String path) {
		// TODO Auto-generated constructor stub
		mLibPath = path;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// TODO Auto-generated method stub
		
		String fileName = getFileName(name);
		
		File file = new File(mLibPath,fileName);
		
		try {
			FileInputStream is = new FileInputStream(file);
			
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			int len = 0;
			byte b = 0;
	        try {
	            while ((len = is.read()) != -1) {
	            	//将数据异或一个数字2进行解密
	            	b = (byte) (len ^ 2);
	            	bos.write(b);
	            }
	        } catch (IOException e) {
	            e.printStackTrace();
	        }
	        
	        byte[] data = bos.toByteArray();
	        is.close();
	        bos.close();
	        
	        return defineClass(name,data,0,data.length);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return super.findClass(name);
	}

	//获取要加载 的class文件名
	private String getFileName(String name) {
		// TODO Auto-generated method stub
		int index = name.lastIndexOf('.');
		if(index == -1){ 
			return name+".classen";
		}else{
			return name.substring(index+1)+".classen";
		}
	}
	
}

测试

咱们能够在ClassLoaderTest.java中的main方法中以下编码:

DeClassLoader diskLoader = new DeClassLoader("D:\\lib");
		try {
			//加载class文件
			Class c = diskLoader.loadClass("com.frank.test.Test");
			
			if(c != null){
				try {
					Object obj = c.newInstance();
					Method method = c.getDeclaredMethod("say",null);
					//经过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

查看运行结果是:

这里写图片描述

能够看到了,一样成功了。如今,咱们有两个自定义的ClassLoader:DiskClassLoader和DeClassLoader,咱们能够尝试一下,看看DiskClassLoader能不能加载Test.classen文件也就是Test.class加密后的文件。

咱们首先移除D:\\lib\\Test.class文件,只剩下一下Test.classen文件,而后进行代码的测试。

DeClassLoader diskLoader1 = new DeClassLoader("D:\\lib");
		try {
			//加载class文件
			Class c = diskLoader1.loadClass("com.frank.test.Test");
			
			if(c != null){
				try {
					Object obj = c.newInstance();
					Method method = c.getDeclaredMethod("say",null);
					//经过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
		try {
			//加载class文件
			Class c = diskLoader.loadClass("com.frank.test.Test");
			
			if(c != null){
				try {
					Object obj = c.newInstance();
					Method method = c.getDeclaredMethod("say",null);
					//经过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

运行结果:
这里写图片描述

咱们能够看到。DeClassLoader运行正常,而DiskClassLoader却找不到Test.class的类,而且它也没法加载Test.classen文件。

Context ClassLoader 线程上下文类加载器

前面讲到过Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,如今又出来这么一个类加载器,这是为何?

前面三个之因此放在前面讲,是由于它们是真实存在的类,并且听从”双亲委托“的机制。而ContextClassLoader其实只是一个概念。

查看Thread.java源码能够发现

public class Thread implements Runnable {

/* The context ClassLoader for this thread */
   private ClassLoader contextClassLoader;
   
   public void setContextClassLoader(ClassLoader cl) {
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           sm.checkPermission(new RuntimePermission("setContextClassLoader"));
       }
       contextClassLoader = cl;
   }

   public ClassLoader getContextClassLoader() {
       if (contextClassLoader == null)
           return null;
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                  Reflection.getCallerClass());
       }
       return contextClassLoader;
   }
}

contextClassLoader只是一个成员变量,经过setContextClassLoader()方法设置,经过getContextClassLoader()设置。

每一个Thread都有一个相关联的ClassLoader,默认是AppClassLoader。而且子线程默认使用父线程的ClassLoader除非子线程特别设置。

咱们一样能够编写代码来加深理解。
如今有2个SpeakTest.class文件,一个源码是

package com.frank.test;

public class SpeakTest implements ISpeak {

	@Override
	public void speak() {
		// TODO Auto-generated method stub
		System.out.println("Test");
	}

}

它生成的SpeakTest.class文件放置在D:\\lib\\test目录下。
另外ISpeak.java代码

package com.frank.test;

public interface ISpeak {
	public void speak();

}

而后,咱们在这里还实现了一个SpeakTest.java

package com.frank.test;

public class SpeakTest implements ISpeak {

	@Override
	public void speak() {
		// TODO Auto-generated method stub
		System.out.println("I\' frank");
	}

}

它生成的SpeakTest.class文件放置在D:\\lib目录下。

而后咱们还要编写另一个ClassLoader,DiskClassLoader1.java这个ClassLoader的代码和DiskClassLoader.java代码一致,咱们要在DiskClassLoader1中加载位置于D:\\lib\\test中的SpeakTest.class文件。

测试代码:

DiskClassLoader1 diskLoader1 = new DiskClassLoader1("D:\\lib\\test");
Class cls1 = null;
try {
//加载class文件
 cls1 = diskLoader1.loadClass("com.frank.test.SpeakTest");
System.out.println(cls1.getClassLoader().toString());
if(cls1 != null){
	try {
		Object obj = cls1.newInstance();
		//SpeakTest1 speak = (SpeakTest1) obj;
		//speak.speak();
		Method method = cls1.getDeclaredMethod("speak",null);
		//经过反射调用Test类的speak方法
		method.invoke(obj, null);
	} catch (InstantiationException | IllegalAccessException 
			| NoSuchMethodException
			| SecurityException | 
			IllegalArgumentException | 
			InvocationTargetException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
	
DiskClassLoader diskLoader = new DiskClassLoader("D:\\lib");
System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
new Thread(new Runnable() {
	
	@Override
	public void run() {
		System.out.println("Thread "+Thread.currentThread().getName()+" classloader: "+Thread.currentThread().getContextClassLoader().toString());
		
		// TODO Auto-generated method stub
		try {
			//加载class文件
		// Thread.currentThread().setContextClassLoader(diskLoader);
			//Class c = diskLoader.loadClass("com.frank.test.SpeakTest");
			ClassLoader cl = Thread.currentThread().getContextClassLoader();
			Class c = cl.loadClass("com.frank.test.SpeakTest");
			// Class c = Class.forName("com.frank.test.SpeakTest");
			System.out.println(c.getClassLoader().toString());
			if(c != null){
				try {
					Object obj = c.newInstance();
					//SpeakTest1 speak = (SpeakTest1) obj;
					//speak.speak();
					Method method = c.getDeclaredMethod("speak",null);
					//经过反射调用Test类的say方法
					method.invoke(obj, null);
				} catch (InstantiationException | IllegalAccessException 
						| NoSuchMethodException
						| SecurityException | 
						IllegalArgumentException | 
						InvocationTargetException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		} catch (ClassNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}).start();

结果以下:
这里写图片描述

咱们能够获得以下的信息:

  1. DiskClassLoader1加载成功了SpeakTest.class文件并执行成功。
  2. 子线程的ContextClassLoader是AppClassLoader。
  3. AppClassLoader加载不了父线程当中已经加载的SpeakTest.class内容。

咱们修改一下代码,在子线程开头处加上这么一句内容。

Thread.currentThread().setContextClassLoader(diskLoader1);

结果以下:
这里写图片描述

能够看到子线程的ContextClassLoader变成了DiskClassLoader。

继续改动代码:

Thread.currentThread().setContextClassLoader(diskLoader);

结果:
这里写图片描述

能够看到DiskClassLoader1和DiskClassLoader分别加载了本身路径下的SpeakTest.class文件,而且它们的类名是同样的com.frank.test.SpeakTest,可是执行结果不同,由于它们的实际内容不同。

Context ClassLoader的运用时机

其实这个我也不是很清楚,个人主业是Android,研究ClassLoader也是为了更好的研究Android。网上的答案说是适应那些Web服务框架软件如Tomcat等。主要为了加载不一样的APP,由于加载器不同,同一份class文件加载后生成的类是不相等的。若是有同窗想多了解更多的细节,请自行查阅相关资料。

总结

  1. ClassLoader用来加载class文件的。
  2. 系统内置的ClassLoader经过双亲委托来加载指定路径下的class和资源。
  3. 能够自定义ClassLoader通常覆盖findClass()方法。
  4. ContextClassLoader与线程相关,能够获取和设置,能够绕过双亲委托的机制。

下一步

  1. 你能够研究ClassLoader在Web容器内的应用了,如Tomcat。
  2. 能够尝试以这个为基础,继续学习Android中的ClassLoader机制。

引用

我这篇文章写了好几天,修修改改,而后加上本身的理解。参考了下面的这些网站。

  1. grepcode ClassLoader源码
  2. http://blog.csdn.net/xyang81/article/details/7292380
  3. http://blog.csdn.net/irelandken/article/details/7048817
  4. https://docs.oracle.com/javase/7/docs/api/java/net/URLClassLoader.html

若是以为文章有所帮助,能够点击这行字给我博客投票,046 号

这里写图片描述