前一篇文章,介绍了字节码是如何被加载,本文介绍一下加载流程中的链接过程,先从内存存储结构说起。
这里以hotspot为例,在读取class文件之后,会创建一个jvm层面(C++)的描述java类的InstanceClass对象:
// InstanceKlass layout:
// [C++ vtbl pointer ] Klass
// [subtype cache ] Klass
// [instance size ] Klass
// [java mirror ] Klass
// [super ] Klass
// [access_flags ] Klass
// [name ] Klass
// [first subklass ] Klass
// [next sibling ] Klass
// [array klasses ]
// [methods ]
// [local interfaces ]
// [transitive interfaces ]
// [fields ]
// [constants ]
// [class loader ]
// [source file name ]
// [inner classes ]
// [static field size ]
// [nonstatic field size ]
// [static oop fields size ]
// [nonstatic oop maps size ]
// [has finalize method ]
// [deoptimization mark bit ]
// [initialization state ]
// [initializing thread ]
// [Java vtable length ]
// [oop map cache (stack maps) ]
// [EMBEDDED Java vtable ] size in words = vtable_len
// [EMBEDDED nonstatic oop-map blocks] size in words = nonstatic_oop_map_size
// The embedded nonstatic oop-map blocks are short pairs (offset, length)
// indicating where oops are located in instances of this klass.
// [EMBEDDED implementor of the interface] only exist for interface
// [EMBEDDED host klass ] only exist for an anonymous class (JSR 292 enabled)
可以看到这个C++类描述了一个class file的类结构(如java.lang.Integer
),根据它就可以创建java类对应的实例了。hotspot中用instanceOopDesc
描述一个java类实例对象,其结构包括对象标记,指向InstanceClass的指针,对象实例数据。
_java_mirror
: InstanceMirrorKlass类型(InstanceKlass的子类),是一个类结构在java语言层面的描述,即java.lang.Class
类型对象的引用。_constants
: 常量池(ConstantPool)指针,初始时常量池中还是符号引用,解析过后会缓存_local_interfaces
,_transitive_interfaces
为直接实现和继承的接口列表_methods
: 方法列表_fields
:字段列表索引,项为六元组[access, name index, sig index, initval index, low_offset, high_offset]
数组是相似的结构,这里不提了
另外,在InstanceKlass
定义了类的状态:
state | desc |
---|---|
allocated | 已分配,还未链接 |
loaded | 已加载并插入类体系,还未链接 |
linked | 已成功链接,但还未初始化 |
being_initialized | 正在初始化 |
fully_initialized | 已成功初始化(这时成功时的最终状态) |
initialization_error | 初始化失败 |
接下来,就是本文要介绍的jvm关于链接过程的规范。
前文提到了链接包含验证 -> 准备 -> 解析
三个阶段,jvm规范里并没有严格限制实现者如何实现,所以不同的jvm实现可以不同,实际上很多虚拟机在为类结构分配内存前已经完成了部分验证工作,解析则可能在初始化时才进行,而且一个类的一个阶段(比如验证是否继承了final修饰的类)会激活另一个类的加载过程,实际情况会很复杂。
验证
验证工作不是一次完成,而是分为多步的,主要验证的内容有字节码静态结构,语法和指令执行。
-
结构验证
- 前4个字节必须是魔数(0xcafebabe)
- 版本是否支持
- 所有可识别的属性都能正确的解析
- class文件末尾不能有多余字节
- 常量池是有效的(引用都能指向有效的常量位置且是正确的引用)
- 字段和方法的引用
name_index
,descriptor_index
都是有效的常量池常量(注意:此时并不验证引用类)
-
语法验证
语法验证包括类继承是否合法,方法重载,覆盖是否正确,非抽象类是否存在抽象方法等等,保证语法正确性。这个过程当然还涉及到常量池里引用的常量是否有效,访问范围合法性等。
-
字节码指令验证
-
指令静态结构验证:主要验证code属性中指令数组的指令是否能正确识别并被当前虚拟机支持,指令及对应操作数是否匹配,数组以指令开始,操作数如果指向常量池必须是有效引用,操作数如果表示局部变量表索引则不能超过
max_locals - 1
(long,double相关指令最大max_locals - 2)等静态约束 -
动态指令执行分析验证:静态验证只能验证指令流的语法正确性,但不能保证其执行的语义有效性,例如,iload指令去读取一个对象引用,jsr跳到方法外去了,return了一个不匹配的类型值等。所以还需要从执行分析。
执行分析验证有早期的类型推导(Type Inference)和jdk1.6之后的基于类型检查(Type Checking,通过StackMapTable)的验证,这块内容比较复杂,我也没研究,所以就不写了
-
Note: 一个类或接口的验证如果触发另一个类或接口M被加载,但不要求M执行验证与准备
准备
准备过程就是创建类或接口的静态field并为它分配零值,注意不是执行类构造器赋值(在初始化阶段执行)。对于基本类型零值就是0
,对于引用类型零值为null
。
可以这样理解:源码中静态字段赋值都是编译的时候转换成方法中的赋值指令,所以要等到指令执行后才赋值为真正的值,但虚拟机在给类字段分配内存时需要先给它个值,所以给了零值。
jvm规范没有要求准备阶段确切什么时候执行但一定要在初始化执行前。
解析
解析过程就是将运行时常量池中的符号引用转换为直接引用的过程
-
符号引用
符号引用就是之前讲字节码时说的以字面量形式表示的引用,如
Ljava.lang.Object
,这种引用方式只能表示引用的是什么,但不知道引用的对象在哪里(内存中的位置) -
直接引用
直接引用就是表示的被引用对象在内存中分配后的地址
遇到anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, ldc2_w, multianewarray, new, putfield, putstatic指令时需要进行解析,已解析过的常量会被标识以防止重复解析,但invokedynamic指令每次执行都会解析。
解析的对象有类或借口
,字段
,类方法
,接口方法
,方法句柄
,动态调用点
:
- 类和接口解析
- 字段解析
- 方法解析
- 接口方法解析
- 方法类型和方法句柄解析
- 动态调用点解析
具体解析过程,在不同的jdk版本中有所区别,没有精力写了,感兴趣可以去看看官方文档:D
往期内容:
下期预告:
jvm类初始化详解
欢迎关注!
