java类的生命周期如下图:

java虚拟机启动不了是为什么 java虚拟机加载过程_初始化

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,其生命周期会经历上图中7个阶段。

其中:验证,准备,解析这3个部分统称为连接。

除过解析外,加载、验证、准备、初始化、卸载这些属性是固定的。类的加载过程必须按这种顺序开始。解析在某些情况下可以在初始化之后进行,对应java语言的动态绑定特性。

接着详细说明java虚拟机中类加载的全过程: 即加载(此处的加载为类加载过程中的一个子阶段,不要混淆),验证,准备,解析和初始化。

1.加载

在加载阶段,java虚拟机完成以下3件事情:

1.1.通过类的全限定名来获取定义该类的二进制字节流。

该字节流java虚拟机规范中并没有规定一定要从字节码文件获取,因此还可以从网络中获取,从压缩包中获取。。等。

1.2.将1中获取的字节流中代表的静态存储结构转换为方法区的运行时数据结构

1.3.在内存中生成一个代表该类的java.lang.Class对象,作为方法区中该类的各种数据的内存访问入口。

2.连接(linking)

连接又分为 验证,准备,解析三个过程。

这几个阶段并不是严格的按先后顺序,而是穿插着进行,包括和前面的加载阶段。

2.1 验证

验证阶段主要是检查字节码文件是否符合规范,例如,有没有cafebabe,有没有相关的访问权限等。

2.2 准备

准备阶段是初始化类的静态变量,赋初始零值。或者给final常量赋值的过程。

2.3 解析

解析阶段主要是将符号引用转换为直接引用。指的是刚刚完成字节码文件加载到内存中,还没有开始执行代码时就开始解析。(注:invokedynamic除外,该指令对应的引用叫做 动态调用点限定符,必须等到实际运行到这个指令时,才能进行解析)

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这7类符合引用进行。

其中,后四种都和动态语言支持相关。

符号引用:符号引用是一组符号,用来描述所引用的目标。符号引用于虚拟机的内存布局无关。符号引用所引用的目标不一定是已加载到虚拟机内存中的内容。

直接引用:直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。与虚拟机内存布局是相关的。直接引用的内容一定是已经在虚拟机内存中的内容。

同一个符合引用在不同虚拟机实例上解析出来的直接引用一般是不会相同的。

2.3.1 类或接口的解析

先假设:

待解析的接口或类,统称为C

如果要把一个符号引用N解析为C.当前代码所属的类为D

  • C不是数组类型:

虚拟机会把代表N的全限定名称传给D的类加载器去加载这个类C。期间可能触发其他相关类的加载,如父类或接口

  • C是数组类型

先按照C不是数组的加载C的方法,加载数组的类型(如果数组类型是类),接着虚拟机生成代表该数组维度和元素的数组对象。当然,解析完成之前要进行权限验证。

2.3.2 字段解析

字段解析会先解析该字段所属的类或接口C,如果C本身就包含了简单名称和字符描述符都与目标相匹配的字段,就返回这个字段的直接引用,否则,按照继承关系从下往上递归搜索父类,返回这个字段的直接引用。否则,查找失败。若查找成功 ,需要进行权限验证。

2.3.3 方法解析

同字段解析,也是需要先解析出该方法所属的类或接口C。

Class文件中类和接口的方法符合引用的常量类型定义是分开的。

  • a.类方法解析:

如果在类的方法表中发现C是个接口,直接抛出异常。

如果C是个类,查找C或其父类中是否有与目标匹配的方法,有则返回这个方法的直接引用,否则,解析失败。若查找成功 ,需要进行权限验证。

  • b.接口方法解析

如果在接口方法表中发现C是个类而非接口,则抛出异常。

否则,在接口C或其父接口中递归查找,是否有与目标匹配的方法。 如果C的不同接口有多个匹配的方法,则会返回其中一个并结束搜索。 否则,解析失败。jdk9之前,所有接口默认是public,所以不存在访问权限问题。但9之后,引入了模块化,以及加入了接口的静态私有方法了,就要检查访问权限了。

3.初始化

初始化是类加载过程的最后一个阶段。直到初始化阶段,jvm才开始执行java程序代码。前面准备阶段了,会赋类变量的初值,那这个阶段就会赋真正程序中定义的值了。用更直观的表达就是:初始化阶段就是执行<clinit>的过程。<clinit>是编译器自动收集类 的所有类变量的赋值语句和静态代码块的语句合并产出的,顺序是语句在java代码中的顺序决定的。静态代码块中只能访问到其之前定义的变量。定义在他之后的,在前面的静态块中可以赋值,但不能访问。当然,java虚拟机会保证在调用子类的clinit执行前,父类的clinit已经执行完毕。因此,去jclasslib中可以看到,java虚拟机中第一个执行的clinit方法一定是java.lang.Object的。

执行接口的clinit不需要先执行父接口的clinit,除非父接口中定义的变量被使用到了。同理,接口的实现类在初始化时也一样不会执行接口的clinit方法。

当然,如果一个类中没有static变量及static代码块,那编译器就不会为这个类生成clinit方法。

java虚拟机必须保证一个类的clinit在多线程环境中正确的加锁同步。如果多个线程去初始化同一个类,那么只会有一个线程去执行,其他线程会阻塞。如果某个线程执行clinit太久,会造成进程阻塞。注意,即使执行clinit的线程退出了,也不会有其他线程再去执行clinit了,因为同一个类加载器下,一个类型只会被初始化一次。