Java 拾遗一 『类加载器』

自言自语
说来惭愧 ,项目都写了好几个,Java底层稍深一点的内容、数据结构、算法,半知半解..





前言

本文试图在Java代码层面讲讲,Java类加载器是如何加载一个1,以及其中的过程,希望对在看的你有所帮助,但更重要的是对我有帮助-.-

如果你发现有错误的地方,还请指正,以免误人误己,非常感谢。

类加载器(ClassLoader)


一个.class字节码,在计算机上本质上就是一个文件,一份二进制流,如何把这份二进制流验证、解析,成为一个java.lang.Class的内存模型,可以被实例化的对象,类加载器(ClassLoader)的工作就是第一步;JVM内部默认有三个类加载器,来介绍一下。


JVM默认的三个类加载器:

  1. Bootstrap ClassLoader (引导类加载器)

      使用C/C++编写,属于JVM的一部分,随虚拟机启动而加载,不能在Java中实例化,它负责加载java的核心类库,默认加载%JAVA_HOME%/jre/lib/*目录下的jar包 2 (当然并不是什么包都加载,按照名称识别) 例如:rt.jar、jsse.jar、jce.jar

  2. ExtClassLoader (扩展类加载器)

      使用Java实现,默认它加载%JAVA_HOME%/jre/lib/ext/*目录下的所有类库 注意,是所有jar包,而且在这里放入jar包,优先覆盖掉项目引用同样的包。(看完本文你会明白)

  3. AppClassLoader (应用类加载器)

      又叫系统类加载器(SystemClassLoader),使用Java实现,默认它加载%CLASSPATH%目录下的类库,以及自身应用的所有类、类库。

   注意,以上三个类加载器的加载路径均使用了默认字眼,可在JVM虚拟机的启动参数中指定它们的加载路径。

blablabla..说了这么多,何以证明?

代码证明

  来,打开eclipase新建test项目,代码走一波。刚好有mysql的驱动包,顺便引到项目上去,show package explorer 视图,点开JRE system library,各jar包的位置一目了然,看图
Java 拾遗一 『类加载器』_第1张图片

测试代码:
代码class.getClassLoader()可以获得它的加载器

public class ClassLoaderTest {
    public static void main(String[] args) {

        //UserBean类,它属于应用自身的类。
        System.out.println(UserBean.class.getClassLoader());

        //mysql-connector-java-5.1.42-bin.jar 的com.mysql.fabric.FabricConnection类,它属于应用类库。 
        System.out.println( FabricConnection.class.getClassLoader());

        //cldrdata.jar 下的 sun.text.resources.cldr.FormatData类, 它在%JAVA_HOME%/jre/lib/ext 目录下。
        System.out.println( FormatData.class.getClassLoader());

        //rt.jar 下的 java.lang.String类,它在%JAVA_HOME%/jre/lib/ 目录下。
        System.out.println(String.class.getClassLoader());
    }
}
class UserBean{
}

运行,不出意外,会类似打印出:

…AppClassLoader@hashcode
…AppClassLoader@hashcode
…ExtClassLoader@hashcode
…BootstrapClassLoader@hashcode

实际输出:

Java 拾遗一 『类加载器』_第2张图片

呃…实际输出 Bootstrap ClassLoader 为”null”,看上面关于它的介绍,使用纯C/C++编写的,它没有对应java.lang.Class 模型,所以无法被引用访问,实例化..请正确理解这个“null”

疑问

  相信好奇的同学都跟我一样有疑问

  1. 类加载器自己本身何时何地被加载?
  2. 它们之间又有什么关系?
  3. 会不会重复加载到类?如果不会,加载类的时候有什么机制? 如果会,为什么?

OK,有疑问就去探寻答案,一个一个来


疑问解决1 : 类加载器的加载

  上面输出图,有个$符号,说明这两个类加载器,是sun.misc.Latncher的内部类,那就找这个Launcher源码看看呗。

然而很遗憾Oracle JDK不开源,并没有sun.misc.Latncher的源码,但是可以参考Open JDK 8 Latncher的源码

了解一下sun.misc.Latncher类的作用

看它的注释:

 /**
 * This class is used by the system to launch the main application.Launcher 
 * (系统通过使用此类来启动主应用程序)                                                    [渣英文=.=]
 * */
public class Launcher {
....

说明它是系统启动时的一个入口类,JVM启动时会从这里开始运行代码,被引导类加载器加载,位于rt.jar。


继续研究 sun.misc.Latncher 源码,在Latncher 的构造方法可以看到:

...
67     public Launcher() {
68         // Create the extension class loader
69         ClassLoader extcl;
70         try {
71             extcl = ExtClassLoader.getExtClassLoader();
72         } catch (IOException e) {
73             throw new InternalError(
74                 "Could not create extension class loader", e);
75         }
76 
77         // Now create the class loader to use to launch the application
78         try {
79             loader = AppClassLoader.getAppClassLoader(extcl);
80         } catch (IOException e) {
81             throw new InternalError(
82                 "Could not create application class loader", e);
83         }
84 
...

}

AppClassLoader (应用类加载器)

   关键第79行:loader = AppClassLoader.getAppClassLoader(extcl);

sun.misc.Latncher中实现并通过给定扩展类加载器的引用创建;

AppClassLoader 的构造方法:

..掐头..
292        /*
293         * Creates a new AppClassLoader
294         */
295        AppClassLoader(URL[] urls, ClassLoader parent) {
296            super(urls, parent, factory);
..去尾..

( parent 即是extcl的参数传递名)

继承链是:

AppClassLoader 继承自 URLClassLoader继承自 SecureClassLoader继承自 ClassLoader

直捣黄龙:
    ClassLoader构造源码:

279    private ClassLoader(Void unused, ClassLoader parent) {
280         this.parent = parent;

AppClassLoader 有一个 parent 变量引用指向 ExtClassLoader

好,来研究 ExtClassLoader.



ExtClassLoader (扩展类加载器)

   第71行: extcl = ExtClassLoader.getExtClassLoader();
同样在sun.misc.Latncher中实现并创建,同样的继承链:
ExtClassLoader 继承自 URLClassLoader 继承自 SecureClassLoader 继承自 ClassLoader

ExtClassLoader 的构造方法:

160        /*
161         * Creates a new ExtClassLoader for the specified directories.
162         */
163        public More ...ExtClassLoader(File[] dirs) throws IOException {
164            super(getExtURLs(dirs), null, factory);

对应的 URLClassLoader构造方法:

185
186    public URLClassLoader(URL[] urls, ClassLoader parent,
187                          URLStreamHandlerFactory factory) {
188        super(parent);

ExtClassLoader parentnull



Bootstrap ClassLoader (引导类加载器)
  正如前面关于它的介绍,随虚拟机启动而加载,纯C/C++编写,只要虚拟机成功启动它便已存在,它在Java层面是“null

稍微有点想象力,应该可以想象出来。

应用类加载器扩展类加载器 加载,而扩展类加载器引导类加载器 加载

上面的代码改一下可以证明:

//UserBean类,它属于应用自身创建类。
System.out.println(UserBean.class.getClassLoader());

System.out.println(UserBean.class.getClassLoader().getParent());

System.out.println(UserBean.class.getClassLoader().getParent().getParent());

输出:


解决疑问2:关联关系

是的,单纯从设计层面来讲,它们都可以独立、非依赖、组合,而是关联关系;应用类加载器 和 扩展类加载器,最终都继承自ClassLoader 抽象类

ClassLoader有个ClassLoader parent;成员变量,AppClassLoader 的 parent 是 ; ExtClassLoader 的 parentnull

那么问题来了,ExtClassLoader 的 parentnull;那它怎么被 Bootstrap ClassLoader加载?



解决疑问3:双亲委托

直接看 ClassLoader这个抽象类的源码;

重点关注它加载类的loadClass();方法,应用类加载器和扩展类加载器都没有覆盖该方法。

...
401     protected Class loadClass(String name, boolean resolve)
402         throws ClassNotFoundException
403     {
404         synchronized (getClassLoadingLock(name)) {
405             // First, check if the class has already been loaded
406             Class c = findLoadedClass(name);
407             if (c == null) {
408                 long t0 = System.nanoTime();
409                 try {
410                     if (parent != null) {
411                         c = parent.loadClass(name, false);
412                     } else {
413                         c = findBootstrapClassOrNull(name);
414                     }
415                 } catch (ClassNotFoundException e) {
416                     // ClassNotFoundException thrown if class not found
417                     // from the non-null parent class loader
418                 }
419 
420                 if (c == null) {
421                     // If still not found, then invoke findClass in order
422                     // to find the class.
423                     long t1 = System.nanoTime();
424                     c = findClass(name);
425 
426                     // this is the defining class loader; record the stats
427                     sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
428                     sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
429                     sun.misc.PerfCounter.getFindClasses().increment();
430                 }
431             }
432             if (resolve) {
433                 resolveClass(c);
434             }
435             return c;
436         }
437     }
...

关键410行:
如果 parent 不为null ,调用parent 的loadClass

否则findBootstrapClassOrNull(name);给引导类加载器加载;

如果引导类加载器都加载不了,第424行调用 findClass尝试加载。

而findClass 默认实现是抛出ClassNotFoundException异常:

529     protected Class findClass(String name) throws ClassNotFoundException {
530         throw new ClassNotFoundException(name);
531     }

下见流程图,简单明了。
Java 拾遗一 『类加载器』_第3张图片

到这里已经可以解答 ExtClassLoader 的 parentnull的疑问,类加载器只要它的parentnull,就会委托给引导类加载器加载。

有人认为基于这种双亲委托机制,是为了避免加载到跟java核心类同名的字节码(例如:java.lang.String),从而损害虚拟机的安全,其实不然,ClassLoader 的loadClass();方法并不是final 的,可以继承后覆盖它,破坏这种委托机制;

这种机制主要目的是避免重复加载,当需要加载一个Class 优先委托给 parent加载,这样上层的ClassLoader 加载过,可以直接返回,遍历所有缓存;而不是自己直接去加载;尴尬造成虚拟机存在两份同样的Class,省去加载过程、内存损耗。

如果需要自定义自己的类加载,应该继承自ClassLoader给定它的 parent是AppClassLoader覆盖 findClass(); 在里面定义自己获取Class的二进制,保持这种委托机制。

THE END


  1. 这里的类,泛指可能是Java 实现的类或者接口。事实上JVM虚拟机能运行,不止是Java编译的class,任何语言编写,编译的字节码,只要符合虚拟机的规范都能在被加载运行,例如: Scala,JRuby.. ↩
  2. 本文探讨的是1.8.0_151版本,每个版本默认目录可能会有所不同,例:jdk9,jre目录已被移除=.=。 ↩

你可能感兴趣的:(JavaSE)