org.apache.commons.lang3包下的常用工具类
类加载器
初学Java的时候,你应该用命令行编译过Java文件。Java代码通过javac编译成class文件,而类加载器的作用,就是把class文件装进虚拟机。
面试请回答:将“通过类的全限定名获取描述类的二进制字节流”这件事放在虚拟机外部,由应用程序自己决定如何实现。
宏观来看,只有两种类加载器:启动类加载器、其他类加载器。
启动类加载器属于虚拟机的一部分,它是用C++写的,看不到源码;其他类加载器是用Java写的,说白了就是一些Java类,一会儿就可以看到了,比如扩展类加载器、应用类加载器。
- 启动类加载器:BootstrapClassLoader
- 扩展类加载器:ExtentionClassLoader
- 应用类加载器:AppClassLoader (也叫做“系统类加载器”)
既然只是把class文件装进虚拟机,为什么要用多种加载器呢?因为Java虚拟机启动的时候,并不会一次性加载所有的class文件(内存会爆),而是根据需要去动态加载。
一、它们分别加载了什么?
类加载器是通过类的全限定名(或者说绝对路径)来找到一个class文件的。可以直接打印启动类加载器BootstrapClassLoader的加载路径看看:
这一小节里,你只关心输出结果就可以了,反正这些API我也是现查的。
1 | URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); |
输出结果(%20是空格):
1 | file:/C:/Program%20Files/Java/jre1.8.0_131/lib/resources.jar |
可以看到,启动类加载器加载的是jre和jre/lib目录下的核心库,具体路径要看你的jre安装在哪里。再打印一下扩展类加载器ExtentionClassLoader的加载路径看看:
1 | URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs(); |
输出结果:
1 | file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/access-bridge-64.jar |
很明显,扩展类加载器加载的是jre/lib/ext目录下的扩展包。这些类库具体是什么不重要,只需要知道不同的类库可能是被不同的类加载器加载的。
JVM是怎么知道我们把JRE安装到哪里了呢?因为你安装完JDK之后配置了环境变量啊!那些 JAVA_HOME、CLASSPATH 之类的就是干这个用的。
最后是AppClassLoader:
1 | URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(); |
输出结果:
1 | file:/D:/JavaWorkSpace/PicklePee/bin/ |
这是当前java工程的bin目录,也就是我们自己的Java代码编译成的class文件所在。
二、Java虚拟机的入口
当我们运行一个Java程序时,首先是JDK安装目录下的jvm.dll启动虚拟机,而sun.misc.Launcher类就是虚拟机执行的第一段Java代码。之前提到,除BootstrapClassLoader以外,其他的类加载器都是用Java实现的——在Launcher里你就可以看到它们。
以下是sun.misc.Launcher的精简版源码,阅读起来应该毫无难度:
1 | public class Launcher { |
可以看到,扩展类加载器和应用类加载器都是Launcher里的静态内部类。它们都是调用了自己的静态方法getExtClassLoader返回自己的实例,看一下发生了什么:
1 | /* 扩展类加载器是Launcher的静态内部类,这里只是把它单独拎出来了 */ |
刚刚传了三个参数给父类URLClassLoader的构造器,继续深入:
1 | public class URLClassLoader extends SecureClassLoader implements Closeable { |
URLClassLoader继续把这个null扔给父类SecureClassLoader?看看它要做什么:
1 | public class SecureClassLoader extends ClassLoader { |
什么也没干,直接扔给了父类:
1 | public abstract class ClassLoader { |
终于到头了,从扩展类加载器的getExtClassLoader()一路走来,发现参数null传给了最顶层ClassLoader的全局变量parent,看一下关系图:
你可能注意到,JDK总是通过一个类似System.getProperty(“xxx”)的方法来获取class文件路径。这个字符串参数到底是哪来的呢?其实它可以在虚拟机启动时手动赋值。比如:
1 | java -D java.ext.dirs=路径 MyClass //这样自定义的路径将覆盖Java本身的拓展类路径 |
回到源码,还记得一开始Launcher类里,得到应用类加载器的这行代码吗?
1 | appClassLoader = AppClassLoader.getAppClassLoader(extentionClassLoader); |
它把创建的扩展类加载器作为参数传给了应用类加载器,进去看一下:
1 | static class AppClassLoader extends URLClassLoader { |
至此应该一切都清晰了,后面的过程与扩展类加载器一样!只不过最终的parent参数会被赋值为扩展类加载器(extcl)而不是null。扯了这么多,这个parent到底是干什么的?
三、父加载器
ClassLoader里的parent是父加载器。刚刚看了类加载器的继承关系图,但是父加载器不是父类,这是两个不同的概念。看一下前面ClassLoader的getParent()方法,任何一个类加载器调用此方法得到的对象就是它的父加载器。
AppClassLoader的父类是URLClassLoader,但是它的父加载器是ExtentionClassLoader。
除了启动类加载器(BootstrapClassLoader),每个类加载器都有一个父加载器。比如刚才的应用类加载器,它的父加载器是扩展类加载器。你可能会说扩展类加载器的parent是null,所以它没有父加载器?
有,它的父加载器就是BootstrapClassLoader。任何parent为null的加载器,其父加载器为BootstrapClassLoader,先记住这个结论,很快你会看到原因。
最后一个问题,如果你直接继承ClassLoader自己实现一个类加载器,且不指定父加载器,那么这个自定义类加载器的父加载器是什么?
是应用类加载器AppClassLoader。可以拉回去看看ClassLoader的无参构造器。
父加载器关系
四、双亲委派模型
有一个描述类加载器加载类过程的术语:双亲委派模型。然而这是一个很有误导性的术语,它应该叫做单亲委派模型(Parent-Delegation Model)。但是没有办法,大家都已经这样叫了。所谓双亲委派,这个亲就是指ClassLoader里的全局变量parent,也就是父加载器。
双亲委派的具体过程如下:
- 当一个类加载器接收到类加载任务时,先查缓存里有没有,如果没有,将任务委托给它的父加载器去执行。
- 父加载器也做同样的事情,一层一层往上委托,直到最顶层的启动类加载器为止。
- 如果启动类加载器没有找到所需加载的类,便将此加载任务退回给下一级类加载器去执行,而下一级的类加载器也做同样的事情。
- 如果最底层类加载器仍然没有找到所需要的class文件,则抛出异常。
所以是一条线传上再传下,并没有什么“双亲”。整个过程的Java实现也没有什么神秘的:
1 | public abstract class ClassLoader { |
什么是解析?把符号引用变为直接引用。比如com.test.Car里面有一个com.test.Wheel类,在编译时Car类并不知道Wheel类的实际内存地址,此时com.test.Wheel只是一个符号。“解析”的意思就是把被引用的类加载入内存,然后将com.test.Wheel这个符号变成一个指针,能够定位到内存中目标。
到现在就剩下findClass这个模板方法了,URLClassLoader继承了ClassLoader以后,重写了此方法,做了三件事:
1 | protected Class<?> findClass(final String name) throws ClassNotFoundException { |
如果我们自己去实现一个类加载器,基本上就是继承ClassLoader之后重写findClass方法,且在此方法的最后调包defineClass。
五、为什么要双亲委派?
确保类的全局唯一性。
如果你自己写的一个类与核心类库中的类重名,会发现这个类可以被正常编译,但永远无法被加载运行。因为你写的这个类不会被应用类加载器加载,而是被委托到顶层,被启动类加载器在核心类库中找到了。如果没有双亲委托机制来确保类的全局唯一性,谁都可以编写一个java.lang.Object类放在classpath下,那应用程序就乱套了。
从安全的角度讲,通过双亲委托机制,Java虚拟机总是先从最可信的Java核心API查找类型,可以防止不可信的类假扮被信任的类对系统造成危害。
六、所以知道这些到底有什么用?
- 面试。
- 研究Tomcat、JBoss等Servlet容器原理,可能得另开一篇了。
- 如果你不想自己的代码被反编译,可以将编译后的代码加密,用自己的类加载器解密。
- 我编不下去了。