在 Java 语言层面, 可以通过 Class 类来描述普通的 Java 类, 当 JVM 创建对象的同时, 会生成对应的 Class 对象, 用来描述此对象的大致模型, 这也是反射的基础. 那么在 JVM 的内部是如何描述一个普通的对象? 我们先从一个简单的示例着手, 这有一个 Child 类:
- public class Child extends Person implements Action {
- // 小孩上几年级
- public int grade;
- // Action 接口就一个动作: walk
- @Override
- public void walk() {
- }
- }
通过 Child child = new Child()来创建对象时, JVM 在堆中开辟空间存放对象实例数据的同时, 会在栈中创建该对象的引用, 用于存放 child 对象在堆内存中的首地址, 大致的示意图如下所示.
新创建对象内存占用示意图
现在请大家思考: 站在 JVM 的角度, 要完整地描述运行时的 child 对象, 需要记录哪些信息?
脑袋里可能马上就会跳出来这些信息:
对象所属类的相关信息: 类 (包含父类) 的名称, 实现了哪些接口, 是否有注解, 方法列表, 属性列表, 常量等
实例数据: 对象存储的有效信息, 比如对象各个属性存储的具体内容
除了这些呢? 其实还有一些运行时的数据, 比如: 锁信息, 线程 ID,GC 标记等.
JVM 是如何记录这些信息的呢? HotSpot VM 采用 OOP-Klass 的模型来描述 Java 对象实例.
Klass
Klass 系对象 (instanceKlass,arrayKlass 等) 用于描述对象的元数据, 其中 instanceKlass 可以认为是 java.lang.Class 的 VM 级别的表示, 但它们并不等价, instanceKlass 主要作用于整个程序运行过程中, 而 Class 类只用于 Java 的反射 API, 接下来将以 instanceKlass 为例来介绍 Klass, 其它对象与之类似.
Klass 类定义了所有 Klass 类型共有的数据结构和行为, 比如类型名称, 与其它类之间的关系, 访问标识符等等, 具体可参看:
- // 代码来自于 hotspot/src/share/vm/oops/klass.hpp
- class Klass : public Metadata {
- // 反映对象整体布局的描述符, 在 32 位系统中占用 4 个字节
- // 如果值为正数, 表示对象大小, 如果值为负数, 表示数组
- jint _layout_helper;
- // 类名称, 比如:"java/lang/String" 表示 String 对象
- // 而[Ljava/lang/String 描述 String 类型的一维数组
- Symbol* _name;
- // 对应的 Java 语言层面的 Class 对象实例
- oop _java_mirror;
- // 父类, 指针指向其父类的首地址
- Klass* _super;
- // 第一个子类
- Klass* _subklass;
- // subklass 指向第一个子类, 如果有多个子类
- // 那么可以通过_subklass->next_sibling()找到下一个子类
- Klass* _next_sibling;
- // Java 中类名和类加载器唯一标识了一个类
- // 由同一个类加载器加载的类通过 _next_link 连接起来
- Klass* _next_link;
- ClassLoaderData* _class_loader_data;
- // 访问标识符, Java 层面通过 Class.getModifiers()获取
- // 比如: 1 表示 public
- jint _modifier_flags;
- // 类或者接口的访问修饰符
- AccessFlags _access_flags;
- // ......
HotSpot 中为每一个已加载的 Java 类创建一个 instanceKlass 对象, 用于在 JVM 层面表示 Java 类, 它包含了虚拟机内部运行一个类所需要的全部信息, 这些成员变量在类的解析阶段 (主要是将常量池中的符号引用转换为直接引用, 即运行时实际内存地址) 完成赋值:
- // 代码来自于 hotspot/src/share/vm/oops/instanceKlass.hpp
- class InstanceKlass: public Klass {
- // 注解
- Annotations* _annotations;
- // 常量
- ConstantPool* _constants;
- // 方法列表
- Array<Method*>* _methods;
- // 方法顺序
- Array<int>* _method_ordering;
- Array<Method*>* _default_methods;
- // 实现的接口
- Array<Klass*>* _local_interfaces;
- // 继承来的接口
- Array<Klass*>* _transitive_interfaces;
- // 静态变量的数量
- u2 _static_oop_field_count;
- // 成员变量的数量
- u2 _java_fields_count;
- // ......
接下来以文章开头的 Child 对象为例, 观察程序运行过程中 Child 类型的 Klass 信息, 以加深大家的理解.
Child 类继承 Person 类并实现的 Action 的所有接口, 通过 HSDB 来探测 Klass 对象信息, 如下图所示, 首先通过 HSDB 的 Class Browser 工具列出所有的类, 找到我们定义的类, 比如 Person 类实例的内存地址为: 0x00000007c0060210, 然后使用这个内存地址到 Inspector 中搜索, 即可得到 Person 类在 HotSpot 内部 instanceKlass 类型的全貌, 如下图所示.
HSDB
从图中可以得到, Person 类的其中一个子类的 Klass 对象内存地址_subklass:Klass @ 0x00000007c0060408, 通过这个地址可以在 Code Browser 中很方便的查找到其对应的类是: Child. 除此之外, 还可以找到一些非常熟悉的属性:
_super: Klass @ 0x00000007c0000f28 Person 类的父类是 Object 类
_mofifier_flags: 1 表示 public
_name: Symbol @ 0x00007ff686715e00 类名称, String 对象的内存地址
_layout_helper: 24 值为正数, 表示对象的大小
_methods: Array<Method> @ 0x00000001171558f0* 方法列表
......
属性太多, 这里无法一一列举, 鼓励大家自己尝试, 随便也学习一下怎么使用 HSDB 来分析 JVM 内部的数据结构和状态, 但不鼓励钻牛角尖似的非要弄清楚每个属性的含义和作用, 至少在当前是不需要的.
再回到 instanceKlass.hpp 里面, 对象的注解, 常量以及方法, 在 VM 中分别使用 Annotations,ConstantPool,Method 来描述, 它们同 Klass 一样, 均继承自 Metadata 或者 MetaspaceObj 类.
在 HotSpot JVM 中, 永久代中用于存放类和方法的元数据以及常量池, 比如 Class 和 Method. 每当一个类初次被加载的时候, 它的元数据都会放到永久代中.
需要注意的是, 在 JDK1.8 中已经引入 Metaspace (元空间)来替换原来的永久代 PermGen, 因此, JDK1.8 里的对象模型实现与 1.7 有很大的不同. 通过上文的分析, 希望能够加深你对这句话的理解.
OOP
OOP 用来描述对象的实例信息, 在 Java 程序运行过程中, 每创建一个 Java 对象, 在 JVM 内部也会相应的创建一个 OOP 对象来表示 Java 对象. oop 的定义 oopDesc 如下 (oop 相关类的定义均会在名称后面添加后缀 Desc, 比如: instanceOopDesc):
- class oopDesc {
- private:
- // Mark Word
- volatile markOop _mark;
- // 元数据
- // 使用了 union 来声明 metadata 是为了在 64 位机器上对对象指针进行压缩
- union _metadata {
- Klass* _klass;
- narrowKlass _compressed_klass;
- } _metadata;
整个 oopDesc 定义了如下信息:
_mark (Mark Word):, 哈希码, GC 分代年龄, 锁状态标志, 线程持有的锁, 偏向线程 ID, 偏向时间戳
_metadata (元数据指针): 指向描述类型的 Klass 对象指针, Klass 对象包含了实例对象所属类型的元数据
在_metadata 中包含一个压缩指针, 在 32 位系统中, 对象的指针长度是 32 位, 而在 64 位系统中, 指针长度为 64 位. 在 64 位系统刚刚兴起的年代, 对于那些从 32 位系统迁移到 64 位系统的引用来说, 平白无故的多了差不多 50% 的内存占用 (主要是指指针占用的内存, 非整个应用的内存占用), 基于节约内存的考量, 可以在 64 位系统上对指针占用的内存进行压缩, 更多的内容可以参考:-XX:+UseCompressedOops 参数.
Mark Word 存储对象自身的运行时数据, 其被设计成一个非固定的数据结构, 可在极小的空间内存储尽量多的信息, 它会根据自己的状态复用自己的存储空间. 比如, 在 32 位系统中, 如果对象处于无锁状态, 那么 Mark Word 的 32bit 空间中的 25 个 bit 用于存储对象的 hash 值, 4bit 用于存储对象的分代年龄, 2bit 用于存储锁标志位, 1bit 用于存储锁的类型; 而当对象处于有锁状态下, 根据锁的类型不同, 存储的数据又不同, 具体的示意图如下:
Mark Word
来源: http://www.jianshu.com/p/306b398616b7