【JVM】详解 类加载器与类加载过程
目录
类加载器
类型
双亲委派模型
破坏双亲委派模型
类加载流程
类加载的时机
加载
连接
验证
准备
解析
初始化
模块下的类加载器
类加载器
类的唯一性是由这个类本身和加载这个类的类加载器共同确定的。如果加载类的类加载器不同,那么这两个类一定不相同。
类型
在JVM视角来看,只有两种类加载器:启动类加载器和其他所有类的加载器。
但在Java开发者视角看,类加载器可以可以分成三层的类加载器模型:
- 启动类加载器(Bootstrap Class Loader):这个类加载器使用C++语言实现,是虚拟机自身的一部分。负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
- 扩展类加载器(Extension Class Loader):它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
- 应用程序类加载器(Application Class Loader):它负责加载用户类路径(ClassPath)上所有的类库。
双亲委派模型
双亲委派机制:双亲委派模型要求除最顶层之外的类加载器都得有自己的父类加载器。当有类加载请求时,必须请求至父类加载器来尝试加载类,因此所有的请求都会达到最顶层的类加载器,如果父类加载器无法加载再尝试自己加载,可以把这看作是一个递归的过程。
优点:
- 保证类的唯一性,避免了不同类加载器加载不同类的情况
- 保证安全性:类加载器只加载信任路径下的类,防止加载伪造的不可信任的类
- 保证隔离性:不同层次的类加载器满足不同的类加载需求,互不影响
破坏双亲委派模型
情形一:基础类型调回用户代码
启动类加载器是无法识别用户代码的,因此Java设计团队引入了 线程上下文类加载器(Thread Context ClassLoader)。
通过线程上下文类加载器,可以实现父类加载器去请求子类加载器完成类加载。
情形二:程序的动态性
所谓程序的动态性,就是实现Java代码的热替换。IBM通过OSGi实现了通过类加载器实现热部署。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。
类加载流程
类加载的时机
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
对于初始化阶段,规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
加载
JVM在加载的过程中完成了三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
对于数组类,数组本身由JVM直接在内存中创建出来,但数组的元素类型依旧需要类加载器加载。
连接
验证
验证分为四个阶段:文件格式验证、元数据验证、字节码验证和符号引用验证。
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。
字节码验证:主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。但是由于数据流分析和控制流分析复杂度较高,Javac编译器和Java虚拟机里进行了一项联合优化,通过 StackMapTable 把尽可能多的校验辅助措施挪到Javac编译器里进行。
符号引用验证:对常量池中的各种符号引用的各类信息进行匹配性校验,验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
准备
将类中的静态变量进行分配空间和初始化零值的过程。
如果存在静态常量,则直接赋值。因为编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置赋值。
解析
将常量池内的符号引用替换为直接引用的过程。
符号引用:符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用的目标不一定存储在JVM内存中。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用的目标一定存储在JVM内存中。
虚拟机实现可以对第一次解析的结果进行缓存,譬如在运行时直接引用常量池中的记录,并把常量标识为已解析状态,从而避免解析动作重复进行。
初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,是执行显式初始化和静态代码块内的初始化合并到一起的方法,不同于用户定义的类的构造器。
注意事项:
- 父类的<clinit>()一定比子类的<clinit>()方法先执行。
- <clinit>()方法不是必须的,如果没有静态代码块并且没有给静态变量赋值的代码,就不会生成<clinit>()。
- 接口中不能写入静态代码块。接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。当一个类实现多个接口时,多个接口的静态代码块执行顺序、资源依赖等可能引发冲突。
- Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步。
模块下的类加载器
扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代。
平台类加载器和应用程序类加载器都不再派生自URLClassLoader。启动类加载器、平台类加载器、应用程序类加载器全都继承于BuiltinClassLoader。
在BuiltinClassLoader中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。