一看你就懂,Java中的ClassLoader详解

作者简介 原创微信公众号郭霖 WeChat ID: guolin_blog

本篇是fank909的第四篇投稿,详细地介绍了Java中的ClassLoader。由于篇幅较长,我这里只推出了基础部分,如果读完感兴趣,大家可以点击文末阅读原文查看进阶部分。

frank909的博客地址:

http://blog.csdn.net/briblue

前言

ClassLoader 翻译过来就是 类加载器,普通的Java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解 ClassLoader 的加载机制,也有利于我们编写出更高效的代码。

ClassLoader 的具体作用就是将 class文件 加载到 jvm虚拟机 中去,程序就可以正确运行了。但是,jvm 启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习 ClassLoader 这种加载机制。

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

Class文件的认识

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

如图:

然后,我们需要在命令行中进行java文件的编译:

javacHelloWorld.java

可以看到目录下生成了.class文件。我们再从命令行中执行命令:

javaHelloWorld

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到 class文件 上,class文件 是字节码格式文件,java虚拟机并不能直接识别我们平常编写的 .java源文件,所以需要javac这个命令转换成 .class文件。另外,如果用 C 或者 Python 编写的程序正确转换成 .class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇:

http://blog.csdn.net/zhangjg_blog/article/details/21486985

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

你还记得Java环境变量吗

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

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

JAVA_HOME

指的是你JDK安装的位置,一般默认安装在C盘,如:

C:\ProgramFiles\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

http://www.grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/sun/misc/Launcher.java

它是一个 java虚拟机 的入口应用:

源码有精简,我们可以得到相关的信息。

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"));

得到的结果是:

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

ExtClassLoader源码

如果你有足够的好奇心,你应该会对它的源码感兴趣:

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

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

结果如下:

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

AppClassLoader源码

可以看到 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.path、java.ext.dirs 和 java.class.path 来加载资源文件的。

接下来我们探讨它们的加载顺序,我们先用 Eclipse 建立一个java工程:

然后创建一个Test.java文件:

publicclassTest{}

然后,编写一个 ClassLoaderTest.java 文件:

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

ClassLoaderis:sun.misc.Launcher$AppClassLoader@73d16e93

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

这个 Test类 是我们自己编写的,那么 int.class 或者是 String.class 的加载是由谁完成的呢?我们可以在代码中尝试:

运行一下,却报错了:

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

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

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

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

运行结果如下:

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

运行结果:

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

父加载器不是父类

我们先前已经粘贴了 ExtClassLoader 和 AppClassLoader 的代码:

可以看见 ExtClassLoader 和 AppClassLoader 同样继承自 URLClassLoader,但上面一小节代码中,为什么调用 AppClassLoader的getParent() 代码会得到 ExtClassLoader 的实例呢?先从 URLClassLoader 说起,这个类又是什么?先上一张类的继承关系图:

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

我们可以看到 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 的部分代码。

我们需要注意的是:

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

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

对应的代码:

答案已经很明了了,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) 方法:

protectedClassloadClass(Stringname,booleanresolve) throwsClassNotFoundException

上面是方法原型,一般实现这个方法的步骤是

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对象。 我们可以从源代码看出这个步骤:

代码解释了双亲委托。要注意的是如果要编写一个 classLoader 的子类,也就是自定义一个 classloader,建议覆盖 findClass()方法,而不要直接改写 loadClass()方法。另外:

前面说过 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:

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

我们编写DiskClassLoader的代码:

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

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

我们点击运行按钮,结果显示:

可以看到,Test类的say方法正确执行,也就是我们写的 DiskClassLoader 编写成功。

回首

讲了这么大的篇幅,自定义ClassLoader才姗姗来迟。 很多同学可能觉得前面有些啰嗦,但我按照自己的思路,我觉得还是有必要的。因为我是围绕一个关键字进行讲解的。

关键字是什么?关键字路径

从开篇的环境变量

到3个主要的JDK自带的类加载器

到自定义的ClassLoader

它们的关联部分就是路径,也就是要加载的class或者是资源的路径。

BootStrap ClassLoader、ExtClassLoader、AppClassLoader 都是加载指定路径下的jar包。如果我们要突破这种限制,实现自己某些特殊的需求,我们就得自定义ClassLoader,自已指定加载的路径,可以是磁盘、内存、网络或者其它。


完。。。。。。。。。。。。。。。。。。。。。

文章原创作者GuoLin 书籍推荐

郭林大神原创android 书籍:《第一行代码 android》

淘宝链接: https://s.click.taobao.com/t?e=m%3D2%26s%3DgKUfuKdAZKocQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67p2n%2BQBNMyE6Rku8%2Bpj6eJall3bs%2B3NRhNHnsKI%2BqxhyM0iVZhTFBom4YIorMPnmg8G0g2OJi%2FzmXHfenomYtn5EW9vzeG8LzfPUwktUBEmkxg5p7bh%2BFbQ%3D&pvid=10_106.6.161.154_3367_1490163222155

推荐阅读更多精彩内容