Java 对象的内存布局
Java Object Layout
Java对象保存在堆内存中。在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。
本文分析均基于HotSpot虚拟机
Java的对象模型
oop-klass model
HotSpot是基于c++实现,而c++是一门面向对象的语言,本身是具备面向对象基本特征的,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model
。OOP(Ordinary Object Pointer
)指的是普通对象指针,而Klass
用来描述对象实例的具体类型。
为什么HotSpot要设计一套oop-klass model
呢?答案是:HotSopt JVM的设计者不想让每个对象中都含有一个vtable
(虚函数表)
oop
//定义了oops共同基类
typedef class oopDesc* oop;
//表示一个Java类型实例
typedef class instanceOopDesc* instanceOop;
//表示一个Java方法
typedef class methodOopDesc* methodOop;
//表示一个Java方法中的不变信息
typedef class constMethodOopDesc* constMethodOop;
//记录性能信息的数据结构
typedef class methodDataOopDesc* methodDataOop;
//定义了数组OOPS的抽象基类
typedef class arrayOopDesc* arrayOop;
//表示持有一个OOPS数组
typedef class objArrayOopDesc* objArrayOop;
//表示容纳基本类型的数组
typedef class typeArrayOopDesc* typeArrayOop;
//表示在Class文件中描述的常量池
typedef class constantPoolOopDesc* constantPoolOop;
//常量池告诉缓存
typedef class constantPoolCacheOopDesc* constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class klassOopDesc* klassOop;
//表示对象头
typedef class markOopDesc* markOop;
**instanceOopDesc表示类实例,arrayOopDesc表示数组。**也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
private:
// field addresses in oop
void* field_base(int offset) const;
jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
address* address_field_addr(int offset) const;
}
class instanceOopDesc : public oopDesc {
}
class arrayOopDesc : public oopDesc {
}
对象头包含了两部分内容:_mark
和_metadata
,而实例数据则保存在oopDesc中定义的各种field中。
_mark
文章开头我们就说过,之所以我们要写这篇文章,是因为对象头中有和锁相关的运行时数据,这些运行时数据是synchronized
以及其他类型的锁实现的重要基础,而关于锁标记、GC分代等信息均保存在_mark
中。因为本文主要介绍的oop-klass
模型,在这里暂时不对对象头做展开,下一篇文章介绍。
_metadata
前面介绍到的_metadata
是一个共用体,其中_klass
是普通指针,_compressed_klass
是压缩类指针。
klass
|
|
Klass向JVM提供两个功能:
- 实现语言层面的Java类(在Klass基类中已经实现)
- 实现Java对象的分发功能(由Klass的子类提供虚函数实现)
内存存储
关于一个Java对象,他的存储是怎样的,一般很多人会回答:对象存储在堆上。稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。今天,再给你一个更加显得牛逼的回答:
对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。
其实如果细追究的话,上面这句话有点故意卖弄的意思。因为我们都知道。方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。
talk is cheap ,show me the code :
class Model
{
public static int a = 1;
public int b;
public Model(int b) {
this.b = b;
}
}
public static void main(String[] args) {
int c = 10;
Model modelA = new Model(2);
Model modelB = new Model(3);
}
存储结构如下:
从上图中可以看到,在方法区的instantKlass中有一个int a=1
的数据存储。在堆内存中的两个对象的oop中,分别维护着int b=3
,int b=2
的实例数据。和oopDesc一样,instantKlass也维护着一些fields
,用来保存类中定义的类数据,比如int a=1
。
对象头
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}
上面代码中的_mark
和_metadata
其实就是对象头的定义。关于_metadata
之前就介绍过,这里不再赘述。由于这个专题主要想介绍和JAVA并发相关的知识,所以本文展开介绍一下_mark
,即mark word。
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。
同样,在HotSpot的源码中我们可以找到关于对象头对象的定义,会一一印证上图的描述。对应与markOop.hpp类。
enum { age_bits = 4,
lock_bits = 2,
biased_lock_bits = 1,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
从上面的枚举定义中可以看出,对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。
从上图中可以看出,对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在32位的虚拟机中有两个Bits是用来存储锁的标记为的,但是我们都知道,两个bits最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖1Bit的空间,使用0和1来区分。
在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,表示非偏向锁。
markOop.hpp类中有关于对象状态的定义:
enum { locked_value = 0,
unlocked_value = 1,
monitor_value = 2,
marked_value = 3,
biased_lock_pattern = 5
};
简单翻译一下:
locked_value(00) = 0
unlocked_value(01) = 1
monitor_value(10) = 2
marked_value(11) = 3
biased_lock_pattern(101) = 5
Java Object Layout
|
|
|
|
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x00c00a08
12 1 boolean A.a true
13 3 (alignment/padding gap)
16 4 boolean[] A.b [true, true, true, true, true, true, true, true]
20 4 (object alignment gap)
Instance size: 24 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
总结
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass
。