Java虚拟机 系列文章
Java虚拟机1 内存管理、GC,包括 Shenandoah ZGC
Java虚拟机2 G1垃圾回收详解, 参数, 日志
Java虚拟机3 Class文件及类加载 (本文)
Java虚拟机4 方法调用原理、动态类型支持
Java虚拟机5 编译与优化
Java虚拟机6 内存模型、线程、锁
总结 Java 不支持的语法特性
Java 协程:Loom Project 实战
其他JVM语言
Class 文件
Class文件是一组以8个字节为单位的二进制流,类似c语言的结构体,如下所示:
有两种数据类型:无符号数和表。如u4表示4字节无符号数;表由多个无符号数或其他表组成。
字段含义
magic:魔数,识别是class文件
minor version 、 major version:Java次版本号、主版本号
*高版本的JDK能向下兼容以前版本的Class文件
constant pool:常量池,包括字面量(Literal)和符号引用(Symbolic References)
access_flags:访问标志
this class、super class、interface:自己、父类、接口列表,记录全限定名
fields:属性列表
methods:方法列表
attributes:其他信息,如字节码指令、泛型信息、注解、Module信息等
类的生命周期
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)
各阶段可能交替执行,如解析阶段需要执行部分验证逻辑
类加载时机
有且只有六种情况需要对类型进行初始化:
- new、getstatic、putstatic、invokestatic 这四条字节码指令,即:
- new关键字
- 读取或设置非final静态字段
- 调用静态方法 - 使用 java.lang.reflect 包对类型进行反射调用
- 当初始化类时,如果父类还没有初始化,则先初始化父类
- 虚拟机启动,初始化主类(包含 main() 方法的类)
- 使用 JDK 7 的动态语言支持, java.lang.invoke.MethodHandle 实例解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial
- 若接口定义了 JDK 8 的默认方法,如果这个接口的实现类发生了初始化,要先初始化该接口
这六类行为称为对一个类型进行主动引用。
其他引用不会触发初始化,称为被动引用,如:
- 通过子类引用父类的非final静态字段,只会触发父类的初始化,不会触发子类的初始化
- 定义数组不会触发数组元素类的初始化
- 引用类的常量(static final)字段,不会触发该类的初始化,因为常量在编译阶段会存入调用类的常量池
接口初始化时,不要求父接口已经初始化。
可以打开 -XX:+TraceClassLoading 追踪加载时机。
类加载过程
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证Class文件符合《Java虚拟机规范》。
分为:
- 文件格式验证
验证 class 文件字节流,并存到方法区中 - 元数据验证
语义分析,验证类的定义是否正确(如不能覆盖父类的 final 方法) - 字节码验证
校验类的方法体,通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的 - 符号引用验证
验证该类是否缺少或者被禁止访问它依赖的外部类、方法、字段等资源,发生于解析阶段将符号引用转换为直接饮用时
可以使用 -Xverify:none 关闭大部分验证
准备
为 static 变量分配内存,并设置初始值,非 final 字段设为零值,final 字段设为程序定义的值
解析
将常量池内的符号引用替换为直接引用
符号引用(Symbolic References):如 class 文件中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等
直接引用(Direct References):直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
初始化
执行类构造器:执行 static 变量的赋值和 static 语句块。
- 执行顺序是源文件中定义顺序
- 变量前边的 static 语句块,可以对其赋值,但不能读取
- 先执行父类构造器,后执行子类构造器,意味着父类的 static 语句块在子类的 static 变量赋值前执行
- 虚拟机保证构造器会加锁同步执行
类加载器(Class Loader)
实现“通过一个类的全限定名来获取描述该类的二进制字节流”。
两个不同的类加载器加载同一个 class 文件,产生的两个 Class 对象可能是不相等的。这里所指的“相等”,包括 Class.equals()、isAssignableFrom()、isInstance()、instanceof 的结果。
双亲委派机制
-
启动类加载器(Bootstrap Class Loader)
c++ 实现,负责加载存放在 <JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 指定的特定类库 -
扩展类加载器(Extension Class Loader)
位于 sun.misc.Launcher$ExtClassLoader,负责加载 <JAVA_HOME>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库 -
应用程序类加载器(Application Class Loader)
位于 sun.misc.Launcher$AppClassLoader,负责加载用户类路径(ClassPath)上所有的类库
双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。父子关系通常使用组合实现。
一个类加载器收到类加载请求时,先委派给父类加载器去完成,只有父加载器不能加载(搜索范围中找不到)时,子加载器才自己去加载。
实现位于 java.lang.ClassLoader.loadClass()
双亲委派并非强制约束,比如 OSGi 的热部署就打破了这一模型。
JDK9 后的修改
JDK9 引入了模块系统,扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,并调整了派生关系。
当平台及应用程序类加载器收到类加载请求,在委派给父加载器前,先判断该类是否归属于某个模块,并优先委派给负责那个模块的类加载器。