昨日,苹果公司给APP中使用了JSPatch、weex 以及 ReactNative 这些热更新技术的一部分开发者,发了一封警告邮件:大意是将不再准许使用动态下发代码机制,如果APP使用了,将有被拒的风险。相关人士:本次警告针对的应该是一些既不太安全,应用程序权限又高的第三方热更框架,同时苹果还释放出一个信号,原则上不予许热更新编程级别的代码。
本篇是 fank909 的第四篇投稿,详细地介绍了Java中的ClassLoader。由于篇幅较长,我这里只推出了基础部分,如果读完感兴趣,大家可以点击文末阅读原文查看进阶部分。
frank909 的博客地址:
http://blog.csdn.net/briblue
ClassLoader 翻译过来就是 类加载器,普通的Java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解 ClassLoader 的加载机制,也有利于我们编写出更高效的代码。
ClassLoader 的具体作用就是将 class文件 加载到 jvm虚拟机 中去,程序就可以正确运行了。但是,jvm 启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习 ClassLoader 这种加载机制。
备注:本文篇幅比较长,但内容简单,大家不要恐慌,安静地耐心翻阅就是
我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序 HelloWorld.java
如图:
然后,我们需要在命令行中进行java文件的编译:
javac HelloWorld.java
可以看到目录下生成了.class文件。我们再从命令行中执行命令:
java HelloWorld
上面是基本代码示例,是所有入门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的时候,最害怕的就是下载 JDK 后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。
因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOME、PATH、CLASSPATH。
JAVA_HOME
指的是你JDK安装的位置,一般默认安装在C盘,如:
C:\Program Files\Java\jdk1.8.0_91
PATH
将程序路径包含在 PATH 当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到 javac 和 java 两个命令。一般的:
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语言系统自带有三个类加载器:
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文件。
如果你有足够的好奇心,你应该会对它的源码感兴趣:
我们先前的内容有说过,可以指定 -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 加载的就是 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文件:
public class Test{}
然后,编写一个 ClassLoaderTest.java 文件:
我们获取到了 Test.class 文件的类加载器,然后打印出来。结果是:
ClassLoader is: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()。
重要方法
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对象。 我们可以从源代码看出这个步骤:
代码解释了双亲委托。要注意的是如果要编写一个 classLoader 的子类,也就是自定义一个 classloader,建议覆盖 findClass()方法,而不要直接改写 loadClass()方法。另外:
前面说过 ExtClassLoader 的 parent 为 null,所以它向上委托时,系统会为它指定 Bootstrap ClassLoader。
不知道大家有没有发现,不管是 Bootstrap ClassLoader 还是 ExtClassLoader等,这些类加载器都只是加载指定的目录下的jar包或者资源。如果在某种情况下,我们需要动态加载一些东西呢?比如从D盘某个文件夹加载一个class文件,或者从网络上下载class主内容然后再进行加载,这样可以吗?
如果要这样做的话,需要我们自定义一个 classloader。
自定义步骤
1. 编写一个类继承自 ClassLoader 抽象类。
2. 复写它的 findClass() 方法。
3. 在 findClass() 方法中调用 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,自已指定加载的路径,可以是磁盘、内存、网络或者其它。
所以,你说路径能不能成为它们的关键字?
当然上面的只是我个人的看法,可能不正确,但现阶段,这样有利于自己的学习理解。
每天学习累了,看些搞笑的段子放松一下吧。关注最具娱乐精神的公众号,每天都有好心情。
如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。
欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: