java对象内存布局模型以及内存大小详解
今天和大家简单介绍下java对象在内存中的布局,以及一个对象在内存中所占的大小,我们以前都了解过比如一个int在java中占4个byte,一个boolean占用一个字节,那么如果是一个Integer对象到底占用多少字节了?影响一个对象在内存中占用大小的因素都有哪些了?希望通过这篇文章能给大家一个比较清晰的认识。
内存存储模型.png在hotspot中,一个java对象由对象头、实例数据和对象填充三部分组成,对象头又由对象标记、类元信息、数组长度(只有数组对象才有)组成。在HotSpot源码中,instanceOop.hpp类中
#include "oops/oop.hpp"
// An instanceOop is an instance of a Java Class
// Evaluating "new HashTable()" will create an instanceOop.
class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
...
}
我们可以看到所有的java对象都会创建一个instanceOop对象
对象头
通过上面的源码,我们可以看到instanceOopdesc继承oopDesc,我们查看oopDesc.hpp代码查看到:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark; // 对象标记
union _metadata {
Klass* _klass; // 普通指针
narrowKlass _compressed_klass; // 压缩指针
} _metadata; // 类元信息
...
可以很清楚的看到顶层Oop中包含了_mark和_metadata,下面我们单独来分析:
1、对象标记(Mark Word)
原文:The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.
对象标记又称Mark Word,用于存储对象运行时的一些特殊数据,比如HashCode、GC分代年龄、锁状态标志、持有锁的线程Id、偏向锁线程Id、偏向时间等。
可以通过下图全面的了解对象标记的组成:
对象头标记组成.png对象标记在32位操作系统和64位操作系统(未开启压缩指针)中分别为4byte和8byte,我们可以从hotspot源码中看到如下介绍:
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
我们可以通过在项目中引入openjdk的jol包,来查看对象大小以及对象头中的信息:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
我们通过一个简单的例子来查看对象头的大小,我的操作系统是64位,JDK1.8版本,默认是开启指针压缩的:
public class JolDemo {
public static void main(String[] args) {
JolDemo jolDemo = new JolDemo();
synchronized (jolDemo) {
System.out.println(ClassLayout.parseInstance(jolDemo).toPrintable());
}
}
}
执行结果:
com.laozhao.oop.JolDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) f0 a8 8a 09 (11110000 10101000 10001010 00001001) (160082160)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到对象头为8位MarkWord + 4位KlassPointer = 12位,填充了4位,最后对象大小为16位。这是因为所有对象大小都要凑够8的倍数,如若不够,就填充对齐,比如这里填充了4个字节。
上面这个例子中,我们特意加入了Synchronized锁,就是为了简单介绍对象头中锁标识的存储信息。可以看到此处在head里第一个8bit 11110000中,最后3位是000,代表使用的是轻量级锁,我们对代码做如下改动:
public class JolDemo { public static void main(String[] args) { JolDemo jolDemo = new JolDemo(); synchronized (jolDemo) { jolDemo.hashCode(); System.out.println(ClassLayout.parseInstance(jolDemo).toPrintable()); } } }
com.laozhao.oop.JolDemo object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 5a d5 83 d2 (01011010 11010101 10000011 11010010) (-763112102) 4 4 (object header) 95 7f 00 00 (10010101 01111111 00000000 00000000) (32661) 8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到 01011010,最后3位变成了010,代表使用的是重量级锁,仅仅是因为我们调用了对下的hashCode(), 各位知道这里的原因么?
2、类元信息(Klass Pointer)
原文:The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the "klass" contains a C++ style "vtable".
类元信息是指向对象类元数据的指针,虚拟机通过这个指针来确定我们的对象属于哪个实例
类元信息在虚拟机中,32位操作系统长度是4byte,64位是8byte(如果开启了指针压缩,则也是4byte)。
可以通过JVM启动参数:-XX:+UseCompressedOops
来启用指针压缩,这个参数只有64位的Hotspot支持,在jdk1.8中默认是开启的,如果需要禁止指针压缩,直接去掉该参数或者更改成-XX:-UseCompressedOops
即可。
我们还是用下面的demo,来查看开启/关闭指针压缩的情况:
public class JolDemo {
public static void main(String[] args) {
JolDemo jolDemo = new JolDemo();
System.out.println(ClassLayout.parseInstance(jolDemo).toPrintable());
}
}
开启指针压缩的结果:
com.laozhao.oop.JolDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们可以看到对象头占了12个字节,填充了4位,对象总大小是16字节。
关闭指针压缩的结果:
com.laozhao.oop.JolDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 28 70 e9 0f (00101000 01110000 11101001 00001111) (266956840)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
关闭指针压缩后可以看到对象头占了16个字节,对象总大小是16字节,此时填充字节数为0.
3、数组长度(Length)
如果是数组对象(objArrayOopDesc或typeArrayOopDesc)则会在对象头中存储数组长度信息。
我们关闭指针压缩,然后采用如下代码来查看数组长度信息:
public class JolDemo {
public static void main(String[] args) {
int[] strs = new int[]{22, 33, 44};
System.out.println(ClassLayout.parseInstance(strs).toPrintable());
}
}
运行代码获得如下结果:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3)
16 12 int [I.<elements> N/A
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
可以看到对象头为8个MarkWord + 4个KlassPointer + 4个数组长度 = 16个字节,数组中有3个int元素,每一个int占用4个字节,总共12个字节,16 + 12 = 28,不足8的倍数,所以填充4个字节。
综上我们通过图标来梳理对象头在操作系统中占用的字节大小:
Mark Word | Klass Pointer | Length | |
---|---|---|---|
32位操作系统 | 4 | 4 | 4 |
64位未开启指针压缩 | 8 | 8 | 4 |
64位开启指针压缩 | 8 | 4 | 4 |
对象实际数据
java对象在内存中的实际大小跟所拥有的成员变量有关,而不受方法和构造函数等的影响。以下是几种基本数据类型在内存中占用的大小情况:
基本数据类型 | 占用字节 |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
reference | 4/8 |
我们通过代码来验证:
public class BasicTypeDemo {
private boolean _boolean = false;
private byte _byte = 1;
private char _char = '2';
private short _short = 2;
private int _int = 4;
private float _float = 4;
private double _double = 8;
private long _long = 8;
public static void main(String[] args) {
BasicTypeDemo demo = new BasicTypeDemo();
System.out.println(ClassLayout.parseInstance(demo).toPrintable());
}
}
执行结果如下:
com.laozhao.oop.BasicTypeDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int BasicTypeDemo._int 4
16 8 double BasicTypeDemo._double 8.0
24 8 long BasicTypeDemo._long 8
32 4 float BasicTypeDemo._float 4.0
36 2 char BasicTypeDemo._char 2
38 2 short BasicTypeDemo._short 2
40 1 boolean BasicTypeDemo._boolean false
41 1 byte BasicTypeDemo._byte 1
42 6 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
包装类(Boolean/Byte/Short/Character/Integer/Long/Double/Float)占用内存的大小, 等于对象头的大小加上基础数据类型的大小。
以Boolean为例:
-
开启压缩指针
对象头12位,boolean基础数据占1位,则总实例大小为
12 + 1 + 3 = 16
, -
关闭压缩指针
对象头16位,boolean基础数据占1位,则总实例大小为
16 + 1 + 7 = 24
所以我们得到包装类类型占用的空间如下表:
包装类型 开启压缩指针(byte) 关闭压缩指针(byte) Byte、BOOLEAN 16 24 SHORT、CHARACTER 16 24 INTEGER、FLOAT 16 24 LONG、DOUBLE 24 24
那同样我们来用代码简单验证下,以开启压缩Long为例:
public class BasicTypeDemo {
public static void main(String[] args) {
Long demo = new Long("2");
System.out.println(ClassLayout.parseInstance(demo).toPrintable());
}
}
java.lang.Long object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) f4 22 00 f8 (11110100 00100010 00000000 11111000) (-134208780)
12 4 (alignment/padding gap)
16 8 long Long.value 2
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
从结果可以看出占用空间是24byte。
对齐填充
java对象占用空间都是8的倍数,比如一个对象包含两个属性:int和long,所占空间为4+8=12,不是8的倍数,所以需要填充4个字节变成16,因此此对象的大小是16字节。
从上面的执行结果中,我们可以看到padding并不是在所有变量的最后填充的,这是因为JVM在变量的声明时会发生重排序,在特定的空间位置会来填充padding来对齐8字节长度,关于hotspot中的alignment,我们后续单独用一篇文章来介绍。