JOL(java object layout): java 对象内存布局

         我们天天都在使用java来new对象,但估计很少有人知道new出来的对象到底长的什么样子?对于普通的java程序员来说,可能从来没有考虑过java中对象的问题,不懂这些也可以写好代码。今天,给大家介绍一款工具JOL,可以满足大家对java对象的所有想象。

1、JOL介绍

        JOL的全称是Java Object Layout 即 java 对象内存布局。是一个用来分析JVM中Object布局的小工具。包括Object在内存中的占用情况,实例对象的引用情况等等。

        JOL可以在代码中使用,也可以独立的以命令行中运行。命令行的我这里就不具体介绍了,今天主要讲解怎么在代码中使用JOL。

使用JOL需要添加maven依赖:


    org.openjdk.jol
    jol-core
    0.10

2、java对象结构说明

 对象的结构包括:

        对象头、

        对象体、

        对齐字节(可有可无,若对象头加上对象体是8的倍数时,则不存在字节对齐)。

2.1、对象头

        对象头包含三部分,Mark Word、class point、数组长度。如果对象不是数组,数组长度可以忽略。

 Hotspot 64位实现

2020042612144328.png

2.1.1、Mark Word

        markword 固定长度8byte,描述对象的identityhashcode,分代年龄,锁信息等。

        以下是 Java对象处于5种不同状态时,Mark Word中 64位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:

 * MarkWord的构成如下:
 * ------------------------------------------------------------------------------|-----------|
 *                     Mark Word(64 bits)                                        |  锁状态   |
 * ------------------------------------------------------------------------------|-----------|
 * unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:0 | lock:01 |  正  常   |
 * ------------------------------------------------------------------------------|-----------|
 * thread:54 |    epoch:2    |        unused:1 | age:4 | biased_lock:1 | lock:01 |  偏向锁   |
 * ------------------------------------------------------------------------------|-----------|
 *                     ptr_to_lock_record:62                           | lock:00 |  轻量级锁 |
 * ------------------------------------------------------------------------------|-----------|
 *                 ptr_to_heavyweight_monitor:62                       | lock:11 |  重量级锁 |
 * ------------------------------------------------------------------------------|-----------|
 *                                                                     | lock:11 |  GC标记   |
 * ------------------------------------------------------------------------------|-----------|

说明:

1、lock。2位,锁状态的标记位
2、biased_lock。1位。对象是否存在偏向锁标记lock与biased_lock共同表示锁对象处于什么锁状态。
3、age。4位,表示JAVA对象的年龄,在GC中,当survivor区中对象复制一次,年龄加1,如果到15之后会移动到老年代,并发GC的年龄阈值为6.
4、identity_hashcode。31位,调用方法 System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到线程 Monitor中。
5、thread。54位,持有偏向锁的线程ID(此处的线程id是操作系统层面的线程唯一Id,与java中的线程id是不一致的,了解即可)。
6、epoch。2位,偏向锁的时间戳。
7、ptr_to_lock_record。62位,轻量级锁状态下,指向栈中锁记录的指针。
8、ptr_to_heavyweight_monitor。62位,重量级锁状态下,指向对象监视器 Monitor的指针。

mark word中锁状态描述(根据后三位判断)

偏向锁位 1bit(是否偏向锁) 锁标志位 2bit 锁状态
0(代表无锁) 01 无锁态(new)
1(偏向锁) 01 偏向锁
- 00 轻量级锁(自旋锁、无锁、自适应自旋锁)
- 10 重量级锁
- 11 GC 标记

2.1.2、Class point(类指针)

        这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即 32位的JVM 为 32位,64位的JVM为 64位。

        如果应用的对象过多,使用 64位的指针将浪费大量内存,统计而言,64位的 JVM将会比 32位的 JVM多耗费 50%的内存。为了节约内存可以使用选项 +UseCompressedOops开启指针压缩,其中,oop即 ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

【1】每个 Class的属性指针(即静态变量);
【2】每个对象的属性指针(即对象变量);
【3】普通对象数组的每个元素指针;
当然,也不是所有的指针都会压缩,一些特殊类型的指针 JVM不会优化,比如指向 PermGen的 Class对象指针(JDK8中指向元空间的 Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

        在64位jvm虚拟机中mark word、Class pointer这两部分都是64位的,所以也就是需要128位大小(16 bytes)。

        注意:64位虚拟机中在堆内存小于32GB的情况下,UseCompressedOops是默认开启的,该参数表示开启指针压缩,会将原来64位的指针压缩为32位。

  • 开启(-XX:+UseCompressedOops) 可以压缩指针。
  • 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针。

2.1.3、数组长度

        如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着 JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位 JVM如果开启 +UseCompressedOops选项,该区域长度也将由64位压缩至32位。

2.2、对象体

        对象体中表示对象中的内容,如对象中每个属性。

2.3、对齐字节

        为了方便虚拟机的寻址,比如64位的虚拟机中对象不能被64整除,会补齐对应位。

java的基础数据类型所占内存情况如下表格:

boolean byte short char int long float double
1 bytes 1 bytes 2 bytes 2 bytes 4 bytes 8 bytes 4 bytes 8 bytes

3、基础概念:

问题1. Java对象如何存储?

        对象的实例(instantOopDesc)保存在堆上,

        对象的元数据(instantKlass,即class文件)保存在方法区(元空间),

        对象的引用保存在栈上。

问题2:指针压缩

        开启指针压缩可以减少对象的内存使用。

        在关闭指针压缩时,String、Integer等字段由于是引用类型,因此分别占8个字节;

        而开启指针压缩之后,这两个字段只分别占用4个字节。

        因此,开启指针压缩,理论上来讲,大约能节省接近百分之五十的内存。(如果对象属性都是引用类型的话)

        jdk8及以后版本已经默认开启指针压缩,无需配置。

4、示例

4.1、空属性-对象布局

public static void main(String[] args) {

    Object object = new Object();

    System.out.println(ClassLayout.parseInstance(object).toPrintable());

}

// 输出结果
java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

/* 说明:
    前两个头为mark word,总共占用 8个字节;
 
    第三个头是 class point , 不采用指针压缩,占用 8个字节;采用指针压缩后,占用 4个字节。

*/
  • OFFSET:偏移地址,单位字节;
  • SIZE:占用的内存大小,单位为字节;
  • TYPE DESCRIPTION: 类型描述,其中object header为对象头;
  1. object header:对象头;
  2. loss due to the next object alignment:由于对象对齐而导致的丢失(有4Byte是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0Byte)。
  • VALUE : 对应内存中当前存储的值;
  • Instance size:实例字节数值大小(**此处一个空的java对象(不包含任意字段属性)实例,其实例大小为``16Byte**)。

 4.2、有属性-对象布局

public static void main(String[] args) {
    Student o = new Student();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

// 输出结果(默认开启指针压缩):
com.brown.Student 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)            43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
  12    4     java.lang.Integer Student.age               0
  16    4     java.lang.String Student.name              null
  20    4     (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


// 输出结果(关闭指针压缩)【(-XX:-UseCompressedOops)】:
 com.brown.Student 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)           30 35 64 1c (00110000 00110101 01100100 00011100) (476329264)
  12    4              (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  16    8    java.lang.String Student.name                      null
  24    8    java.lang.Integer Student.age                      null
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

指针压缩对于内存的优化: 

        开通指针压缩时,该对象所占的内存是:24Byte,

        关闭指针压缩时,该对象所占的内存是:32Byte,节省25%的内存

​ 对象头大小的变化:

​        关闭指针压缩时,对象头中元数据指针为Klass类型,占用8个字节;

        开启指针压缩时,对象头中元数据指针为narrowKlass 类型,占用8个字节。

4.3、关于锁-对象布局

此处以synchronized为例,分析MarkWord中对象锁信息的存储情况。

public static void main(String[] args) {

    User user = new User();

    synchronized (user) {
        // 上锁后,打印对象内存布局
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

// 输出结果
com.example.entity.User object internals:
 OFFSET  SIZE                             TYPE DESCRIPTION                               VALUE
      0     4                                  (object header)                           e8 f5 b0 02 (11101000 11110101 10110000 00000010) (45151720)
      4     4                                  (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                  (object header)                           94 ef 00 f8 (10010100 11101111 00000000 11111000) (-134156396)
     12     4                   java.lang.Long User.id                                   null
     16     4                 java.lang.String User.userName                             null
     20     4                 java.lang.String User.passWord                             null
     24     4   com.example.entity.UserSexEnum User.userSex                              null
     28     4                 java.lang.String User.nickName                             null
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看出,现在打印的第一行Mark Word 结果,已经和 上面 4.2 中的输出结果不一样了。

MarkWordk为 0x0000 0000 02b0 f5e8,  二进制为 00000000 00000000 00000000 00000000  00000010 10110000 11110101 11101000 

0x在Java里面是16进制的表示,0x引导的数都是十六进制数

倒数第三位为"0",说明不是偏向锁状态,倒数两位为"10",因此,是轻量级锁状态,那么前面62位就是指向栈中锁记录的指针。

另外,可以看出,执行Synchronized代码块的时候,锁定对象。

计算机用的和输出到的正好相反【把输出结果中的十六进制数倒过来拼一起就是这一串了】。这里涉及到一个知识点“大端存储与小端存储”(汇编语言)。

Little-Endian:低位字节存放在内存的低地址端,高位字节存放在内存的高地址端。
Big-Endian:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。
 

5、使用JOL分析VM信息

常用的方法:

计算对象的大小(单位为字节):ClassLayout.parseInstance(obj).instanceSize()
查看对象内部信息: ClassLayout.parseInstance(obj).toPrintable()
查看对象外部信息:包括引用的对象:GraphLayout.parseInstance(obj).toPrintable()
查看对象占用空间总大小:GraphLayout.parseInstance(obj).totalSize()

你可能感兴趣的:(Java基础,#,线程,并发,#,锁,jvm,java)