HotSpot通过Method与ConstMethod来保存方法元信息。
1、Method
Method没有子类,定义在method.hpp文件中,其类继承关系如下图:
Method用于表示一个Java方法,因为一个应用有成千上万个方法,因此保证Method类在内存中短小非常有必要。为了本地GC方便,Method把所有的指针变量和方法大小放在Method内存布局的前面。
方法本身的不可变数据,如字节码用ConstMethod表示,可变数据如Profile统计的性能数据等用MethodData表示,都通过指针访问。
如果是本地方法,Method内存结构的最后是native_function和signature_handler属性,按照解释器的要求,这两个必须在固定的偏移处。
Method类中声明的属性如下:
源代码位置:/vm/oops/method.hpp class Method : public Metadata { friend class VMStructs; private: ConstMethod* _constMethod; // Method read-only data. // MethodData结构基础是ProfileData,记录函数运行状态下的数据 // MethodData里面分为3个部分,一个是函数类型等运行相关统计数据,一个是参数类型运行相关统计数据, // 还有一个是extra扩展区保存着deoptimization的相关信息 MethodData* _method_data; MethodCounters* _method_counters; AccessFlags _access_flags; // Access flags // vtable index of this method (see VtableIndexFlag) // note: can have vtables with >2**16 elements (because of inheritance) int _vtable_index; u2 _method_size; // size of this object u1 _intrinsic_id; // vmSymbols::intrinsic_id (0 == _none) // Flags,在定义时指定各个变量占用的位 u1 _jfr_towrite : 1, // Flags _caller_sensitive : 1, _force_inline : 1, _hidden : 1, _dont_inline : 1, : 3; // Entry point for calling both from and to the interpreter. address _i2i_entry; // All-args-on-stack calling convention /* _adapter 指向该Java方法的签名(signature)所对应的 i2c2i adapter stub。其实是一个 i2c stub 和一个 c2i stub 粘在一起这样的对象,可以看到用的时候都是从 _adapter 取 get_i2c_entry() 或 get_c2i_entry()。这些adapter stub用于在HotSpot VM里的解释模式与编译模式的代码之间适配其 calling convention。HotSpot VM里的解释模式calling convention用栈来传递参数,而编译模式的 calling convention更多采用寄存器来传递参数,两者不兼容,因而从解释模式的代码调用已经被编译的方法, 或者反之,都需要在调用时进行适配。 */ // Adapter blob (i2c/c2i) for this Method*. Set once when method is linked. AdapterHandlerEntry* _adapter; /* _from_compiled_entry 初始值指向c2i adapter stub。原因上面已经说了,因为一开始该方法尚未被JIT编译, 需要在解释模式执行,那么从已经JIT编译好的Java方法调用过来的话就需要进行calling convention的转换, 把参数挪到正确的位置上。当该方法被JIT编译并“安装”完之后,_from_compiled_entry 就会指向编译出来的机 器码的入口,具体说时指向verified entry point。如果要抛弃之前编译好的机器码,那么 _from_compiled_entry 会恢复为指向 c2i stub。 */ // Entry point for calling from compiled code, to compiled code if it exists // or else the interpreter. // Cache of: _code ? _code->entry_point() : _adapter->c2i_entry() volatile address _from_compiled_entry; // The entry point for calling both from and to compiled code is // "_code->entry_point()". Because of tiered compilation and de-opt, this // field can come and go. It can transition from NULL to not-null at any // time (whenever a compile completes). It can transition from not-null to // NULL only at safepoints (because of a de-opt). // nmethod全名native method,指向的是Java method编译的一个版本。 // 当一个方法被JIT编译后会生成一个nmethod,指向的是编译的代码 // _code的类型为nmethod nmethod* volatile _code; // Points to the corresponding piece of native code /* _from_interpreted_entry 初始的值与 _i2i_entry 一样。但后面当该Java方法被JIT编译并“安装”之后, _from_interpreted_entry 就会被设置为指向 i2c adapter stub。而如果因为某些原因需要抛弃掉之前已 经编译并安装好的机器码,则 _from_interpreted_entry 会被恢复为 _i2i_entry。 */ // Cache of _code ? _adapter->i2c_entry() : _i2i_entry volatile address _from_interpreted_entry; // 如果有_code,则通过i2c_entry转向编译方法,否则通过_i2i_entry转向解释方法 ... }
HotSpot虚拟机中的Method存储在元数据区(在JDK1.8之前是PermGen)。
Method中最后定义的几个属性非常重要。用以方法的解释执行和编译执行。一个方法可能有多个入口:
1、_i2i_entry :指向字节码解释执行的入口;
2、_code->entry_point() :指向JIT编译代码的入口。编译后的代码存储在CodeCache中,这是专门为动态生成的代码开辟的一块本地内存;
3、i2c和c2i适配器:用来在解释执行和编译执行之间进行转换,由于解释执行和编译执行的调用约定不同,所以专门做了适配器来适配。
可以这样总结一下,如果转换的目标是解析执行,那么:
- from_compiled_code_entry_point 的值为 c2i adapter
- from_interpreter_entry_point 的值为 interpreter entry point
如果转换的目标是编译执行:
- from_compiled_code_entry_point 的值为 nmethod entry point
- from_interpreter_entry_point 的值为 i2c adapter
Method类中定义的各属性的介绍如下表所示。
字段名 | 描述 |
_constMethod |
ConstMethod指针,该类定义在constMethod.hpp文件中,用于表示方法的不可变的部分,如方法ID, 方法的字节码大小,方法名在常量池中的索引等 |
_method_data |
MethodData指针,该类在methodData.hpp中定义,用于表示一个方法在执行期间收集的相关信息, 如方法的调用次数,在C1编译期间代码循环和阻塞的次数,Profile收集的方法性能相关的数据等 |
_method_counters |
MethodCounters指针,该类在methodCounters.hpp中定义,用于大量编译优化相关的计数,
主要用于基于调用频率的热点方法的跟踪统计 |
_access_flags | AccessFlags类,表示方法的访问控制标识 |
_vtable_index | 该方法在vtable表中的索引 |
_method_size | 这个Method对象的大小,以字宽为单位 |
_intrinsic_id |
固有方法(intrinsic method)在虚拟机中表示一些众所周知的方法,针对它们可以做特别处理,生成独特的代码例程。 虚拟机发现一个方法是固有方法就不会走逐行解释字节码这条路径而是跳到独特的代码例程上面, 所有的固有方法都定义在 |
_compiled_invocation_count | 编译后的方法叫nmethod,这个就是用来计数编译后的nmethod调用了多少次,如果该方法是解释执行就为0 |
_i2i_entry | 解释器的入口地址 |
_adapter | 此方法在解释器和编译器执行的适配器 |
_from_compiled_entry | 执行编译后的代码的入口地址 |
_code | nmethod类指针,表示该方法编译后的本地代码 |
_from_interpreted_entry | code ? _adapter->i2c_entry() : _i2i_entry的缓存 |
基中_access_flags的取值如下:
_vtable_index的取值如下:
2、ConstMethod类介绍
ConstMethod对象代表方法中不可变的部分,例如字节码。类的定义如下:
源代码位置:/hotspot/src/share/vm/oop class ConstMethod : public MetaspaceObj { ... private: ... // 其中_constants,_method_idnum这两个是连接Method的参数,因为Method有ConstMethod指针, // 但ConstMethod没有Method的指针,需要通过如下步骤来查找: // ConstantPool -> InstanceKlass -> Method数组->通过_method_idnum获取对应的Method指针 // 原文链接:https://blog.csdn.net/raintungli/article/details/83857136 ConstantPool* _constants; // Constant pool int _constMethod_size; u2 _flags; // Size of Java bytecodes allocated immediately after Method*. u2 _code_size; u2 _name_index; // Method name (index in constant pool) u2 _signature_index; // Method signature (index in constant pool) u2 _method_idnum; // unique identification number for the method within the class // initially corresponds to the index into the methods array. // but this may change with redefinition u2 _max_stack; // Maximum number of entries on the expression stack u2 _max_locals; // Number of local variables used by this method u2 _size_of_parameters;// size of the parameter block (receiver + arguments) in words ... }
该类用于表示方法的不可变部分,如方法的id(_method_idnum),方法的字节码大小,方法名在常量池中的索引等,其中_constMethod_size的大小以字宽为单位,_code_size的大小以字节为单位,其内存结构如下图,因为异常检查表平均长度小于2,本地变量表大多数情况下没有,所以这两个没有被压缩保存。访问这些内嵌表都很快,不是性能瓶颈。ConstMethod提供了获取内嵌部分,如字节码的起始地址,然后可以据此偏移地址获取的方法字节码。
相关文章的链接如下:
1、在Ubuntu 16.04上编译OpenJDK8的源代码
13、类加载器
14、类的双亲委派机制
15、核心类的预装载
16、Java主类的装载
17、触发类的装载
18、类文件介绍
19、文件流
20、解析Class文件
21、常量池解析(1)
22、常量池解析(2)
23、字段解析(1)
24、字段解析之伪共享(2)
25、字段解析(3)
作者持续维护的个人博客classloading.com。
关注公众号,有HotSpot源码剖析系列文章!
参考文章: