类加载器
类加载器用来把类加载到Java虚拟机中。从JDK 1.2 版本开始,类的加载过程采用父亲委托机制(双亲委托机制),这种机制能更好地保证Java 平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载Sample类时, loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
Java虚拟机自带了以下几种加载器。
-
根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassL oader类。
-
扩展(Extension)类加载器:它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext 子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java 类,是java.lang.ClassLoader类的子类。
-
系统(System) 类加载器,也称为应用类加载器(Application):它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path 所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。系统类加载器是纯Java类,是java.lang.ClassLoader 类的子类。
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承CLassLoader类。
类的加载
JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告这错误(LinkageError错误)
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类加载器的双亲委托机制
在双亲委托机制中,各个加载器按照父子关系形成了逻辑上的树形关系,除了根类加载器之外,其余的类加载器都有且只有一个父加载器。
当一个类加载器想要加载一个类的时候,并不会立刻由自己去完成这个加载,而是将这个动作提交给父加载器,如果父加载器之上还有父加载器,依旧会向上传递,直到到达根类加载器,根类加载器是没有父加载器的,便开始尝试执行加载操作,如果发现自己加载不到这个类,此时并不会抛异常,而是又把这个加载操作原路传递回去,各个层级的加载器依次尝试加载,如果一直回到最初的加载器依旧加载失败时,就会抛出异常。而如果上面任意一个父加载器加载成功了,整个流程就算成功了。
注意,并不是所有虚拟机都一定遵循双亲委托机制,这里特指Oracle的,也就是官方的虚拟机实现。比如OSGI就打破了双亲委托机制,并不是完全按照标准来的。
下面来做一个小实验来初探类加载器:
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class c = Class.forName("java.lang.String");
System.out.println(c.getClassLoader());
Class c1 = Class.forName("fun.lain.laintest.jvm.C2");
System.out.println(c1.getClassLoader());
}
}
class C2{
}
输出结果:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
首先我们来看一下class.getClassLoader()这个方法。下面是该方法源代码的注释:
/**
* Returns the class loader for the class. Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
*
* <p> If a security manager is present, and the caller's class loader is
* not null and the caller's class loader is not the same as or an ancestor of
* the class loader for the class whose class loader is requested, then
* this method calls the security manager's {@code checkPermission}
* method with a {@code RuntimePermission("getClassLoader")}
* permission to ensure it's ok to access the class loader for the class.
*
* <p>If this object
* represents a primitive type or void, null is returned.
*
* @return the class loader that loaded the class or interface
* represented by this object.
* @throws SecurityException
* if a security manager exists and its
* {@code checkPermission} method denies
* access to the class loader for the class.
* @see java.lang.ClassLoader
* @see SecurityManager#checkPermission
* @see java.lang.RuntimePermission
*/
可以看到,注释开头的几句就能解决我们的问题,首先getClassLoader方法是获取加载了该类的类加载器,然后在某些实现中,会通过返回null来标识根类加载器,本方法也会返回null来表示根加载器。
通过上面双亲委托机制的图,我们也可以看出,根类加载器是会从rt.jar中寻找类的,而String确实是在rt.jar包中,因此实际加载了String类的类加载器就确实是根类加载器,getClassLoader通过返回null来表示了它。
而C2是在我们的工程目录下的,根类加载器并不会去工程目录下寻找类,因此会返回到AppClassLoader,由它来加载classpath下的类,因此C2获取到的类加载器是AppClassLoader,和输出结果一致。
这里我们再来回顾一下类加载的问题:
如下所示:
class CL{
static {
System.out.println("Lain");
}
}
public class ClassLoadTest {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("----------");
Class clazz = ClassLoader.getSystemClassLoader().loadClass("fun.lain.jvm.CL");
System.out.println("----------");
clazz = Class.forName("fun.lain.jvm.CL");
System.out.println("----------");
System.out.println(clazz);
}
}
输出结果:
----------
----------
Lain
----------
class fun.lain.jvm.CL
我们这里获取了一个SystemClassLoader,通过系统加载器(应用加载器),去加载项目中的CL类,此时并不会触发CL的初始化,但反射就会,同样是获得一个类对象,通过类加载器加载,是不会触发初始化的。