一般来说,Java源代码(.java)经过编译器编译成字节码(.class)后,类加载器读取字节码文件,最终加载并转换成 java.lang.Class
类的一个实例。
Java中的类加载器大致分为两种,一种是系统提供的,另外一种是Java开发者开发的。而系统提供的类加载器主要有三个:
- 引导类加载器(Bootstrap Class Loader):最顶层的类加载器,主要加载核心的类库,JRE目录下的rt.jar,resources.jar,charsets.jar等jar包和class,一般是用原生C或C++来实现的。
- 扩展类加载器(Extension Class Loader):用来加载JRElibext目录下的jar包和class等,由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现;
- 系统类加载器(System Class Loader),也可以成为应用类加载器:加载Java当前应用CLASSPATH下的所有类,由AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,一般情况下,它是Java应用程序默认的类加载器。
除了系统提供的类加载器外,开发者可以通过继承java.lang.ClassLoader类或组合的形式来实现自己的类加载器,以满足一些特殊的需求。
JVM要求除了最上层的引导类加载器之外,所有的类加载器都应当由一个父类加载器,而这个父加载器可以通过下表给出的getParent方法得到。这种加载模式就是所谓的双亲委派模式,如下图所示:
除了Bootstrap加载器,基本上所有的类加载器都是java.lang.ClassLoader类的一个实例。
ClassLoader类大致说明:
java.lang.ClassLoader
类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class
类的一个实例。除此之外,ClassLoader
还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader
提供了一系列的方法,比较重要的方法如下:
方法 | 说明 |
---|---|
getParent() |
返回该类加载器的父类加载器。 |
loadClass(String name) |
加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) |
查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) |
查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) |
把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) |
链接指定的 Java 类。 |
表示类名称的 name
参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1
和 com.example.Sample$Inner
等表示方式。
每一个Java类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()
方法就可以获取到此引用。所以说这种类加载器间的父子关系一般都是通过组合,而不是继承来实现的。
通过一个小程序来看看:
public class ClassLoaderTest { public static void main(String[] args) throws Exception { ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); while (classLoader != null) { System.out.println(classLoader.toString()); classLoader = classLoader.getParent(); } } }
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
可以看到,第一个输出的是ClassLoaderTest类的类加载器,即系统类加载器,它是 sun.misc.Launcher$AppClassLoader@18b4aac2类的实例;第二个是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader@1540e19d 类的实例。需要注意的是,没有输出引导类的加载器,因为由于bootstrap的实现不是Java,所以JDK源码中,对于父加载器是bootstrap的情况下,getParent方法返回的是null。
让我们来看一下双亲委派模式的简易流程:
“双亲委派模型”简单来说就是:
1.先检查需要加载的类是否已经被加载,如果没有被加载,则委托父加载器加载,父类继续检查,尝试请父类加载,这个过程是从下-------> 上;
2.如果走到顶层发现类没有被加载过,那么会从顶层开始往下逐层尝试加载,这个过程是从上 ------> 下;
3.如果最终都加载不了,那就会抛出异常;
这里必须要提一提JVM如何判定两个类是否相等:
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。
对于两个相同名称的类而言,不同的类加载器为相同名称的类创建了额外的命名空间,不同类加载器间加载的类之间是不兼容的。命名空间的作用抽象理解就是:
- 竖直方向上,父加载加载的类对所有子加载器可见;
- 水平方向上,子类之间各自加载的类对于各自是不可见的,达到了隔离的效果;
这就解释了另外一个问题,为什么JVM使用双亲委派模式,主要为了保证 Java 核心库的类型安全,防止重复与恶意加载;
比如Java应用都至少要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object
类,而且这些类之间是不兼容的。
并且如果要加载一个System类,使用委托机制,会递归的向父类查找,最终都是委托给最顶层的启动类加载器进行加载也就是用Bootstrap尝试加载,如果加载不了再向下查找。如果能在Bootstrap中找到然后加载,然后缓存下来,如果此时另一个类也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回缓存中的System即可而不需要重新加载,从而避免了恶意加载。
线程上下文类加载器(Context Class Loader)
还有一种类加载器,被称为Context Class Loader,线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。ContextClassLoader并不是一种新的类加载器,而是一种抽象的说法,它的获取和设置可以通过Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl) 来实现。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。
ContextClassLoader的存在是为了解决双亲委派机制无法解决的问题。双亲委派机制委托链上面的图已经说过了,一般说来,处于委托链下层的classLoader可以很容易的使用上层classLoader所加载的类;而反过来,如果上层的classLoader要使用下层的classLoader所加载的类的话,由于双亲委派机制是单向的,所以无法通过双亲委派来实现,所以就有了ContextClassLoader,这种情况下就可以把某个位于委派链下层的ClassLoader设置为线程的contextClassLoader,这种情况及突破了双亲委派的限制了。
其实,BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。他们的加载可以通过一个关键字来说明:路径;
public class ClassLoaderTest { public static void main(String[] args) throws Exception { System.out.println(System.getProperty("sun.boot.class.path")); System.out.println("======================="); System.out.println(System.getProperty("java.ext.dirs")); System.out.println("=========================="); System.out.println(System.getProperty("java.class.path")); } }
结果:
E:softwareJDK8.0jrelib esources.jar; E:softwareJDK8.0jrelib t.jar; E:softwareJDK8.0jrelibsunrsasign.jar; E:softwareJDK8.0jrelibjsse.jar; E:softwareJDK8.0jrelibjce.jar; E:softwareJDK8.0jrelibcharsets.jar; E:softwareJDK8.0jrelibjfr.jar; E:softwareJDK8.0jreclasses ======================= E:softwareJDK8.0jrelibext; C:WindowsSunJavalibext ========================== E:softwareJDK8.0jrelibcharsets.jar; E:softwareJDK8.0jrelibdeploy.jar; E:softwareJDK8.0jrelibextaccess-bridge-64.jar; E:softwareJDK8.0jrelibextsunpkcs11.jar; ...... E:softwareJDK8.0jrelibextzipfs.jar; E:softwareJDK8.0jrelibjavaws.jar; E:softwareJDK8.0jrelibjce.jar; E:softwareJDK8.0jrelibjfr.jar; E:softwareJDK8.0jrelibjfxswt.jar; E:softwareJDK8.0jrelibjsse.jar; E:softwareJDK8.0jrelibmanagement-agent.jar; E:softwareJDK8.0jrelibplugin.jar; E:softwareJDK8.0jrelib esources.jar; E:softwareJDK8.0jrelib t.jar; C:UsersIdeaProjectsuntitledoutproductionJavaTest; E:softwareideaIntelliJ IDEA 2017.1.3libidea_rt.jar
由于CLASSPATH下jar包太多,省略了一部分;不过,从打印的内容我们也可以看到他们加载的资源情况。
继承体系如下,classLoader的入口是:sun.misc.Launcher,其中ExtClassLoader和AppClassLoader是Launcher的内部静态类;
主要看下ClassLoader的loadClass方法,其他的源码等下篇文章再写:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查这个类是否已经被加载了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 如果class没有被加载且已经设置parent,那么请求其父加载器加载 if (parent != null) { c = parent.loadClass(name, false); } else { //如果没有设定parent类加载器,则寻找BootstrapClss并尝试使用Boot loader加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父加载器找不到class时会抛出ClassNotFoundException异常 } if (c == null) { // 如果当前这个loader所有的父加载器以及顶层的Bootstrap ClassLoader都不能加载待加载的类 // 那么则调用自己的findClass()方法来加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
注意:Class.forName使用的是哪个类加载器?
首先,class.forName是带参数的,可以指定类加载器,如果没有指定,那默认使用的是当前类的类加载器,也就是默认的AppClassLoader;
public class ClassLoaderTest { public static void main(String[] args) throws Exception { ClassLoader test = ClassLoaderTest.class.getClassLoader(); System.out.println(test); ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); System.out.println(contextClassLoader); ClassLoader forNameClassLoader = Class.forName("com.test.ForNameTest").getClassLoader(); System.out.println(forNameClassLoader); } } class ForNameTest{}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
参考自:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://www.blogjava.net/zhuxing/archive/2008/08/08/220841.html
http://blog.csdn.net/briblue/article/details/54973413
感谢RednaxelaFX,R神。