我们天天都在使用java来new对象,但估计很少有人知道new出来的对象到底长的什么样子?对于普通的java程序员来说,可能从来没有考虑过java中对象的问题,不懂这些也可以写好代码。今天,给大家介绍一款工具JOL,可以满足大家对java对象的所有想象。
1、JOL介绍
JOL的全称是Java Object Layout 即 java 对象内存布局。是一个用来分析JVM中Object布局的小工具。包括Object在内存中的占用情况,实例对象的引用情况等等。
JOL可以在代码中使用,也可以独立的以命令行中运行。命令行的我这里就不具体介绍了,今天主要讲解怎么在代码中使用JOL。
使用JOL需要添加maven依赖:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
2、java对象结构说明
对象的结构包括:
对象头、
对象体、
对齐字节(可有可无,若对象头加上对象体是8的倍数时,则不存在字节对齐)。
2.1、对象头
对象头包含三部分,Mark Word、class point、数组长度。如果对象不是数组,数组长度可以忽略。
Hotspot 64位实现
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为对象头;
- object header:对象头;
- 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()