类加载过程
欢迎光临我的博客[http://poetize.cn],前端使用Vue2,聊天室使用Vue3,后台使用Spring Boot
一个类从加载到内存开始,一直到被卸载结束,它的整个生命周期包括
加载
、链接(验证、准备、解析)
、初始化
、使用
、卸载
阶段
类初始化条件
1. 遇到new、putstatic、getstatic及invokestatic这4条字节码指令时,如果类没有初始化,则立即进行初始化
这4个命令分别代表实例化一个类、设置&读取一个静态字段(没有被final修饰)、调用类的静态方法
2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化
3. 当初始化一个类的时候,发现其父类没有初始化
4. 当虚拟机启动时,需用将执行启动的主类(有main()方法的那个类)进行初始化
5. 当使用动态语言时,如果一个java.lang.invoke.MethodHandle实例最终的解析结果是
REF_getStatic、REF_putStatic、REF_invokeStatic句柄时,并且这个句柄对应的类没有初始化
加载
加载指的是把class字节码文件从各个来源通过" 类加载器 "装载入内存中
"加载"是"类加载"这个过程的一个阶段,是 “类加载”过程中最先开始进行的操作,加载阶段,虚拟机需要完成三件事:
1、 根据类的全限定名获取定义此类的二进制字节流;
2、 将这个字节流代表的静态存储结构转换为方法区的运行时数据结构;
3、 在方法区中为这个类生成一个java.lang.Class对象,作为方法区这个类的访问入口。
数组的加载跟普通类型加载有所不同,因为数组本身不是通过类加载器加载产生的,数组类是虚拟机自动生成的
但是数组的类型是通过类加载器完成加载的,数组类的创建过程需要遵循以下规则:
1、 如果数组的类型是引用类型,则引用类型需要使用递归来进行加载,
并且数组需要被加载该数组类型的类加载器的命名空间上进行标识;
2、 如果数组的类型不是引用类型,是基本数据类型,Java虚拟机将会把数组标记为与引导类加载器关联;
3、 数组的可见性与数组类型的可见性保持一致,如果数组类型是基本类型,则默认可见性为public。
验证
验证是为了保证class文件中的内容是符合虚拟机规范的二进制字节流,防止通过执行一些不安全的二进制字节流而导致虚拟机奔溃。
验证是类加载的第二个阶段,这个阶段也是持续时间最长(从阶段连续性来说)
这个阶段从加载开始进行,一直进行到解析阶段结束。
1. 文件格式验证
这一阶段主要验证字节流是否符合class文件格式规范,并且能被虚拟机处理,保证输入的字节流能被正确的解析并且存储在方法区
2. 元数据验证
第二阶段主要是进行语法分析,以保证class文件符合Java语法规范
3. 字节码验证
这个阶段是语义分析,是验证过程中最复杂的一个阶段
主要目的是通过数据流和控制流,确定语义是合法并且符合逻辑的
这个阶段主要是针对方法体进行分析,以保证方法在运行过程中不会出现危害虚拟机的操作
准备
准备阶段是为类变量分配内存并且设置初始化值的阶段
这些变量所使用的内存都在方法区分配。
这个阶段进行初始化的数据只有静态字段,并且是赋值初始化值(final修饰的字段除外),不是代码中定义的值。
public static int value = 123; //value在方法区分配内存,并且设置初始值0
public static final int value = 123; //在准备阶段将会被赋值123,并且不会引起类的初始化过程
解析
解析阶段是将常量池内的符号引用替换为直接引用的过程
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。
比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。
直接引用,可以理解为一个内存地址,或者一个偏移量。
比如类方法,类变量的直接引用是指向方法区的指针;
而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。
运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。
初始化
只对static修饰的变量或语句进行初始化
初始化阶段是类加载过程的最后一步,这个阶段才开始真正的执行用户定义的Java程序。
在准备阶段,变量已经赋过一次系统要求的初始值
而在初始化阶段,则需要为类变量(非final修饰的类变量)和其他变量赋值,其实就是执行类的<clinit>()方法
虚拟机会保证父类的<clinit>()方法先于子类的<clinit>()执行,java.lang.Object的<clinit>()方法是最先执行的
<clinit>()方法对于类和接口来说不是必须的,
如果类或接口中没有定义类变量,也没有静态语句块,那么编译器将不为这个类或者接口生成<clinit>()方法