类加载就是虚拟机将java的Class文件加载到内存,并对数据进行验证,准备,解析,初始化的一个过程。将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
一、类的生命周期
首先附上一张类的生命周期的一张图:
类的生命周期分为 加载、链接、初始化、使用、和卸载五个阶段。其中链接阶段又分为验证、准备和解析,使用阶段分为对象初始化、垃圾搜集和对象终结。在这些阶段中发生的顺序基本都是确定的,唯有解析阶段不确定,有可能发生在初始化之前,也有可能发生在初始化之后,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
-
加载
加载为类装载的第一阶段,它会获取类的的二进制流,它会将calss文件中的类的信息转换为方法区的数据结构,在java堆中生成相对应的java.lang.Class对象,作为对方法区中这些数据的访问入口。相对于类加载的其他阶段而来说,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
获取类的二进制流的方法:
①. 读取本地系统中直接加载
②. 将java源文件动态编译为.class文件
③. 读取jar文件中的.class文件
④. 从网络上下载.class文件
-
链接
①. 验证(为了保证class流的格式是正确的)
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。其中包含文件格式验证(是否以0xCAFEBABE开头、版本号是否合理);元数据验证(是否有父类、是否继承了final类、非抽象类是否实现了所有的抽象方法);字节码验证(运行检查、栈数据类型和操作码数据参数是否吻合、跳转指令是否指定到合适的位置);符号引用验证(常量池中描述的类是否存在、访问的方法或字段是否存在并且有足够的权限)。
②. 准备(分配内存,并为类设置初始值)
在方法区中分配内存,并为类设置初始值。例如:public static int v = 1 这段代码在准备阶段v会被设置为默认值0,而不是1,只有在初始化阶段v才会被置为1,而常量则会在初始化阶段直接置为设置的值
③. 解析(把类中的符号引用转换为直接引用)
符号引用是一组符号来描述目标,可以是任何字面量。直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
-
初始化
①. 执行类的构造器 static变量赋值,执行static语句
②. 子类的调用前要保证父类被调用
③. 是线程安全的
下面简单看一个例子:
/** * * @author Herrt灬凌夜 * */ class SuperClass { static { System.out.println("SuperClass init!"); } public static int value=123; } class SubClass extends SuperClass{ static { System.out.println("SubClass init!"); } } public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); } }
执行结果为:
SuperClass init! 123
从上例可以看出,在调用SubClass.value时首先初始化了SubClass的父类SuperClass,并且执行了SuperClass类中的静态代码块,初始化了value的值,但是并没有去触发SubClass类的初始化。只有当程序”首次”并且是”主动使用”类的时候,才会执行初始化。
主动使用:
①. 创建类的实例
②. 访问类的静态变量、或给该类的静态变量赋值
③. 调用类的静态方法
④. 反射调用类的静态方法、或反射创建类实例
⑤. JVM启动时被标明为启动类
⑥. 初始化一个类的子类
- 使用
①. 对象实例化:当我们去实例化一个对象时,首先虚拟机会去常量池定位这个类的符号引用,并检查这个类是否被加载,解析和初始化过,如果没有的话就需要首先执行这几个过程,如果已经执行过这几个过程,那么虚拟机就会为新生对象分配内存(一般是指针碰撞或者空闲列表方式),将内存空间进行对象初始化(初始值全部为对应0值),设置对象头信息,将对象引入栈,执行构造器
②. 垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收(在前面的文章中有写到GC回收算法)
③. 对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头
- 卸载
即类的生命周期走到了最后一步,程序中不再有该类的引用,该类也就会被JVM执行垃圾回收,从此生命结束…(满足下面三个条件时该类就会被回收)
①. 当一个类在java堆中没有任何实例时
②. 该类的ClassLoader已经被回收
③. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
二、什么是类装载器 ClassLoader
Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。Classloader是一个抽象类,它的实例将读入的java字节码装载到JVM中,可以单独定制,满足不同的字节码流获取方式,主要负责类装载过程中的加载阶段。
大部分java程序会使用以下3中系统提供的类加载器:
- 启动类加载器(Bootstrap ClassLoader): 这个类加载器负责将存放在<java_home>lib目录中的,或者被 -Xbootclasspath参数所指定的路径中的,并且在巡检识别的(仅仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会被加载)类库加载到虚拟机中。此类加载器并不继承于java.lang.ClassLoader,由原生代码(如C语言)编写,不能被java程序直接调用。
- 扩展类加载器(Extendsion ClassLoader):此类负责加载<java_home>libext目录中的,或者被java.ext.dirs系统变量所指定的路径的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader): 这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器.一般情况下这就是系统默认的类加载器.
三、JDK中 ClassLoader默认设计模式(双亲委派模式)
双亲委派模式:一个类加载器接受到加载类的请求时,首先会去看自己是否加载过这个类,如果加载过则直接返回,如果没有加载过,不会立即去尝试加载这个类,而是把请求委托给父加载器,直到Bootstrap ClassLoader。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
我们在开发过程中偶尔会出现ClassNotFoundException的异常,那么在什么情况下才会出现这样的异常呢?
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
自底向上检查类是否加载,自顶向上尝试加载类。
在classLoader中比较重要的几个方法:
方法名 | 入参 | 出参 | 描述 |
---|---|---|---|
loadClass | String name | Class | 根据Class的名字去装载这个class,并且返回这个Class |
defineClass | byte[]b,int off, int len | final Class | 定义一个class, 给定一个byte数组,偏移量和长度,就是将class的文件以流信息传入,然后转换为一个class |
findClass | String name | Class | 在loadClass方法中做调用,查找class,自定义classLoad时推荐是重载这个方法 |
findLoadedClass | String name | final Class | 查找已经被加载的class,如果查找不到再去加载这个类,如果找到则不去做二次加载 |
在loadClass中,首先去找这个class,如果找到则返回,确保一个类只会被加载1次,否则则委托父级类加载器去加载。
注意:此处的 parent 不是该类的父类,而是父加载器。
我们看一个简单的例子:
package com.wyx.service; public class FindClassOrder { public static void main(String[] args) { HelloLoder loder = new HelloLoder(); loder.print(); } }
package com.wyx.service; public class HelloLoder { public void print () { System.out.println("I am apploader"); } }
我们直接执行main方法,发现输出的是 I am apploader
然后我们将在在其他地方创建相同的类,注意包名也要相同。将此类的class文件放到E盘的tmp目录下,注意包也要一级一级创建,执行时在 VM arguments 加上-Xbootclasspath/a:E: mp
package com.wyx.service;
public class HelloLoder {
public void print () {
System.out.println("I am bootloader");
}
}
此时再次执行,我们发现执行的结果不再是 I am apploader 而是 I am bootloader 了。
上面例子中,首先我们执行时首先去 AppClassLoader 中看这个类是否在这个类加载器中被加载,没有找到就去ExtClassLoader中找,ExtClassLoader中也没有找到,于是去BootStrapClassLoader 中找,此时也没有找到就开始加载,此时没有指定BootStrapClassLoader加载的位置,于是没有找到 HelloLoder.class,所以继续交给ExtClassLoader去加载,ExtClassLoader也没有找到 HelloLoder.class,于是交给AppClassLoader加载,在AppClassLoader中找到的 HelloLoder.class中输出的是 I am apploader 。 当我们指定 BootStrapClassLoader的加载位置为 -Xbootclasspath/a:E: mp 时,在BootStrapClassLoader加载时就找到了 HelloLoder.class 文件,所以BootStrapClassLoader就直接加载了这个类,而不是等到AppClassLoader去加载。而此时这个类中的输出为 I am bootloader 。这个例子说明了类的加载是从上向下的。
我们再来看一个例子,在这个例子中我们同样指定BootStrapClassLoader加载的位置 :-Xbootclasspath/a:E: mp
public class FindClassOrder { public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { //获取类加载器 ClassLoader cl = FindClassOrder.class.getClassLoader(); //获取要加载的类 byte[] buffer = null; try { File file = new File("D:\Users\wuyouxin\eclipse-workspace\springBootTest\target\classes\com\wyx\service\HelloLoder.class"); FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(1000); byte[] b = new byte[1000]; int n; while ((n = fis.read(b)) != -1) { bos.write(b, 0, n); } fis.close(); bos.close(); buffer = bos.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } Method md_defineClass = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class,int.class,int.class); //因为 defineClass 是protected final的,如果不设置则不可调用 md_defineClass.setAccessible(true); //强制使用AppClassLoader加载 HelloLoder.class md_defineClass.invoke(cl, buffer, 0, buffer.length); md_defineClass.setAccessible(false); HelloLoder loder = new HelloLoder(); System.out.println(loder.getClass().getClassLoader()); loder.print(); } }
不同的是我们使用 AppClassLoader 强制加载了 第一个 HelloLoder.class,所以此时的执行结果为:
我们可以看出加载的类加载器为AppClassLoader ,执行的语句也为 I am apploader,所以当系统需要使用HelloLoder类时,直接去AppClassLoader 找类有没有被加载,现在找到了就不继续向父级类加载器发起委派了。此例说明,类的检查是否被加载是自底向上的。
四、上下文加载器(Thread.SetContextClassLoader())
但是,这种双亲委派模式存在一个问题,就是查看类加载都是从下往上的,所以,在顶层的classLoder中无法加载底层classLoder的类,也就是说在 BootStrapClassLoader 中无法加载 应用层的类,而在rt.jar中有些类中的方法可能在我们应用中被重写了,但是正在 BootStrapClassLoader 中是无法加载到重写的方法的。 但是有时候就需要在 BootStrapClassLoader 去访问应用class中加载的类,这种模式就无法做到。为了解决这个问题就提出了一个上下文加载器,他用以解决顶层classLoader无法访问底层classLoader的类的问题,基本思想就是在顶层classLoader中传入底层classLoader的实例。它可以任务是一个角色,因为他不是一个具体的classLoder,他可以由任何一个classLoader来做这个上下文加载器。
下面这个方法来自于javax.xml.parsers.FactoryFinder类中的getProviderClass 方法,我们可以看出其中加载时将 cl 传入后加载className 而此时的 cl 就作为一个上下文加载器。
五、双亲模式的破坏
在jdk中默认的classLoader的加载模式是双亲委派模式,但是并不是必须要这么去做。比如tomcat中的WebappClassLoader就是先加载自己的class,找不到再去委派给上级的 classLoader。 而OSGi(热部署)的classLoader的结构是网状的,它内部有自己的一套算法,根据自己的需要去自由的加载class。
六、自定义 ClassLoader
下面代码是自定义 OrderClassLoader 中的部分代码:其中就是首先加载自己的类,如果无法加载则在委托上级加载器去加载。
/** * 自定义 classLoder * @author Herrt灬凌夜 * */ public class OrderClassLoader extends ClassLoader { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class re = findClass(name); if (re == null) { System.out.println("无法载入类:" + name + "需要委托父加载器"); return super.loadClass(name, resolve); } return re; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //首先去找这个classloder中是否有加载这个类 Class clazz = this.findLoadedClass(name); //如果没有,则去加载 if (null == clazz) { try { File file = new File(name); FileInputStream fis = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(1000); byte[] b = new byte[1000]; int n; while ((n = fis.read(b)) != -1) { bos.write(b, 0, n); } fis.close(); bos.close(); byte[] buffer = bos.toByteArray(); //加载类 clazz = defineClass(name, buffer, 0, buffer.length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return clazz; } }
-------------------- END ---------------------
最后附上作者的微信公众号地址和博客地址
公众号:wuyouxin_gzh