《深入理解java虚拟机——JVM高级特性与最佳实践》阅读笔记 虚拟机类加载机制

java 的类型加载、连接、初始化都在运行期间完成,动态扩展的语言特性就是以此为基础。一个广泛应用的例子是,用户通过 java 预定义及自定义的类加载器,让本地应用程序在运行时从网络加载二进制流作为程序的一部分。

类的生命周期分为7个阶段:

阶段名称
1加载
2验证
3准备
4解析
5初始化
6使用
7卸载

其中,验证,准备以及解析阶段统称为“连接”。
类的“解析”与“使用”阶段的顺序并不确定,动态绑定时解析可以在初始化阶段之后再开始

加载阶段由虚拟机按具体实现自由控制,而对于初始化阶段只有以下5种情况会触发

序号规则
1new、getstatic、putstatic、invokestatic 4条字节码指令出现时,需进行初始化;对应场景:new实例化对象,读取或设置静态字段(被 final 修饰或已经在编译期将结果放入常量池的字段除外),调用类的静态方法时
2使用 java.lang.reflect 的方法对类进行反射调用时
3初始化类时,若其父类未初始化,则需先初始化父类
4jvm 启动时,先初始化含有 main() 方法的主类
5使用 jdk1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,且句柄对应的类未初始化时需初始化。

以上场景中的行为称为对类的“主动引用”,其余的所有方式均不会触发类初始化,称为“被动引用”。
不会触发初始化的场景:

序号详细场景
1通过数组定义来引用类,不会触发其初始化操作
2常量在编译期会存入调用常量的类的常量池中,并没有引用定义常量的类,因此不会触发其初始化
3子类引用父类的静态字段只会让父类初始化,子类不会初始化(只有直接定义静态字段的类才会被初始化

接口也有初始化过程,编译期间会生成 ‘<‘clinit’>’() 类构造器来初始化定义的变量。与类的初始化不同在于,初始化时不要求父接口初始化,只有在使用到父接口时才需要初始化。

类加载第一步,“加载”动作需要完成的操作:

序号具体操作
1通过类的全限定名获取定义此类的二进制字节流
2将字节流代表的静态存储结构转化为方法区的运行时数据结构
3在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区该类各种数据的访问入口

规范中没有具体限制,因此实现方式非常多样化,就第1条规则而言,有很多技术与之相关:

  1. 从 zip 读取,成为日后 jar,war,ear 的基础
  2. 从网络获取,典型应用为 Applet
  3. 运行时计算生成,如动态代理技术,在 java.lang.reflect.Proxy 中使用 ProxyGenerator.generateProxyClass 为特定接口生成 “*$Proxy” 的代理类的二进制字节流
  4. JSP文件生成 class
  5. 数据库中读取,某些中间服务器将程序安装到数据库以此完成代码在集群间分发

数组类不通过类加载器创建,而是由 JVM 直接创建。但是数组类的元素类型最终需要依靠类加载器创建。

序号规则详情
1如果数组的组件类型是引用类型,就用递归调用本节中的加载过程加载组件类型,数组类将在加载该组件类型的类加载器的类名称空间上被标识
2如果组件类型不是引用类,则 JVM 将把数组类标记为与引导类加载器关联
3数组类可见性与组件类型可见性一致。如果可见类型不是引用类型,则数组类可见性将默认为 public

加载阶段完成后,JVM 外部的二进制字节流就按照所需格式存储在方法区中,而存储格式由虚拟机自行定义实现。随后在内存中实例化一个 java.lang.Class 类的对象(HotSpot 虚拟机中,该对象存放于方法区),该对象将作为程序访问这些数据的外部接口。
加载与连接的部分内容虽然交叉进行,但这些夹在加载阶段中的动作,仍属于连接阶段的内容,开始时间保持着固定顺序。

验证是连接阶段的第一步,用于确保 Class 文件中的字节流包含的信息符合虚拟机要求,不会危害虚拟机自身。这个阶段由于十分重要,因此其工作量在类加载子系统中占据了相当大的比重。
总共有4个阶段的检验工作:

阶段名称
1文件格式验证
2元数据验证
3字节码验证
4符号引用验证

阶段1验证字节流是否符合 Class 文件格式规范并能被当前版本虚拟机处理。可能包括的验证点:

  1. 是否以 0xCAFEBABE 开头
  2. 版本号是否在处理范围内
  3. 检查常量 tag 标志,查看是否有不支持的常量类型
  4. 指向常量的索引中是否有指向不存在或不符合类型的常量
  5. CONSTANT_Utf8_info 类型的常量中是否有不符合 utf8 编码的常量
  6. Class 文件中是否有被删除或附加的其它信息

二进制字节流通过该阶段验证之后,才能进入方法区存储(之后的3个阶段都基于方法区的存储结构进行,不再操作字节流)。

第2阶段是对字节码的元数据进行语义分析,保证描述的信息符合 java 规范要求。可能包括的验证点:

  1. 是否有父类
  2. 父类是否继承了 final 类
  3. 该类是否实现父类或接口中需要实现的所有方法
  4. 类中的字段方法是否与父类矛盾

第3阶段通过对数据流和控制流进行分析,确定程序语法合格。该阶段对类的方法体进行校验分析,以确保方法在运行时不会出现损害虚拟机的安全事件,例如:

  1. 保证操作数栈的数据类型与指令代码序列能配合工作,不会出现加载时认定的类型与实际存储的类型不一致
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上
  3. 保证方法体中类型转换是有效的

jdk1.6 之后在 javac 编译器以及 jvm 中进行了优化,给方法体的 Code 属性的属性表增加了一项“StackMapTable”的属性。该属性描述了方法体中所有基本块(按流程拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验期间就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 中记录是否合法即可。

最后的符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,目的在于确保解析动作可以正常执行。该阶段在连接第三阶段–解析阶段发生。检验内容:

  1. 符号引用中通过字符串描述的全限定名能否找到对应的类
  2. 指定类中是否存在符合方法字段描述以及简单名称所描述的方法和字段。
  3. 符号引用中的类、字段、访问权限是否可被当前类访问
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值