类加载器及双亲委派模型
●类加载器
类加载的过程
类加载器
●双亲委派模式
工作流程
优势
原理及源码分析
URLClassLoader
扩展类加载器和系统类加载器
●类加载器间的关系
类与类加载器
显式加载和隐式加载
●自定义类加载器
类加载器
1.1 类加载的过程
”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象。而将class文件加载到虚拟机内存的过程称为类加载,具体流程如下:
加载:将class字节码文件从各个来源通过类加载器装载入虚拟机内存
一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,远程网络实时编译的字节流
验证:保证加载进来的字节流符合虚拟机规范,不会造成安全错误
包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
准备:为类变量分配内存,并且赋予初值
(类变量:又称静态变量,由static关键字修饰)
这里所说的初值不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值。注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
解析:将常量池内的符号引用替换为直接引用的过程
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用
初始化:对类变量初始化,是执行类构造器的过程
换句话说,只对static修饰的变量或语句进行初始化
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
前面提到的例子static int i=5;中,只初始化了默认值的static变量i将会在这个阶段赋值为5
1.2 类加载器
类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象的工具。如果站在JVM的角度来看,只存在两种类加载器:
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库(jar包)加载到内存中
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
●扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库
●应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,开发者可以直接使用应用程序类加载器。一般情况下该类加载器是程序中默认的类加载器
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示:
需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说只有当需要使用该类时,才会将它的class文件加载到内存生成class对象。而且如上图所示,在加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,下面我们来具体了解一下双亲委派模式
2 双亲委派模式
2.1 工作流程
如果一个类加载器收到了类加载请求,它首先不会自己去加载这个类,而是将这个类委托给父加载器去完成,每一层加载器都是如此,因此所有加载请求最终应该传送到顶层的启动类加载器中,只有父加载器反馈无法加载时,子加载器才会自己尝试去加载
2.2 优势
通过这种模式,使得Java类具备了一种带有优先级的层次关系,利用这种层次关系可以避免类的重复加载。当父类已经执行了类的加载操作,子类的ClassLoader就不会再重复执行
其次考虑到了安全因素。双重委派模型保证了Java的核心api不会被随意替换
2.3 原理及源码分析
从图可以看出顶层的类加载器是ClassLoader类,它是一个抽象类。除启动类加载器外,所有的类加载器都继承自ClassLoader。ClassLoader类中有以下几个重要方法:
●loadClass(String)
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法。loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
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
// 如果都没有找到,则通过自定义实现的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(c);
}
return c;
}
}
正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载器的父加载器去加载。倘若没有父加载器,则交给启动类加载器(Bootstrap ClassLoader)去加载。最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)
●findClass(String)
JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(关于defineClass()稍后会进一步介绍),ClassLoader类中findClass()方法源码如下:
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
●defineClass(byte[] b, int off, int len)
defineClass()方法用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑)。通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。前面提到findClass方法通常是和defineClass方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行
●resolveClass(Class≺?≻ c)
使用该方法可以使类的Class对象在创建完成的同时被解析
2.4 URLClassLoader
ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁
从类图结构看出URLClassLoader中存在一个URLClassPath类,负责找到要加载的字节码,再读取成字节流,最后通过defineClass()方法创建类的Class对象
从URLClassLoader类的结构图可以看出其构造方法都有一个必须传递的参数URL[],该参数代表字节码文件的路径
URL[]也是URLClassPath类的必传参数。在创建URLClassPath对象时,会根据传递过来的URL[]中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认Loader类去加载相应路径下的class文件,
当JVM调用findClass()方法时,就由这3个加载器中的一个将class文件的字节码流加载到内存中,最后利用字节码流创建类的class对象。请记住,如果我们在定义类加载器时选择继承ClassLoader类而非URLClassLoader,必须手动编写findclass()方法的加载逻辑以及获取字节码流的逻辑
2.5 扩展类加载器和系统类加载器
拓展类加载器ExtClassLoader和系统类加载器AppClassLoader都继承与URLClassLoader,是由sun.misc.Launcher创建的静态内部类,其主要类结构如下:
我们发现ExtClassLoader并没有重写loadClass()方法,这说明其遵循双亲委派模式,而AppClassLoader虽然重载了loadCass()方法,但该方法只是增加了新增了包权限检测功能,最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式,其重载方法源码如下:
/**
* Override loadClass 方法,新增包权限检测功能
*/
public Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));//包权限检测
}
}
//依然调用父类的方法
return (super.loadClass(name, resolve));
}
3 类加载器间的关系
我们进一步了解类加载器间的关系(注意这里的关系并非指继承关系),主要可以分为以下4点:
●启动类加载器,由C++实现,没有父类。
●扩展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
●应用程序类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
●自定义类加载器,父类加载器为AppClassLoader
Lancher的构造器源码:
public Launcher() {
ClassLoader extcl;
try {
// 首先创建拓展类加载器
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader");
}
// Now create the class loader to use to launch the application
try {
//再创建AppClassLoader并把ExtClassLoader传递给它作为父类加载器
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader");
}
//设置线程上下文类加载器,稍后分析
Thread.currentThread().setContextClassLoader(loader);
//省略其他没必要的代码......
}
}
显然Lancher初始化时首先会创建ExtClassLoader类加载器,然后再创建AppClassLoader并把ExtClassLoader传递给它作为父类加载器,这里还把AppClassLoader默认设置为线程上下文类加载器,关于线程上下文类加载器稍后会分析。那ExtClassLoader类加载器为什么是null呢?看下面的源码创建过程就明白,在创建ExtClassLoader强制设置了其父加载器为null
//构造方法
public ExtClassLoader(File[] dirs) throws IOException {
//调用父类构造URLClassLoader传递null作为parent
super(getExtURLs(dirs), null, factory);
}
//URLClassLoader构造
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
}
4 类与类加载器
4.1 类与类加载器
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件
●类的完整类名必须一致,包括包名
●加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是覆写loadclass方法(跳过缓存查找)。从前面双亲委派模式对loadClass()方法的源码分析中可以知,在方法第一步会通过Class<?> c = findLoadedClass(name);从缓存查找,类名完整名称相同则不会再次被加载,因此我们必须绕过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法,这样也可以避免从缓存查找,如下
String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//直接调用findClass()方法创建类的Class对象,以避免缓存查找
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");
System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());
/**
* 直接调用findClass方法输出结果:
* findClass->obj1:723074861
findClass->obj2:895328852
生成不同的实例
*/
如果调用父类的loadClass方法,由于缓存查找机制的存在,结果如下,除非重写loadClass()方法去掉缓存查找步骤,不过现在一般都不建议重写loadClass()方法
//直接调用父类的loadClass()方法
Class<?> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class<?> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");
//不同实例对象的自定义类加载器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系统类加载器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());
/**
* 直接调用loadClass方法的输出结果,注意并没有重写loadClass方法
* loadClass->obj1:1872034366
loadClass->obj2:1872034366
Class-> obj3:1872034366
都是同一个实例
*/
所以,如果不从缓存查询相同完全类名的class对象,那么在ClassLoader的实例对象不同的情况下,同一字节码文件创建的class对象是不同的
4.2 显式加载和隐式加载
所谓class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式,显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。在日常开发以上两种方式一般会混合使用
5 自定义类加载器
通过前面的分析可知,实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。那么编写自定义类加载器的意义何在呢?
●当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象
●当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑
●当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑
自定义类加载器的具体内容请参考原文:深入理解Java类加载器(ClassLoader)