基本功:初探对象的内存布局

java中我们随处可见的都是对象,而对象成为我们与计算机内核交换的主要载体,使用起来也非常简单,然而一个对象是如何被JVM创建的却是极其的复杂,它要经历类加载机制、分片内存以及设置对象头的内存布局。

下面讲介绍下Hotspot JVM下新建对象需要基本过程。

前言-字节码

new语句最会被编译而成的字节码,而它将包含用来请求内存指令。

public class ObjectSizeMain{
	public static void main(String []args){
		ObjectSizeMain obj=new ObjectSizeMain();
	}
	
}

C:\Users\Administrator\Desktop>javap -v ObjectSizeMain.class
Classfile /C:/Users/Administrator/Desktop/ObjectSizeMain.class
  Last modified 2019-6-1; size 290 bytes
  MD5 checksum 1635999ab99de05314d8568fbaaf6900
  Compiled from "ObjectSizeMain.java"
public class ObjectSizeMain
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // ObjectSizeMain
   #3 = Methodref          #2.#13         // ObjectSizeMain."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               ObjectSizeMain.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               ObjectSizeMain
  #15 = Utf8               java/lang/Object
{
  public ObjectSizeMain();
    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
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class ObjectSizeMain
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8

}
SourceFile: "ObjectSizeMain.java"

重点关注下红色部分内容,它透露了一些信息:

  1. new 指令,用来请求内存及用来调用构造器的 invokespecial 指令
  2. invokespecial指令,用于调用私有实例方法、构造器,以及使用super关键字调用父类的实例方法或构造器,和所实现接口的默认方法

关于字节码可以参阅《实战JAVA虚拟机  JVM故障诊断与性能优化》中class文件结构的章节

1. 类加载机制

JVM首先检查一个new指令的参数是否能在常量池中定位到一个符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过(实际上就是在检查new的对象所属的类是否已经执行过类加载机制)。如果没有,先进行类加载机制加载类。可参阅你忽略的ClassLoader

2. 分配堆内存

简单理解为:确定对象内存大小并从Java堆中划分出来

  • 在类加载完成后,一个对象所需的内存大小就可以完全确定了
  • 为对象分配空间,即把一块儿确定大小的内存从Java堆中划分出来

内存分配两种方式

Java堆内存是否规整,取决于GC收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的,然后选择对应的方式。

指针碰撞

适用场合:堆内存规整(即没有内存碎片)的情况下

原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可

GC收集器:Serial、ParNew

空闲列表

适用场合:堆内存不规整的情况下

原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例(这一块儿可以类比memcached的slab模型),最后更新列表记录。

GC收集器:CMS

内存分配并发问题

堆内存是各个线程的共享区域,所以在操作堆内存的时候,需要处理并发问题。处理的方式有两种:

CAS+失败重试

做法与AtomicInteger的getAndSet(int newValue)方法的实现方式类似

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();// 获取当前值(旧值)
        if (compareAndSet(current, newValue))// CAS新值替代旧值
            return current;// 返回旧值
    }
}

TLAB

(Thread Local Allocation Buffer)

原理:为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配

-XX:+/-UseTLAB:是否使用TLAB

-XX:TLABWasteTargetPercent:设置TLAB可占用的Eden区的比率,默认为1%

-XX:PrintTLAB:查看TLAB的使用情况

JVM会根据以下三个条件来给每个线程分配合适大小的TLAB:

1.-XX:TLABWasteTargetPercent

2.线程数量

3.线程是否频繁分配对象

3. 初始化零值并设置对象头

  • 对象的实例字段不需要赋初始值也可以直接使用其默认零值
  • 每一种类型的对象都有不同的默认零值

对象在内存中存储的布局分为三块

对象头

存储对象自身的运行时数据:Mark Word(在32bit和64bit虚拟机上长度分别为32bit和64bit),包含如下信息:

  • 对象hashCode
  • 对象GC分代年龄,年龄大导致进入老生代
  • 锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)
  • 线程持有的锁(无锁、偏向锁、轻量级锁、重量级锁)
  • oops-类型指针,ordinary object pointers,普通对象指向类元数据的指针(32bit-->32bit,64bit-->64bit(未开启压缩指针),32bit(开启压缩指针))。JVM通过这个指针来确定这个对象是哪个类的实例(根据对象确定其Class的指针)

-XX:+UseCompressedOops:JDK 8下默认为启用,启用对象的指针压缩,节约内存占用的大小

-XX:+CompactFields:JDK 8下默认为启用,分配一个非static的字段在前面字段缝隙中,提高内存的利用率

-XX:FieldsAllocationStyle=1:JDK 8下默认值为‘1’,实例对象中有效信息的存储顺序

  • 0:先放入oops,然后在放入基本变量类型(顺序:longs/doubles、ints/floats、shorts/chars、bytes/booleans),如果longs/doubles都有按定义的顺序
  • 1:先放入基本变量类型(顺序:longs/doubles、ints/floats、shorts/chars、bytes/booleans),然后放入oops
  • 2:oops和基本变量类型交叉存储

注:Mark Word具有非固定的数据结构,以便在极小的空间内存储尽量多的信息。如果对象是一个数组,对象头必须有一块儿用于记录数组长度的数据。JVM可以通过Java对象的元数据确定对象长度,但是对于数组不行。

一般对象头大小
                    32 位系统   64 位系统(+UseCompressedOops)    64 位系统(-UseCompressedOops)
Mark Word       4 bytes        8 bytes                                  8 bytes
Class Pointer   4 bytes        4 bytes                                  8 bytes
总计                 8 bytes        12 bytes                               16 bytes

数组对象对象头大小
                32 位系统        64 位系统(+UseCompressedOops)    64 位系统(-UseCompressedOops)
Mark Word      4 bytes     8 bytes                            8 bytes
Class Pointer  4 bytes     4 bytes                            8 bytes
Length            4 bytes     4 bytes                             2 bytes
总计                12 bytes   16 bytes                          20 bytes

基本数据类型(32/64位,不区分压缩)
double    8 bytes
long        8 bytes
float        4 bytes
int           4 bytes
char        2 bytes
short       2 bytes
byte        1 bytes
boolean  1 bytes
oops       4 bytes
包装类型
                              +useCompressedOops    -useCompressedOops
Byte, Boolean        16 bytes                           24 bytes
Short, Character     16 bytes                          24 bytes
Integer, Float          16 bytes                          24 bytes
Long, Double           24 bytes                         24 bytes

在java中对象的每个成员属性都有一个offset,通过UnsafeUtils.unsafe().objectFieldOffset可以获得。但分配内存是有些差别,64位系统中CPU一次读操作可读取64bit(8 bytes)的数据,读取属性long不能读取2次(如果从对象头开始就会发生),因此会打破存储顺序(FieldsAllocationStyle)。字段重排列技术指的是重新分配字段的先后顺序,以达到内存对齐的目的。

实例数据 对象真正存储的有效信息
对齐填充 HotSpot VM的自动内存管理系统要求对象大小必须是8字节的整数倍,对齐填充没有特别的含义,它仅仅起着占位符的作用

4. 执行init

为对象的字段赋值(这里会根据所写程序给实例赋值)

小刀牛试

public class ObjectSize_test {
    /**
     * 启动参数中添加: -javaagent:D:\Program\repository\classmexer\classmexer\0.03\classmexer-0.03.jar
     */
    @Test
    public void showObjSize() {
        LongInstace v = new LongInstace();
        //24=12+8+2+2=对象头+long+byte+padding(2),不压缩的话就是16+8+2...
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v));
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v));

        LongInstace2 v2 = new LongInstace2();
        //16=12+4=对象头+oops
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v2));
        //40=16+24=LongInstace2+Long
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v2));

        IntegerInstace1 v3 = new IntegerInstace1();
        //16=12+4=对象头+oops
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(v3));
        //32=16+16=LongInstace3+Integer,有压缩
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(v3));

        LongInstace[]vs=new LongInstace[2];
        vs[0]=v;
        System.out.printf("shallow size:    %s.byte\n", MemoryUtil.memoryUsageOf(vs));
        System.out.printf("retained size:   %s.byte\n", MemoryUtil.deepMemoryUsageOf(vs));

        System.out.printf("first item offset : %s,per item size: %s \n",UnsafeUtils.unsafe().arrayBaseOffset(vs.getClass()),UnsafeUtils.unsafe().arrayIndexScale(LongInstace[].class));

        ComplexInstance obj = new ComplexInstance();
        //48=12+1+2+2+4+4+8+8+padding(7)
        System.out.println("ComplexInstance:" + MemoryUtil.memoryUsageOf(obj));
    }

    @Test
    public void showLayout() throws NoSuchFieldException {
        /**
         * 无填充
         * 内存布局:对象头(12) + oops(4)
         */
        long offset = UnsafeUtils.unsafe().objectFieldOffset(IntegerInstace1.class.getDeclaredField("value"));
        //12=对象头(12) 开始
        System.out.printf("IntegerInstace1.value(Integer):  %s \n", offset);
        /**
         * 有填充
         * 规则:64位系统中CPU一次读操作可读取64bit(8 bytes)的数据,读取属性long不能读取2次(如果从对象头开始就会发生)
         */
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace1.class.getDeclaredField("value"));
        //16=对象头(12)+padding(4)
        System.out.printf("LongInstace1.value(long):    %s \n", offset);
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value2"));
        //12=对象头(12)    -XX:+CompactFields生效
        System.out.printf("LongInstace.value2(byte):    %s \n", offset);
        offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value3"));
        //13=对象头(12)+value2     -XX:+CompactFields生效
        System.out.printf("LongInstace.value3(char):    %s \n", offset);
        /**
         * 内部有多个成员变量时,布局按-XX:FieldsAllocationStyle=1(longs/doubles、ints/floats、shorts/chars、bytes/booleans)
         * double=24=对象头(12)+int(4)+long(8),double和long是有变量定义的顺序决定的,不行你可以试试
         * 48=对象头(12)+int(4)+long(8)+double(8)+float(4)+char(2)+short(2)+boolean(1)+padding(7)
         */
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("bool"));
        System.out.printf("ComplexInstance.bool(boolean):   %s \n", offset);//40
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("cha"));
        System.out.printf("ComplexInstance.cha(char):   %s \n", offset);//36
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("sho"));
        System.out.printf("ComplexInstance.sho(short):   %s \n", offset);//38
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("in"));
        System.out.printf("ComplexInstance.in(int):   %s \n", offset);//12
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("flo"));
        System.out.printf("ComplexInstance.flo(float):   %s \n", offset);//32
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("lon"));
        System.out.printf("ComplexInstance.lon(long):   %s \n", offset);//16
        offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("dou"));
        System.out.printf("ComplexInstance.dou(double):   %s \n", offset);//24
    }

    public final static class LongInstace {
        protected long value = 0L;
        byte value2;
        boolean value3;
    }

    public final static class LongInstace1 {
        protected long value = 0L;
    }

    public final static class LongInstace2 {
        Long value = 0L;
    }

    public final static class IntegerInstace1 {
        Integer value = 0;
    }

    public final static class ComplexInstance {
        boolean bool;

        char cha;
        short sho;

        int in;
        float flo;

        long lon;
        double dou;
    }
}

 

参阅资料

  • 《HotSpot实战》
  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(最新第二版)》
  • 《实战JAVA虚拟机  JVM故障诊断与性能优化》
  • 《分布式Java应用:基础与实践》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章