类加载器子系统
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
# 1.1 类加载流程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
加载,验证,准备,解析,初始化,使用,卸载这7个阶段,其中验证、准备、解析3个部分统称为连接。JVM没有规定类加载的时机,但却严格规定了五种情况下必须立即对类进行初始化,加载自然要在此之前。
- 运行JVM必须指定一个含有main方法的主类,虚拟机会先初始化这个类。
- 遇到new、getstatic、putstatic、invokestatic这四条指令时,如果类没有被初始化,则首先对类进行初始化。
- 使用java.lang.reflect包的方法对类进行反射调用时,若类没有进行初始化,则触发其初始化。
- 当初始化一个类时假如该类的父类没有进行初始化,首先触发其父类的初始化。
- 当使用Jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putstatic、REF_inokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,触发该类初始化。
# 1、加载
在加载的过程中,虚拟机会完成以下三件事情:
通过一个类的全限定名加载该类对应的二进制字节流。
将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区各个类访问该类的入口。
# 2、验证
这一步的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。具体验证的东西如下:
文件格式验证:这里验证的时字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据的验证:就是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,例如:这个类是否有父类,除了 java.lang.Object之外。
字节码校验:字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:① 在字节码的执行过程中,是否会跳转到一条不存在的指令。② 函数的调用是否传递了正确类型的参数。③ 变量的赋值是不是给了正确的数据类型等。
符号引用验证:虚拟机在将符号引用转化为直接引用,验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError,如果一个 方法无法被找到,则会抛出NoSuchMethodError;这个转化动作将在连接的第三个阶段-解析阶段中发生。
# 3、准备
为类变量(static修饰的变量)分配内存并且设置该类变量的默认初始值,即零值,初始化阶段才会设置代码中的初始值
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化
这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配给Java堆中。
# 4、解析
解析阶段是虚拟机将常量池 (opens new window)内的符号引用(类、变量、方法等的描述符 [名称])替换为直接引用(直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄 [地址])的过程
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行
# 5、初始化
初始化阶段编译器会将类文件声明的静态赋值变量和静态代码块合并生成
<clinit>方法并进行调用。
初始化阶段就是执行类构造器方法
<clinit>的过程,这个方法不需要定义,只需要类中有静态的属性或者代码块即可,javac编译器自动收集所有类变量的赋值动作和静态代码块中的语句合并而来构造器方法中指令按照源文件出现的顺序执行
如果该类有父类,jvm会保证子类的
<clinit>在执行前,执行父类的<clinit>虚拟机必须保证一个类的
<clinit>方法在多线程情况下被加锁,类只需要被加载一次
# 1.2 类加载器分类
JVM层面支持两种类加载器:
启动类加载器和自定义类加载器,启动类加载器由C++编写,属于虚拟机自身的一部分;继承自java.lang.ClassLoader的类加载器都属于自定义类加载器,由Java编写。逻辑上我们可以根据各加载器的不同功能继续划分为:扩展类加载器、应用程序类加载器和自定义类加载器。
# 1、启动类加载器
由C/C++语言实现,嵌套在JVM内部
负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
没有父加载器,加载扩展类和应用程序类加载器,并作为他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
# 2、扩展类加载器
由Java语言实现,派生于ClassLoader类
负责加载java.ext.dirs系统属性所指定目录中的类库,或JAVA_HOME/jre/lib/ext目录(扩展目录)下的类库,如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载
作为类(在rt.jar中)被启动类加载器加载,父类加载器为启动类加载器
# 3、应用程序类加载器
由Java语言实现,派生于ClassLoader类
负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
作为类被扩展类加载器加载,父类加载器为扩展类加载器
该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器,所以有些场合中也称它为“系统类加载器”
# 4、自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。自定义类加载器作用:
隔离加载类(相同包名和类名的两个类会冲突,引入自己定义类加载器可以规避冲突问题)
修改类加载的方式
扩展加载源(默认从jar包、war包等源加载,可以自定义自己的源)
防止源码泄漏(对编译后的class字节码进行加密,加载时用自定义的类加载器进行解密后使用)
# 1.3 类加载器协作方式
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成Class对象,当触发类加载时,JVM并不知道当前类具体由哪个加载器加载,都是先给到默认类加载器(应用程序类加载器),默认类加载器怎么分配到具体的加载器呢,这边使用了一种叫双亲委派模型的加载机制。
# 1、双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。举例如下:
当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
# 2、全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
# 3、缓存机制
- 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
- 这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
- 使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
- 相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。