JVM的类加载机制
- 类从被加载到虚拟机内存中开始,到卸载出内存为止,他的整个生命周期包括:加载(loading), 验证(Verification),准备(Preparation),解析(Resloution),初始化(initialization),使用(Using)和卸载(OnLoading) 七个阶段,其中,验证,准备,解析,三个部分统称为链接(linking)
每个类加载的流程
- 以上的加载,验证,准备,解析的流程是基本固定化的,也就是对于大部分类使用之前都必须按照这个流程进行,但是解析的流程不确定,现目前jvm存在动态解析,所以解析这个流程比较重要`
- 注意,验证这个流程是在管道搭建完成后,通过IO读取数据的时候,就会执行验证流程,如果验证不通过,就会报
java.lang.CLassFormatError
异常
JVM类加载各个流程拆解
加载(Loading)
- 加载Loading阶段就是JVM第一次去加载和读取对应位置上的文件,上述的整个类加载过程其实都是类加载器在完成。相当于正式建立了IO通道,在这通道上,我们需要作上述的一系列事情,比如验证,准备,解析等
JVM再什么情况下会加载一个类?JVM如何加载一个类
Java程序运行的时候,首先会将包含Main方法的启动类加载到内存中,如果这个启动类中又创建了一个对象,那么,这个时候JVM就会在内存中扫描是否含有这个类,如果没有就会去将被创建类的字节码文件加载到内存中
运行并查看JVM类加载相关的指令
java-XX:TranceClassLoading-cp .cn.itsource.load.LoadTest
执行效果:
验证(Verification)
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《JAVA虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全
验证主要包括四种验证:文件格式验证(魔数CAFEBABE),元数据验证,字节码验证,符号引用验证,简单来说就是我们的.class文件是否符合JVM规范,是否有被篡改,否则JVM是没法执行该字节码文件的
每个符合规范的JAVA文件二进制开头应该是对应的魔数CA FE BA BE
如果字节码文件的二进制文件的开头CA FE BA BE被修改之后,当运行的时候,会报异常:
准备(Preparation)
准备阶段是正式为类中定义的变量,及
静态变量(被statistic修饰的变量)
,分配内存并设置类变量的初始值的阶段JVM在准备阶段的操作流程图
准备阶段工作流程梳理
当JVM通过加载阶段创建通道之后,加载的.class文件通过验证流程之后,会先将.class文件解析成为一个.class对象模板,存放在方法区的元空间中,但是此时并不会创建对象,当对象模板解析完成之后,会在堆内存中创建一个.class字节码文件对象,然后给对象中的静态变量赋初始化值,后续在程序中不论是new对象还是通过反射创建对象,都是以堆内存中class对象作为模板来复制
注意
- statistic变量在JDK7之前存储于instanceKlass末尾,从JDK7开始,存储于 java_mirror末尾,换句话说,1.7之前是存在于方法区,1.7之后是存在于堆内存中
java_mirror
:一个指向堆内存对象的引用属性
特殊情况
static变量分配空间何赋值是两个步骤,分配空间(内存分配)在准备阶段完成,赋值在初始化阶段完成
关于准备阶段,还有两个容易产生混淆的概念需要着重强调,首先是这时候进行内存分配的变量仅包括静态变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆内存当中,其次是这里所说的初始值“通常情况下”是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123,因为这个时候尚未开始执行任何java方法,而把value赋值为123的put static指令是程序被编译后,存放于类构造器()方法中,所以把value赋值为123的动作要到类的初始化阶段才会被执行
如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
上面提到在'通常情况下'初始值是零值,那言外之意就是会有些'特殊情况';如果类字段的字段属性表中存在ConstanValue属性,那再准备阶段变量值就会被初始化为ConstanVlaue属性所指定的初始化值,假设上面的类变量定义为:
public stattic final int value = 123;
编译时javaC将会为value生成
ConstanValue
属性,在准备阶段虚拟机就会根据ConstanValue
属性来给value变量赋值为123如果static变量是final的,但是属于引用类型,那么赋值也会在初始化阶段完成
解析阶段
- 主要将常量池中的符号引用替换为直接引用的过程
- java代码在进行javaC编译的时候,在虚拟机加载Class文件的时候进行动态链接,在Class文件中不会保存各个方法,各个属性的布局信息,这些字段,方法的符号引用不经过虚拟机在运行期间转换的话是无法得到真正的入口地址,也就无法直接被虚拟机使用的,当虚拟机唑类加载时,将会从常量池中获取对应的符号,引用,再在类创建时或运行时解析,翻译到具体的内存地址中
- 符号引用(Symbolic References)
- 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到到虚拟机内存当中的内容,各种虚拟机实行的内存布局可以各不相同,但是他们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义再《JAVA虚拟机规范》的class文件格式中
- 直接引用(DIrect References)
- 直接引用是可以直接指向目标的指针,相对偏移量或者是一个能直接间接定位达到目标的句柄,直接引用是和虚拟机实现的内存布局直接相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在JVM的内存中存在
初始化阶段
通过准备阶段类变量已经赋过一次系统要求的初始化零值,而初始化阶段就是在给类变量进行赋值操作
初始化阶段会执行类构造器方法(),该方法不同于类构造器(是虚拟机视角下的()方法);该方法不需要定义,是javaC编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而成
同理,静态代码块的操作也是在Clinit方法中执行
注意
- 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块中是可以赋值,但是不能访问(
如下代码,编译是可以通过的,但是在执行的时候会报错
)
- 编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块中是可以赋值,但是不能访问(
父类初始化先执行
- ()方法与类的构造函数(及在虚拟机视角中的实例构造器方法)不同,他不需要显式的调用父类构造器,java虚拟机会保证在子类的方法执行之前,父类的方法已经执行完毕,因此在java虚拟机中第一个被执行的方法的类型一定是java.lang.Object
线程同步
- java虚拟机必须保证一个类的方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的方法,其他线程都需要阻塞等待,直到活动线程执行完毕方法,如果在一个类的方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
- 其他线程虽然会被阻塞,但如果执行方法的那条线程退出方法之后,其他线程唤醒后则不会在次进入方法,同一个类加载器下,一个类型只会被初始化一次
总结
- 一个类如果已经被一个线程加载到内存中,也就代表加载过程中的这些阶段(加载-连接)都已经执行完毕,那么后续的线程想要访问对应的字节码对象直接访问即可,不需要再次去加载这个类的字节码文件了
对应了我们javaSE中的语法问题,一个类中的静态修饰的内容会随着类的加载而加载,并且只会被加载一次 《这个说法仅仅限于同一个类加载器》
总结
- 类加载阶段:建立通道:本地磁盘到内存的通道
- 验证阶段:校验字节码文件是否符合JVM的规范
- 准备阶段:分配内存空间,给类变量设置初始化值
- 解析阶段:将符号引用(占位符)转化为直接引用(真实内存地址)
- 初始化阶段:给类变量进行赋值的操作