图文详解Java对象内存布局

先抛出一个问题:一个空对象内存大小是多少?看完这篇文章或许会有收获

对象内存布局

在这里插入图片描述

对象头(Header)

对象头包含Mark Word、类型指针和数组长度。

Mark Word

Mark Word:用于存储程序运行时的标志位,如锁状态、GC分代年龄和哈希码等。在64位系统下,这部分占8字节;在32位系统下,占4字节

以64位系统为例,Mark Word结构图如下:

在这里插入图片描述

HashCode类似于对象的ID,通过Hash算法生成,常用equals()比较对象是否相等;

分代年龄是指该对象经历了多少次垃圾回收,默认情况下,一个对象在新生代中经历15次垃圾回收(分代年龄>15),仍然存活的话,便会进入老年代

锁标志位是JVM用来识别该对象是否被上锁,以及锁的级别(JVM根据锁膨胀过程会有偏向锁,轻量级锁和重量级锁三个等级);

Class Pointer(类型指针)

类型指针指向实例对象对应的类信息的内存地址,其占用的内存大小有两种情况:

当开启了指针压缩(64位系统默认开启),内存大小是4字节,不开启指针压缩,内存大小是8字节

指针压缩的对象:

  • 类型指针
  • 对象的实例数据
  • 数组对象
数组长度

这部分有数组对象特有,其他对象不存在这部分。

实例数据(Instance Data)

64位系统下,

boolean 和 byte 1字节

char 和 short 2字节

int 和 float 4字节

double 和 long 8字节

对象引用reference 8字节

这部分是实例数据的大小。比如

public class User{
  private Integer id;
  private String userName;
  private Hobby hobby;
  private Object obj;
}

可以推断出 该对象的实例数据部分占的内存大小为4+8+8+8=28字节(不开启指针压缩)或者 4+4+4+4==16字节(开启指针压缩)

对齐填充(Padding)

对齐填充的目的是保证对象的大小是8字节的整数倍。不是必须的,如果对象大小已经是8字节的整数倍了,就不需要对齐填充了

CPU在进行内存访问时,一次寻址的指针大小是8字节

假设没有对齐填充,数据在内存的存放情况如下

在这里插入图片描述

如果想要访问类型为long的数据,因为CPU每次的寻址大小是8字节,则CPU必须两次读取内存,第一次CPU访问0x00-0x07,第二次访问0x08-0x0F,结合两次的结果 才能访问到long的数据

假如采用对齐填充的话,在char和long之间填充一个空字节,数据在内存的存放情况如下
在这里插入图片描述

如果这个时候想要访问long数据的话,只需要一次CPU访问,即读取0x08-0x0F。

对齐填充存在的意义就是为了提高CPU访问数据的效率,这是一种以空间换时间的做法;正如我们所见,虽然访问效率提高了(减少了内存访问次数),但是在0x07处产生了空间浪费。

JOL工具包分析对象内存布局

引入依赖

 <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>RELEASE</version>
 </dependency>
public class User {
    private Integer id;
    private String name;
    private Object obj;
    private List<Integer> list;

    public static void printf(User p) {
        // 查看对象的整体结构信息
        System.out.println(ClassLayout.parseInstance(p).toPrintable());
    }

    public static void main(String[] args) {
        User user=new User();
        System.out.println(user.hashCode());
        User.printf(user);
    }

}
输出结果如下:
495053715
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.mybatisplus.demo.demo.jvm.User object internals:
 OFFSET  SIZE                TYPE DESCRIPTION                               VALUE0     4                  (object header)                           01 93 eb 81 (00000001 10010011 11101011 10000001) (-2115267839)4     4                  (object header)                           1d 00 00 00 (00011101 00000000 00000000 00000000) (29)8     4                  (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)12     4   java.lang.Integer User.id                                   null16     4    java.lang.String User.name                                 null20     4    java.lang.Object User.obj                                  null24     4      java.util.List User.list                                 null28     4                     (loss due to the next object alignment)
⑤ Instance size: 32 bytes
⑥ Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以通过vm args : -XX:+PrintFlagsFinal 查看到UseCompressedOops 值为true。印证了前面说的64位操作系统默认开启指针压缩

对上面的输出格式做介绍:

标记①、②是对象头的部分,标记③是实例数据部分,标记④是对齐填充,标记⑤ 代表对象的大小,标记⑥ 代表因对齐填充导致内存消耗的大小(若不需要对齐填充,也就没有④和⑥了)

其中① 代表了Mark Word,占用8个字节,其中第一个字节是00000001,结合前面Mark Word的结构发现其是无锁结构的Mark Word。这里对第一个字节拆分下

在这里插入图片描述

再来分析下hashcode,其值是495053715,对应16进制是1d81eb93,对应二进制是 00011101 10000001 11101011 10010011

但是发现①处设置的值是93eb811d,刚好是hashcode的倒序存放,将二进制倒置10010011 11101011 10000001 00011101 刚好跟上面吻合。其实这是大端存储的存储方式。

大端存储: 高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。

小端存储: 高位字节存储在内存的高地址端,低位字节存储在内存的低地址端。这是我们平常的存放逻辑

举例:65306 对应 FF1A,如果采用小端存储就是FF1A,如果采用大端存储就是1AFF

因为Mark Word结构说这种hashCode是31位的,因此我认为对应原始二进制数据( 00011101 10000001 11101011 10010011)的最高位0

余下的3个字节,合24位没有使用。

②处是类型指针,指向对象类元数据的内存地址,由于使用了指针压缩,内存大小大小由8字节->4字节

③处是实例数据,跟我们之前分析的一致,使用了指针压缩,这几部分占用大小都是4字节(int 基本数据类型 占4个字节)。

对前面①、②、③处进行内存大小计算:(4+4)+4+(4+4+4+4)=28字节

由于规定对象大小是8字节的整数倍,需要填充4字节,凑够32字节,并且刚好按顺序①①、②③、③③处 都能凑成8字节的整数倍,因此对齐填充在最后一个③后

所以⑤处对象的大小是32字节,⑥处对齐填充的大小是4字节。

手动关闭指针压缩,看下内存大小变换

VM Options: -XX:-UseCompressedOops

495053715
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.mybatisplus.demo.demo.jvm.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 93 eb 81 (00000001 10010011 11101011 10000001) (-2115267839)
      4     4                    (object header)                           1d 00 00 00 (00011101 00000000 00000000 00000000) (29)
      8     4                    (object header)                           80 f0 66 0e (10000000 11110000 01100110 00001110) (241627264)
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
①   16     4                     int User.id                                   0
     20     4                    (alignment/padding gap)                  
     24     8   java.lang.String User.name                                 null
     32     8   java.lang.Object User.obj                                  null
     40     8     java.util.List User.list                                 null
Instance size: 48 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以发现 几个引用类型的内存大小均变成了8字节。通过前面的分析,对齐填充自然而然地在①后

问题: 如果一个空对象,内存大小是多少呢?

空对象的话,没有实例数据。

对象头部分:Mark Word 占8字节,类型指针(指针压缩):占4字节;类型指针(不启用指针压缩):占8字节,

对齐填充:为了保证对象的内存大小是8 的整数倍,需要填充4字节(指针压缩);不启用指针压缩,不需要填充字节

因此一个空对象大小是16字节。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值