定义:
虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为“类加载器“。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。简单来讲,比较两个类是否“相等”,必须是这两个类由同一个类加载器加载的才有意义,否则,即使这两个类来源于同一个class、被同一个虚拟机加载,只要这两个类是由不同的加载器加载,那么这两个类一定不相等。
从Java虚拟机角度上来讲,只存在两种不同的类加载器:一种是启动类加载器又叫根类加载器(Bootstrap ClassLoader ),这个类是由C++实现的,是虚拟机的一部分;另一种就是其他所有类加载器,这些加载器都是有Java实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader。
类加载机制
类加载器的加载机制采用双亲委派模型(Parents Delegation Model)或者说是父亲委托机制。双亲委派模型要求除了顶层是启动类加载器之外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般不会使用继承方式来实现,而都是使用组合关系来复用父类的代码,Java虚拟机为我们提供了三种类加载器,最顶层的启动类加载器、然后是扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)又叫系统类加载器,用户如果有需要还可以自定义类加载器,继承java.lang.ClassLoader抽象类实现自定义加载器,所有的自定义加载器都需要指定一个父类加载器,未指定父类加载器的情况下,默认父类加载器为应用程序类加载器。从下面的ClassLoader中的源码片段就可以看出来:
1 //这个构造方法是指定父类加载器的,通过构造方法传进来的是父类加载器 2 protected ClassLoader(ClassLoader parent) { 3 this(checkCreateClassLoader(), parent); 4 } 5 6 //不带参数的构造方法,默认会将父类加载器指定为应用程序类加载器, 、 7 //getSystemClassLoader这个方法会获取应用程序类加载器 8 protected ClassLoader() { 9 this(checkCreateClassLoader(), getSystemClassLoader()); 10 }
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器进行加载,每一个层次的类加载器都是如此,因此最终所有的请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于最顶端的启动类加载器进行加载,因此Object类在各种类加载环境中都是一个类。相反如果没有采用双亲委派模型,由各个类加载器进行各自加载的话,如果用户自己编写了一个java.lang.Object类,并放在classpath中,那系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证。双亲委派模型的实现很简单,实现代码都集中在ClassLoader抽象类的loadClass方法中,如下代码清单,逻辑清晰易懂,首先检查类是否被加载,若没有则调用父类加载器进行加载,若父加载器为空则默认使用启动类加载器作为父加载器进行加载。如果父类加载失败后抛出ClassNotFoundException异常之后,再调用自己的findClass方法进行加载,代码如下:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // First, check if the class has already been loaded 6 Class c = findLoadedClass(name); 7 if (c == null) { 8 long t0 = System.nanoTime(); 9 try { 10 if (parent != null) { 11 c = parent.loadClass(name, false); 12 } else { 13 c = findBootstrapClassOrNull(name); 14 } 15 } catch (ClassNotFoundException e) { 16 // ClassNotFoundException thrown if class not found 17 // from the non-null parent class loader 18 } 19 20 if (c == null) { 21 // If still not found, then invoke findClass in order 22 // to find the class. 23 long t1 = System.nanoTime(); 24 c = findClass(name); 25 26 // this is the defining class loader; record the stats 27 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 28 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 29 sun.misc.PerfCounter.getFindClasses().increment(); 30 } 31 } 32 if (resolve) { 33 resolveClass(c); 34 } 35 return c; 36 }
下面是Java虚拟机提供的几种类加载器的详细介绍
- 启动类加载器(Bootstrap)、又叫根类加载器:该加载器没有父类加载器,它由C++实现,它负责加载虚拟机的核心类库,如java.lang.*等,它从<JAVA_HOME>\lib中、或者是被-Xbootclasspath参数所指定的路径中、并且是虚拟机所识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使被放在lib中也不会被加载)的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,它没有继承java.lang.ClassLoader类,用户在编写自定义的类加载器时,如果需要把加载请求委派给启动类加载器,那直接用null代替即可,如下代码所示,为ClassLoader.getClassLoader代码片段:
-
1 // Returns the class's class loader, or null if none. 2 static ClassLoader getClassLoader(Class<?> caller) { 3 // This can be null if the VM is requesting it 4 if (caller == null) { 5 return null; 6 } 7 // Circumvent security check since this is package-private 8 return caller.getClassLoader0(); 9 }
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader,由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以一般也称它为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
命名空间:
每一个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字相同的两个类,在不同的命名空间中则有可能出现。
运行时包:
由同一类加载器加载的属于相同包的类组成运行时包。决定两个类是不是属于一个运行时包,不仅看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(即默认包访问权限以及由protect修饰的类及类成员)的属性。这样的限制能避免用户自定义的类冒充核心类库去访问核心类的包可见成员。假设用户自定义了一个java.lang.Test类,并由自定义加载器加载,Test类并不能访问核心类库java.lang.*中的包可见成员。