本文贴出的类加载器相关源码全部做了精简,只要会CRUD就能看懂。并不是所有JDK源码都是开源的,如果你想自己看完整的源码,却发现在Eclipse里看不到sun包源码,可以直接拉到最后一小节。但是个人强烈不建议毫无目的地通读源码。
〇、类加载器是干什么的?
初学Java的时候,你应该用命令行编译过Java文件。Java代码通过javac编译成class文件,而类加载器的作用,就是把class文件装进虚拟机。
面试请回答:将“通过类的全限定名获取描述类的二进制字节流”这件事放在虚拟机外部,由应用程序自己决定如何实现。
宏观来看,只有两种类加载器:启动类加载器、其他类加载器。
启动类加载器属于虚拟机的一部分,它是用C++写的,看不到源码;其他类加载器是用Java写的,说白了就是一些Java类,一会儿就可以看到了,比如扩展类加载器、应用类加载器。
启动类加载器:BootstrapClassLoader扩展类加载器:ExtentionClassLoader应用类加载器:AppClassLoader (也叫做“系统类加载器”)
既然只是把class文件装进虚拟机,为什么要用多种加载器呢?因为Java虚拟机启动的时候,并不会一次性加载所有的class文件(内存会爆),而是根据需要去动态加载。
一、它们分别加载了什么?
类加载器是通过类的全限定名(或者说绝对路径)来找到一个class文件的。可以直接打印启动类加载器BootstrapClassLoader的加载路径看看:
这一小节里,你只关心输出结果就可以了,反正这些API我也是现查的。
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();for (URL url : urls) {System.out.println(url);}
输出结果(%20是空格):
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/resources.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/rt.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/sunrsasign.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/jsse.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/jce.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/charsets.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/jfr.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/classes
可以看到,启动类加载器加载的是jre和jre/lib目录下的核心库,具体路径要看你的jre安装在哪里。再打印一下扩展类加载器ExtentionClassLoader的加载路径看看:
URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();for (URL url : urls) {System.out.println(url);}
输出结果:
file:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/access-bridge-64.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/cldrdata.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dnsns.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/dns_sd.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jaccess.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/jfxrt.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/localedata.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/nashorn.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunec.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunjce_provider.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunmscapi.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/sunpkcs11.jarfile:/C:/Program%20Files/Java/jre1.8.0_131/lib/ext/zipfs.jar
很明显,扩展类加载器加载的是jre/lib/ext目录下的扩展包。这些类库具体是什么不重要,只需要知道不同的类库可能是被不同的类加载器加载的。
JVM是怎么知道我们把JRE安装到哪里了呢?因为你安装完JDK之后配置了环境变量啊!那些 JAVA_HOME、CLASSPATH 之类的就是干这个用的。
最后是AppClassLoader:
URL[] urls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs();for (URL url : urls) {System.out.println(url);}
输出结果:
file:/D:/JavaWorkSpace/PicklePee/bin/
这是当前java工程的bin目录,也就是我们自己的Java代码编译成的class文件所在。
二、Java虚拟机的入口
当我们运行一个Java程序时,首先是JDK安装目录下的jvm.dll启动虚拟机,而sun.misc.Launcher类就是虚拟机执行的第一段Java代码。之前提到,除BootstrapClassLoader以外,其他的类加载器都是用Java实现的——在Launcher里你就可以看到它们。
以下是sun.misc.Launcher的精简版源码,阅读起来应该毫无难度:
public class Launcher {private static Launcher launcher = new Launcher();private ClassLoader appClassLoader;// 启动类加载器不是Java类,我们这里拿到的是其加载路径字符串 private static String bootClassPath = System.getProperty("sun.boot.class.path");public Launcher() {ClassLoader extentionClassLoader; // 扩展类加载器在这里 try {extentionClassLoader = ExtClassLoader.getExtClassLoader();} catch (Exception e) {...}try {// 应用类加载器在这里,get时把扩展类加载器作为参数,后面我们会回到这里。 appClassLoader = AppClassLoader.getAppClassLoader(extentionClassLoader);} catch (Exception e) {...}}// 静态内部类:扩展类加载器,父类是URLClassLoader static class ExtClassLoader extends URLClassLoader {}// 静态内部类:应用类加载器,父类是URLClassLoader static class AppClassLoader extends URLClassLoader {}public static Launcher getLauncher() {return launcher;}public ClassLoader getClassLoader() {return appClassLoader;}private static URLStreamHandlerFactory factory = new Factory();private static class Factory implements URLStreamHandlerFactory {public URLStreamHandler createURLStreamHandler(String protocol) {/* 创建一个文件句柄(File Handler) 我们硬盘上的class文件就是通过这个句柄进入内存 */}}}
可以看到,扩展类加载器和应用类加载器都是Launcher里的静态内部类。它们都是调用了自己的静态方法getExtClassLoader返回自己的实例,看一下发生了什么:
/* 扩展类加载器是Launcher的静态内部类,这里只是把它单独拎出来了 */static class ExtClassLoader extends URLClassLoader {public ExtClassLoader(File[] dirs) throws IOException {// 交给了父类URLClassLoader处理。中间的参数是null,它是什么呢? super(getExtURLs(dirs), null, factory);}public static ExtClassLoader getExtClassLoader() throws IOException {final File[] dirs = getExtDirs();ExtClassLoader extentionClassLoader = new ExtClassLoader(dirs);return extentionClassLoader}private static File[] getExtDirs() {// 就这样拿到了扩展类加载器的加载路径,这跟第一小节我们打印的路径是一样的。 String s = System.getProperty("java.ext.dirs");File[] dirs;... 按照;分割字符串s,转化为一个File数组 ...return dirs;}}
刚刚传了三个参数给父类URLClassLoader的构造器,继续深入:
public class URLClassLoader extends SecureClassLoader implements Closeable {private ArrayList<URL> path = new ArrayList<URL>();public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {super(parent); // 第二个参数null是这里的parent,然后又再次扔给了父类/* 保存这些类库的路径并创建一个获取jar文件的句柄,想干什么已经很明显了 */for (int i = 0; i < urls.length; i++) {path.add(urls[i]);}if (factory != null) {jarHandler = factory.createURLStreamHandler("jar");}}}
URLClassLoader继续把这个null扔给父类SecureClassLoader?看看它要做什么:
public class SecureClassLoader extends ClassLoader {protected SecureClassLoader(ClassLoader parent) {super(parent);...}...}
什么也没干,直接扔给了父类:
public abstract class ClassLoader {private final ClassLoader parent;private ClassLoader(Void unused, ClassLoader parent) {this.parent = parent; // parent最终是ClassLoader里的全局变量 ...}// 我们从子类到达这里 protected ClassLoader(ClassLoader parent) {this(checkCreateClassLoader(), parent);}protected ClassLoader() {// 默认parent是系统(应用)类加载器! this(checkCreateClassLoader(), getSystemClassLoader());}public final ClassLoader getParent() {if (parent == null) {return null;}return parent;}/* 防止恶意代码对系统产生影响,有兴趣可以搜索Java安全管理器 */private static Void checkCreateClassLoader() {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkCreateClassLoader();}return null;}}
终于到头了,从扩展类加载器的getExtClassLoader()一路走来,发现参数null传给了最顶层ClassLoader的全局变量parent,看一下关系图:
你可能注意到,JDK总是通过一个类似System.getProperty("xxx")的方法来获取class文件路径。这个字符串参数到底是哪来的呢?其实它可以在虚拟机启动时手动赋值。比如:
java -D java.ext.dirs=路径 MyClass //这样自定义的路径将覆盖Java本身的拓展类路径还有一个命令是 -Xbootclasspath 可以改变核心类库的加载路径,知道有这回事儿就行了,最好别用。
回到源码,还记得一开始Launcher类里,得到应用类加载器的这行代码吗?
appClassLoader = AppClassLoader.getAppClassLoader(extentionClassLoader);
它把创建的扩展类加载器作为参数传给了应用类加载器,进去看一下:
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 = getClassPath(s);URL[] urls = pathToURLs(path);// 传入的扩展类加载器extcl在这里 return new AppClassLoader(urls, extcl);}AppClassLoader(URL[] urls, ClassLoader parent) {// AppClassLoader的父类也是URLClassLoader,只不过第二个参数由null变为扩展类加载器 super(urls, parent, factory);}}
至此应该一切都清晰了,后面的过程与扩展类加载器一样!只不过最终的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实现也没有什么神秘的:
public abstract class ClassLoader {// name: Class文件的绝对路径 // resolve: 找到后是否立即解析(什么是解析?) protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (lock) {// 尝试从缓存获取,这也是为什么修改了Class后需重启JVM才能生效 Class<?> target = findLoadedClass(name); // native方法 if (target == null) {try {if (parent != null) {// 委托给父加载器, 只查找不解析 target = parent.loadClass(name, false);} else {// 父加载器为null,则委托给启动类加载器BootstrapClassloader target = findBootstrapClassOrNull(name); // native方法 }} catch (ClassNotFoundException e) {...}if (target == null) {// 父加载器没有找到,才调用自己的findClass()方法 target = findClass(name);}}if (resolve) {resolveClass(target); // native方法 }return target;}}// findClass是模板方法,需要重写 protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}}
什么是解析?把符号引用变为直接引用。比如com.test.Car里面有一个com.test.Wheel类,在编译时Car类并不知道Wheel类的实际内存地址,此时com.test.Wheel只是一个符号。“解析”的意思就是把被引用的类加载入内存,然后将com.test.Wheel这个符号变成一个指针,能够定位到内存中目标。
到现在就剩下findClass这个模板方法了,URLClassLoader继承了ClassLoader以后,重写了此方法,做了三件事:
protected Class<?> findClass(final String name) throws ClassNotFoundException {// 1、安全检查 // 2、根据绝对路径把硬盘上class文件读入内存 byte[] raw = getBytes(name);// 3、将二进制数据转换成class对象 return defineClass(raw);}
如果我们自己去实现一个类加载器,基本上就是继承ClassLoader之后重写findClass方法,且在此方法的最后调包defineClass。
五、为什么要双亲委派?
确保类的全局唯一性。
如果你自己写的一个类与核心类库中的类重名,会发现这个类可以被正常编译,但永远无法被加载运行。因为你写的这个类不会被应用类加载器加载,而是被委托到顶层,被启动类加载器在核心类库中找到了。如果没有双亲委托机制来确保类的全局唯一性,谁都可以编写一个java.lang.Object类放在classpath下,那应用程序就乱套了。
从安全的角度讲,通过双亲委托机制,Java虚拟机总是先从最可信的Java核心API查找类型,可以防止不可信的类假扮被信任的类对系统造成危害。
六、所以知道这些到底有什么用?
面试。研究Tomcat、JBoss等Servlet容器原理,可能得另开一篇了。如果你不想自己的代码被反编译,可以将编译后的代码加密,用自己的类加载器解密。我编不下去了。
★、用Eclipse查看sun包源码
sun包源码正常情况下是看不到的。
想看这部分源码,先确定一下你当前的Java版本(cmd 输入 java -version)
然后下载对应的 OpenJDK。从外网下载速度可能比较慢,我搞了一套6-12版本放到了百度网盘。
链接:https://pan.baidu.com/s/1ZwllHyhxSDihAhi8ZqustA提取码:cx2c
下载之后解压到任意目录,然后Eclispe里 Window → Preferences:
弹出External Folder后,打开刚刚解压的OpenJDK文件夹,找到src路径:
不同版本OpenJDK目录结构可能与此图不一样,但是肯定都能找到src目录,选它就没错!
然后重启Eclipse。完成!
暂无评论数据