十、类加载器原理
我们自己编写的.java的源文件,经过编译器编译成.class的字节码文件,再把字节码文件装载到JVM中,映射到各个内存区域中,我们的程序就可以在内存中运行了。那么字节码文件是怎样装载到JVM中的呢?中间经过了哪些步骤?常说的双亲委派模式又是怎么回事?
(一)、类装载流程(3大步+5小步)
一个Java类的生命周期:
加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
1、加载
加载是类装载的第一步,首先通过.class字节码文件的路径读取到二进制流,并解析二进制流。将里面的元数据(类型、常量等)载入到方法区,在java堆中生成对应的java.lang.Class对象。加载来源如下:
- 1.本地.class文件
- 2.jar包的.class文件
- 3.网络.class文件
- 4.把一个java源文件动态编译,加载
2、连接分为3步,验证、准备、解析
2.1、验证
验证的主要目的就是判断class文件的合法性。
①比如class文件一定是以0xCAFEBABE(特定形式)开头的。
②另外对版本号也会做验证,例如如果使用java1.8编译后的class文件要在java1.6虚拟机上运行,因为版本问题就会验证不通过。
③除此之外还会对元数据、字节码进行验证,具体的验证过程就复杂的多了,可以专门查看相关资料去了解。
2.2、准备
准备过程就是为类的静态变量分配内存,并将其初始化为默认值。例如:
①public static int v=1;这段代码在准备阶段v的值就会被初始化为0,只有到后面类初始化阶段时才会被设置为1。
②但是对于static final(常量),在准备阶段就会被设置成指定的值,例如:public static final int v=1;这段代码在准备阶段v的值就是1。
2.3、解析
解析过程就是将符号引用替换为直接引用。例如某个类继承java.lang.object,原来的符号引用记录的是“java.lang.object”这个符号,凭借这个符号并不能找到java.lang.object这个对象在哪里?而直接引用就是要找到java.lang.object所在的内存地址,建立直接引用关系,这样就方便查询到具体对象。
3、初始化
初始化过程,主要包括执行类构造方法、static变量赋值语句,staic{}语句块,需要注意的是如果一个子类进行初始化,那么它会事先初始化其父类,保证父类在子类之前被初始化。所以,在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。
(1)类什么时候才被初始化 (类初始化不一定是通过new)
1)创建类的实例,也就是new一个对象
2)访问某个类或接口的静态变量,或者对该静态变量赋值
3)调用类的静态方法
4)反射(Class.forName(“com.lyj.load”))
5)初始化一个类的子类(会首先初始化子类的父类)
6)JVM启动时标明的启动类,即文件名和类名相同的那个类
(2)类的初始化顺序
1)如果这个类还没有被加载和链接,那先进行加载和链接
2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3)假如类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语句。
4)总的来说,初始化顺序依次是:
a.如果是一个类:(静态变量、静态代码块)–>(普通变量、普通代码块)–> 构造器
b.如果是子类继承一个父类:父类静态变量–>父类静态代码块 –> 子类静态变量–>子类静态代码块 –> 父类非静态变量–>父类普通代码块->父类默认构造方法-> 子类非静态变量–>子类普通代码块->子类构造方法
说完了类加载过程,我们来介绍一下这个过程当中的主角:类加载器。
(二)类加载器
类加载器ClassLoader,它是一个抽象类,ClassLoader的具体实例负责把java字节码读取到JVM当中,ClassLoader还可以定制以满足不同字节码流的加载方式,比如从网络加载、从文件加载。ClassLoader的实例负责整个类装载过程中的“加载”阶段。
系统中的ClassLoader:
1、BootStrap Classloader (启动ClassLoader)
2、Extension ClassLoader (扩展ClassLoader)
3、App ClassLoader(应用 ClassLoader)
4、Custom ClassLoader(自定义ClassLoader)
每个ClassLoader都有另外一个ClassLoader作为父ClassLoader,BootStrap Classloader除外,它没有父Classloader。
(三)ClassLoader加载机制如下:
从下往上依次检查类是否被加载。一般情况下,首先从App ClassLoader中调用findLoadedClass方法查看是否已经加载,如果没有加载,则会交给父类Extension ClassLoader;Extension ClassLoader去查看是否加载,还没加载,则再调用其父类BootstrapClassLoader;BootstrapClassLoader查看是否已经加载,如果仍然没有,从上往下尝试加载类,那么从 Bootstrap ClassLoader到 App ClassLoader依次尝试加载。
上述过程叫做双亲模式。
值得注意的是即使两个类来源于相同的class文件,如果使用不同的类加载器加载,加载后的对象是完全不同的,这个不同反应在对象的 equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用 instanceof 关键字对对象所属关系的判定结果。
(四)双亲模式
1、问题:顶层ClassLoader(BootstrapClassLoader),无法加载底层ClassLoader(App ClassLoader)的类。
2、问题举例:javax.xml.parsers包中定义了xml解析的类接口,Service Provider Interface SPI 位于rt.jar,即接口在启动ClassLoader中。而SPI的实现类,在AppLoader。这样就无法用BootstrapClassLoader去加载SPI的实现类。
3、解决方式:JDK中提供了一个方法用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题:
Thread. setContextClassLoader()
基本思想是在顶层ClassLoader中,传入底层ClassLoader的实例。
(五)双亲模式的破坏
双亲模式是默认的模式,但不是必须这么做。例如,Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent;OSGi的ClassLoader形成网状结构,根据需要自由加载Class。