java类加载之链接过程(附hotspot类对象描述)

1,407 阅读5分钟

前一篇文章,介绍了字节码是如何被加载,本文介绍一下加载流程中的链接过程,先从内存存储结构说起。

这里以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

往期内容:

详解字节码(class)文件

读取class文件

通过源码,实例详解java类加载机制

下期预告:

jvm类初始化详解

欢迎关注!