程序员老赵java架构师之路

java对象内存布局模型以及内存大小详解

2020-05-19  本文已影响0人  l只为终点

今天和大家简单介绍下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为例:

  1. 开启压缩指针

    对象头12位,boolean基础数据占1位,则总实例大小为12 + 1 + 3 = 16

  2. 关闭压缩指针

    对象头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,我们后续单独用一篇文章来介绍。

上一篇下一篇

猜你喜欢

热点阅读