JVM类加载过程
所属分类 jvm
浏览量 839
Java虚拟机把描述类的数据从Class文件加载到内存,
并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,
这个过程被称作虚拟机的类加载机制
类生命周期
加载 loading
链接 linking (验证 verification 准备 preparation 解析 resolution)
初始化 initialization
使用 using
卸载 unloading
java编译时不像其他语言需要连接,
类型的加载、连接和初始化过程都是在程序运行期间完成的。
编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,
用户可以通过Java预置的或自定义类加载器,
让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。
运行时加载广泛应用于Java程序之中。
《Java虚拟机规范》 严格规定有且只有六种情况必须立即对类进行“初始化”
(而加载、验证、准备自然需要在此之前开始)
1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,
如果类型没有进行过初始化,则需要先触发其初始化阶段。
能够生成这四条指令的典型Java代码场景有:
使用new关键字实例化对象的时候
读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
调用一个类型的静态方法的时候
2)使用java.lang.reflect包的方法对类型进行反射调用的时候,
如果类型没有进行过初始化,则需要先触发其初始化。
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 7新加入的动态语言支持时,
如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,
并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,
如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
接口与类真正有所区别的是前面讲述的六种“有且仅有”需要触发初始化场景中的第三种
当一个类在初始化时,要求其父类全部都已经初始化过了,
但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,
只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化
类加载过程
加载
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
获取类的二进制字节流的形式
从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
从网络中获取,这种场景最典型的应用就是Web Applet
运行时计算生成,这种场景使用得最多的就是动态代理技术,
在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。
验证
验证阶段大致上会完成下面四个阶段的检验动作
文件格式验证、元数据验证、字节码验证和符号引用验证。
“停机问题”(Halting Problem),即不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。
准备
正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
首先是这时候进行内存分配的仅包括类变量,
而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段过后的初始值为0而不是123,
因为这时尚未开始执行任何Java方法,
而把value赋值为123的putstatic指令是程序被编译后,
存放于类构造器< clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用与虚拟机实现的内存布局无关,
引用的目标并不一定是已经加载到虚拟机内存当中的内容。
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
直接引用是和虚拟机实现的内存布局直接相关
1.类或接口的解析
需要判断该类是否是数组类型
如果我们说一个D拥有C的访问权限,那就意味着以下3条规则中至少有其中一条成立:
被访问类C是public的,并且与访问类D处于同一个模块。
被访问类C是public的,不与访问类D处于同一个模块,但是被访问类C的模块允许被访问类D的模块进行访问。
被访问类C不是public的,但是它与访问类D处于同一个包中。
2.字段解析
首先将会对字段表内class_index 项中索引的CONSTANT_Class_info符号引用进行解析,
也就是字段所属的类或接口的符号引用;
3.方法解析
先解析出方法表的class_index项中索引的方法所属的类或接口的符号引用,
如果解析成功,那么我们依然用C表示这个类。
1)由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,
如果在类的方法表中发现class_index中索引的C是个接口的话,
那就直接抛出java.lang.IncompatibleClassChangeError异常。
2)如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,
如果有则返回这个方法的直接引用,查找结束。
3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,
如果有则返回这个方法的直接引用,查找结束。
4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,
如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常。
5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,
如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。
4.接口方法解析
方法解析类似
JDK 9中增加了接口的静态私有方法,也有了模块化的访问约束,
所以从JDK 9起,接口方法的访问也完全有可能因访问权限控制而出现java.lang.IllegalAccessError异常。
初始化
初始化阶段就是执行类构造器< clinit>()方法的过程。
< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,
编译器收集的顺序是由语句在源文件中出现的顺序决定的,
静态语句块中只能访问到定义在静态语句块之前的变量,
定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
< clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器< init>()方法)不同,
它不需要显式地调用父类构造器,
Java虚拟机会保证在子类的< clinit>()方法执行前,父类的< clinit>()方法已经执行完毕
因此在Java虚拟机中第一个被执行的< clinit>()方法的类型肯定是java.lang.Object。
由于父类的< clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
上一篇
下一篇
ROE简介
java class类文件结构
JVM字节码指令
JVM类加载器
修佛必读《心经》《金刚经》《楞严经》
JVM模块化系统