ClassLoader类加载器可以说是Java中必学内容之一,无论是想要去研究Concurrent包、Unsafe,还是深入学习Spark等分布式计算框架,都必须对此有一定的理解。笔者在写之前也只了解了皮毛,想通过这篇文章,结合一些书籍和博客,加深对ClassLoader的理解,并分享一下。
xxx.class想必不陌生,JVM不会理解我们写的Java源文件, 我们必须把Java源文件编译成class文件, 才能被JVM识别, 对于JVM而言, class文件就相当于一个接口,处于中间的位置, 对于理解整个体系有着承上启下的作用。 如图所示:
相关JVM的基础,可以看下–>初探JVM原理与结构
深入理解Java Class文件
ClassLoader的具体作用就是将class文件加载到JVM虚拟机中去,程序就可以正确运行了。但是,JVM启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是,一次性加载那么多jar包那么多class,那内存不崩溃。
我们知道,java是一种动态语言。那么怎样理解这个“动态”呢?或者说一门语言具备了什么特性,才能称之为动态语言呢?我们都知道JVM执行的不是本地机器码指令,而是执行一种称之为字节码的指令(存在于class文件中)。这就要求虚拟机在真正执行字节码之前,先把相关的class文件加载到内存中。虚拟机不是一次性加载所有需要的class文件,因为它在执行的时候根本不会知道以后会用到哪些class文件。它是每用到一个类,就会在运行时“动态地”加载和这个类相关的class文件。这就是java被称之为动态性语言的根本原因。除了动态加载类之外,还会动态的初始化类,对类进行动态链接。
在JVM中负责对类进行加载的正是本文要介绍的类加载器(ClassLoader),所以,类加载器是JVM不可或缺的重要组件。
Java语言系统自带有三个类加载器:
Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。
Extension ClassLoader 主要负责加载Java的扩展类库,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。
我们上面简单介绍了3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath和-D java.ext.dirs这两个虚拟机参数选项。
自定义的ClassLoader 扩展Java虚拟机获取Class数据的能力,可以加载指定路径下的class文件,一般继承ClassLoader重写findClass()方法即可。一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。因为这样就能够保证它能访问系统内置加载器加载成功的class文件。
各个ClassLoader的层次和功能如下图所示,从ClassLoader的层次自顶往下为启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。其中,应用类加载器的双亲为扩展类加载器,扩展类加载器的双亲为启动类加载器。当系统需要使用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。
我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?
我可以先告诉你答案
为了更好的理解,我们可以查看源码。
看sun.misc.Launcher,它是一个java虚拟机的入口应用。
源码这边就不贴了,有兴趣可以自行去看Launcher,我们可以得到结论BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是对应环境属性sun.boot.class.path、java.ext.dirs和java.class.path来加载资源文件的。
编写一个ClassLoaderTest.java:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderTest.class.getClassLoader();
System.out.println("ClassLoader is:" + cl.toString());
}
}
执行得到结果:
也就是说明ClassLoaderTest.class文件是由AppClassLoader加载的。
那么基础类型呢? 如int、boolean:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderTest.class.getClassLoader();
System.out.println("ClassLoader is:" + cl.toString());
cl=int.class.getClassLoader();
System.out.println("ClassLoader is:" + cl.toString());
}
}
提示的是空指针,意思是int.class这类基础类没有类加载器加载?
当然不是!
int.class是由Bootstrap ClassLoader加载的。它是完全由C++代码实现的,并且在Java中没有对象与之对应,它也是虚拟机的核心组件。Extension ClassLoader和AppClassLoader都有对应的Java对象可供使用。
无法在Java代码中直接访问Bootstrap ClassLoader,因为这是一个纯C实现,因此任何加载在Bootstrap ClassLoader中的类是无法获得其ClassLoader实例的。由于int、String等基础类型属于Java核心类,由启动类加载器加载,故以上代码返回的是null。
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的现象。
尝试一下:
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderTest.class.getClassLoader();
System.out.println("ClassLoader is:" + cl.toString());
System.out.println("ClassLoader is:" + cl.getParent().toString());
System.out.println("ClassLoader is:" + cl.getParent().getParent().toString());
}
}
系统中的ClassLoader在协同工作时,默认会使用双亲委托模式。即在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载。
注意:双亲为null有两种情况:第一,其双亲就是启动类加载器;第二,当前加载器就是启动类加载器。判断类是否加载时,应用类加载器会顺着双亲路径往上判断,直到启动类加载器。但是启动类加载器不会往下询问,这个委托是单向的。
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。
JDK文档中是这样写的,通过指定的全限定类名加载class,它通过同名的loadClass(String, boolean)方法。
执行findLoadedClass(String)去检测这个class是不是已经加载过了。
执行父加载器的loadClass方法。如果父加载器为null,则JVM内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
如果向上委托父加载器没有加载成功,则通过findClass(String)查找。
如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。
源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
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()
resolveClass(c);
}
return c;
}
}
前文提到,检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。