前言
Java里面的类加载机制,可以说是Java虚拟机核心组件之一,掌握和理解JVM虚拟机的架构,将有助于我们站在底层原理的角度上来理解Java语言,这也是为什么我们学习一个新的知识时,如果不理解原理全靠死记硬背,我相信过不了几天便会忘记的一干二净。
Java是一门跨平台的语言,而JVM虚拟机则在这中间扮演了非常重要的角色,对于我们编写的.java文件,在编译期间会被转换成二进制的class文件,我们也叫做bytecode(字节码),那么这些class文件是如何被加载进JVM虚拟机里面,又是如何被执行呢?
这就引入了今天我们文章要重点分析的知识之Java类加载器,在此之前我们重新来回顾下JVM的执行架构,借用网上的一张图片,可以非常直观的帮助我们了解:
Java虚拟机的核心由三个重要的组件构成:
(1)类加载系统
(2)运行时数据区域
(3)执行引擎
在这里面我们需要重点理解和掌握的包括,类加载机制,运行时数据区域,及执行引擎里面的GC回收器的算法和原理。
运行时数据区域在前面文章已经介绍过,gc算法和原理打算放下一篇文章单聊,本篇文章我们重点介绍类加载器机制。
文章开头我们提到过我们写的java源码文件,在编译后会转成二进制的字节码的class文件,如果我们想要使用它们,那么必须通过类加载器加载处理之后才能使用。
为什么需要类加载器
从广义的概念上Java语言里面只有两种类加载器:
(1)Bootstrap CLassloder(引导类加载器)
(2)User Define Classloader(用户自定义的类加载器)
引导类加载器是本身就是JVM规范的一部分,它与OS平台有关,依赖于OS的实现方式加载类型(包括Java API的类和接口),所以在Java里面引导类加载器只能是native实现的,尽管它是所有类加载器的父加载器,但它却不是Java实现的,所以Java里面引导加载器返回的是null。
Java的引导加载器是严格封闭的,因为其作用就是负责加载Java核心的基础库如rt.jar等,这里面就包含了我们常用的java.lang.xxx等相关类,引导类加载的库保证了类型安全,如果你想自定义一个Long类来替换Java基础库的Long类几乎是做不到的。
而自定义的类加载器机制则提供了非常灵活的扩展机制,允许我们自定义加载器来实现一些特殊的功能。
为什么需要自定义类加载器?
这里列举几种场景:
(1)加密。对字节码加密,Java的类文件可以被很容易反编译,为了提高安全性,我们再编译的时候可以加入加密算法,改变二进制文件的编码,然后在定义专门的来加载器来加载加密后文件,在加载之前解密二进制字节码,在加载,这样就可以提高安全性。
(2)以非标准的方式加载类文件。 比如我们的类文件存放在数据库,FTP,或者在从某个网站上下载。
(3)在运行时候动态的去系统外部加载运行一个类。
(4)在同一个应用中,通过类加载器实现环境或者资源的隔离。
(5)通过类加载器实现灵活的可插拔机制。
Java类加载器的双亲委派机制
从上面可以看到自定义类加载器的强大之处,在我们要实现自定义的类加载器之前,我们需要先了解下Java里面的类加载器是如何加载类的。
Java里面的ClassLoader类是实现自定义类加载器的关键,ClassLoader类是一个抽象类,其提供了自定义类加载器的通用描述,其主要的子类如下:
ClassLoader SecureClassLoader URLClassLoader ExtClassLoader AppClassLoader
根据Java平台的具体实现,实际的类加载器顺序如下:
这里大家需要注意一点,类加载器的顺序并不是所谓的继承关系,其实是逻辑组合关系。
前面提到过引导类加载器是所有加载器的前提,尽管Java语言里面不存在具体的这个类,因为其与操作系统有关,所以是native方法实现。但其却是Java里面所有类加载器名副其实的父加载器,其加载的资源路径是:
%JAVA_HOME%/jre/lib
接着我们看ExtClassLoader加载器的,加载路径是
%JAVA_HOME%/jre/lib/ext或者是java.ext.dirs属性里面配置的路径
最后是AppClassLoader加载器,其加载的资源路径是:
当前的classpath的路径
通过上面的分析,我们能够看到其实类加载器的本质是,加载了什么路径下的资源文件,对于上面的几个类加载的路径,我们可以在Java虚拟机启动类Launcher源码中找到答案:
其中引导类加载器的路径是:
System.getProperty("sun.boot.class.path");
ExtClassLoader类加载器的路径是:
System.getProperty("java.ext.dirs")
最后AppClassLoader类加载器的路径是:
System.getProperty("java.class.path")
通过下面这个测试方法,就可印证:
public static void showClassLoaderForeachPath(){ System.out.println(); //BoostrapClassLoader String[] split=System.getProperty("sun.boot.class.path").split(":"); for(String data:split){ System.out.println(data); } System.out.println("==================="); //ExeClassLoader String[] split1=System.getProperty("java.ext.dirs").split(":"); for(String data:split1){ System.out.println(data); } System.out.println("==================="); //AppClassLoader String[] split2=System.getProperty("java.class.path").split(":"); for(String data:split2){ System.out.println(data); } System.out.println("================"); }
接着我们随便定义一个测试类,看看该类的加载器的情况:
public static void showClassLoaderPath(){ System.out.println(ClassLoaderTest.class.getClassLoader()); System.out.println(ClassLoaderTest.class.getClassLoader().getParent()); System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent()); System.out.println("------------------------------------"); System.out.println(int.class.getClassLoader()); System.out.println(Long.class.getClassLoader()); }
输出结果:
sun.misc.Launcher$AppClassLoader@511d50c0sun.misc.Launcher$ExtClassLoader@5e481248null------------------------------------nullnull
可以看到我们自定义的类都是由AppClassLoader这个类加载器加载的,而AppClassLoader是谁由加载呢?
在第二行代码地方能看到是ExtClassLoader加载的,注意这里再次强调类加载器层次非继承关系。
然后我们接着看ExtClassLoader类加载器的父类,发现输出的是null,这在前面已经说了引导加载器是native实现的,所以在Java里面是访问不到的所以是null。
到这里,我们的疑问点集中在为什么类加载器非继承关系,因为在上面的类图里面AppClassLoader与ExtClassLoader是平级兄弟关系,那么为什么说AppClassLoader是由ExtClassLoader作为父类加载器呢?
答案就在源码中,首先看下ClassLoader这个抽象类的构造函数:
//1 protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }//2 protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); }//3 private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); package2certs = new ConcurrentHashMap<>(); domains = Collections.synchronizedSet(new HashSet()); assertionLock = new Object(); } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; package2certs = new Hashtable<>(); domains = new HashSet<>(); assertionLock = this; } }
我们发现无参和一参的构造函数都是调用二参构造函数,二参构造函数的第二个参数恰恰就是指定的父类加载器,如果使用的是无参构造函数,默认调用是:
public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; }
接着看initSystemClassLoader这个方法,这个方法里面有个关键的地方在于调用了sun.misc.Launcher之后,从这个类里面获取了ClassLoader实例:
sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; scl = l.getClassLoader(); }
接着我们看下Launcher类的构造方法时如何定义的:
public Launcher() { Launcher.ExtClassLoader var1; try { //1 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader