JVM拾遗(4): Java对象的创建及内存布局

上一节JVM拾遗(3): 类装载机制讲了JVM如何将类装载到虚拟机以供后续使用
那么JVM是如何创建类的实例呢?该对象是如何分配内存的?

1. 实例化

1.1 创建对象的方式

Java对象的创建, 有多种方式,最简单就是new XXClass, 还可以通过反射xx.clone(),反序列化以及黑科技Unsafe.allocateInstance等方法.

new和反射创建对象实例的时候,会初始化实例字段.

如果类没有构造器,会默认添加构造器,并且编译成<init>方法.
默认生成的构造器里,如果父类有无参构造器, 会隐式递归调用父类的构造器.

如下类:

1
2
3
4
5
public class TestClass {
public void test() {
TestClass t = new TestClass();
}
}

生成的字节码如下(用javap -v TestClass可以得到):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class TestClass
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return

那两行invokespecial指令是用来调用类构造器或者类的私有方法的.

可以看到,TestClass类添加了默认的构造器,生成了<init>方法,同时调用了Object类的<init>方法,也就是java.lang.Object类的构造方法.
也可以显式调用父类的构造器, 使用super(a,b,c)这样的形式即可

1.2 虚拟机如何处理new指令

new指令会实例化一个对象, JVM如何处理new指令生成具体的对象并不是规范的一部分,所以这里指的JVM实现指的是Hotspot的实现

下面是new指令实例化对象的过程:

  1. JVM会判断对应的类是否被加载,链接,初始化, 判断的依据是根据符号引用找方法区是否有类的数据.如果没有需要进行类的加载过程
  2. 为对象分配内存, 有2种方式
    • 指针提升(bump pointer, 注意没翻译错), 如果堆内存是规整的, 那么只需要把指针移动一个对象大小的距离就完成了分配
    • 空闲列表(free list), 如果堆内存是不连续的,碎片很多, JVM会维护一个可用内存队列, 从中查出可用内存分配给对象.
      具体使用哪种方式依据堆内存的情况而定,而堆内存的情况很大受到gc的影响,后面会细谈
  3. 初始化内存, JVM会将分配到的内存初始化为0值.
  4. 设置对象的元数据信息到对象头, 例如对象的hashcode,gc分带年龄,偏向锁状态等信息

一般来讲,new指令后面都会跟着invokespecial来执行<init>方法,也就是执行类构造器里的逻辑.

对象创建的并发安全问题

JVM处理对象创建的并发安全一般有2种方法:

  1. CAS操作加上失败重试
  2. 内存分配的动作按照线程划分在不同的内存空间区域进行,这些预先分配的内存仅属于该线程私有,也称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB), 可以通过-XX:UseTLAB来设置

2. 对象的内存布局

对象的内存布局也不是JVM规范的一部分,属于实现的细节,Hotspot将对象分成3部分,分别是:

  1. 对象头(Object Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

2.1 Object Header

对象头由标记字段(Mark Word)类型指针(Klass Pointer)组成

Mark Word用于存储该对象的运行时数据, 包括:

  • 对象hashcode, gc分代年龄
  • 锁记录指针, 重量级锁指针
  • 偏向线程id, 偏向时间戳

在64位的虚拟机上标记字段一般是8个字节,类型指针也是8个字节,总共就是16个字节.

可以使用-XX:UseCompressedOops来开启压缩指针, 以减少对象的内存使用量, 默认是开启的.

类型指针指向的是对象的元数据信息, 也就是对象所属类的信息.

HotSpot实现

Hotspot一般用OOP-Klass二分模型来实现, OOP(Ordinary Object Pointer)是普通对象指针,Klass用来描述具体的类型.

以下是openjdk的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// hotspot/src/share/vm/oops/oop.hpp
class oopDesc {
friend class VMStructs;
private:
// mark word真身
volatile markOop _mark;
// 元数据指针
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;

...

public:
markOop mark() const { return _mark; }
Klass* klass() const;
narrowKlass* compressed_klass_addr();

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;
Metadata** metadata_field_addr(int offset) const;

...省略非关键代码...

其中OOP的实现就是instanceOopDescarrayOopDesc,分别是普通对象实现和数组对象实现, 均继承自上面的oopDesc,
数组对象比普通对象多一个长度字段.
oopDesc存放有_mark_metadata, _mark就是标记字段(mark word), 而_metadata里就有一个指针指向Klass.

限于篇幅,markOop.hpp的代码就不贴了,大致就是上面提到的信息。

类型指针(上面_meta__klass指针)指向的Klass信息, 即类在JVM的真身, 看看就好.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// hotspot/src/share/vm/oops/instanceKlass.hpp
class InstanceKlass: public Klass {
...

// See "The Java Virtual Machine Specification" section 2.16.2-5 for a detailed description
// of the class loading & initialization procedure, and the use of the states.
// 类的加载状态, JVM规范里的各个阶段可能会穿插进行
enum ClassState {
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
...
}

// hotspot/src/share/vm/oops/klass.hpp
class Klass : public Metadata {
friend class VMStructs;
protected:
jint _layout_helper;
juint _super_check_offset;
Symbol* _name;

// Cache of last observed secondary supertype
Klass* _secondary_super_cache;
// Array of all secondary supertypes
Array<Klass*>* _secondary_supers;
// Ordered list of all primary supertypes
Klass* _primary_supers[_primary_super_limit];
// java/lang/Class instance mirroring this class
oop _java_mirror;
// Superclass
Klass* _super;
// First subclass (NULL if none); _subklass->next_sibling() is next one
Klass* _subklass;
// Sibling link (or NULL); links all subklasses of a klass
Klass* _next_sibling;

// All klasses loaded by a class loader are chained through these links
Klass* _next_link;

// The VM's representation of the ClassLoader used to load this class.
// Provide access the corresponding instance java.lang.ClassLoader.
ClassLoaderData* _class_loader_data;

jint _modifier_flags; // Processed access flags, for use by Class.getModifiers.
AccessFlags _access_flags; // Access flags. The class/interface distinction is stored here.

// Biased locking implementation and statistics
// (the 64-bit chunk goes first, to avoid some fragmentation)
jlong _last_biased_lock_bulk_revocation_time;
markOop _prototype_header; // Used when biased locking is both enabled and disabled for this type
jint _biased_lock_revocation_count;

TRACE_DEFINE_KLASS_TRACE_ID;

// Remembered sets support for the oops in the klasses.
jbyte _modified_oops; // Card Table Equivalent (YC/CMS support)
jbyte _accumulated_modified_oops; // Mod Union Equivalent (CMS support)

...省略不重要的部分...

类的各种数据非常多,不过都是实现细节,为了更好的存储JVM规范里的class file format对应的数据,如类的名字符号,符号引用,常量池数据, 方法引用等等
而继承的类Metadata如下:

1
2
3
4
5
6
7
8
9
10
// hotspot/src/share/vm/oops/metadata.hpp
class Metadata : public MetaspaceObj {
public:
NOT_PRODUCT(Metadata() { _valid = 0; })
NOT_PRODUCT(bool is_valid() const volatile { return _valid == 0; })
...一堆辅助函数...

int identity_hash() { return (int)(uintptr_t)this; }

...

没错,这个identity_hash函数就是hashcode()函数的真身, 可以看到返回了对象地址:).

Metadata继承的MetaspaceObj则是8以后才有的, java8移除了永久代(PermGen),可能因为经常溢出(Spring: 你们看我干啥?), 生活在元空间的对象都要继承这个类,这个放后面聊.

2.2 Instance Data

实例数据存在instanceOopDesc的父类oopDesc类的实例里,就是上面那堆私有属性, JVM还会对字段重排序,相同的宽度可能被分到一起,比如long/double.所以父类的变量可能出现在子类之前,子类中较窄的变量也可能插入到父类的间隙。

Hotspot采用的方法是直接指针访问对象, 如图:

计算对象的大小可以单独讲一小节,也比较有意思,后面再说。

2.3 Padding

对齐填充是最常见的优化手段,CPU一次寻址一般是2的倍数,所以一般会按照2的倍数来对齐提高CPU效率.这个似乎没什么好讲的.
此外,JVM上对齐填充也方便gc, JVM能直接计算出对象的大小, 就能快速定位到对象的起始终止地址.

3. 总结

本节回顾了java对象的创建方式,以及hotspot执行new指令的一系列操作,包括寻找加载类,分配初始化内存,设置对象的元数据信息到header

同时我们通过代码分析了hotspot中对象的内存布局,分为Object Header, Instance DataPadding, 其中对象头包括mark wordklass pointer, mark word里有丰富的对象运行时信息,比如hashcode,gc分代年龄等,而klass pointer指向一个c++对象Klass的实例,也就是JVM类的真身.存放的类的各种信息,如类的名称,继承的父类,实现的接口,字段信息,方法信息等.而Instance Data存放了运行时类的实例数据值, 字段数据的地址会被重排序优化掉。

instanceKlass对象放在了方法区,instanceOop放在了堆,instanceOop的引用放在了JVM栈.

希望大家有所收获:).