Java Object Layout – Java对象的内存布局

java对象实例64位操作系统.png

在 Java 程序中,我们拥有多种新建对象的方式。除了最为常见的 new 语句之外,我们还可以通过反射机制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance 方法来新建对象

Object.clone 方法和反序列化通过直接复制已有的数据,来初始化新建对象的实例字段。Unsafe.allocateInstance 方法则没有初始化实例字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。

new 语句为例,字节码将包含用来请求内存的 new 指令,以及用来调用构造器的 invokespecial 指令。

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

对应字节码

  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    NEW top/zcwfeng/java/test/TestFoo
    DUP
    INVOKESPECIAL top/zcwfeng/java/test/TestFoo.<init> ()V
    ASTORE 1

如果一个类没有定义任何构造器的话,java编译器会自动添加一个无参数的构造器

public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN

压缩指针

在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段(Mark Word)和类型指针(Klass Pointer)所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息,而类型指针则指向该对象的类。

在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位。也就是说,每一个 Java 对象在内存中的额外开销就是 16 个字节。以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。

为了尽量较少对象的内存使用量,64 位 Java 虚拟机引入了压缩指针 [1] 的概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本 64 位的 Java 对象指针压缩成 32 位的。

这样一来,对象头中的类型指针也会被压缩成 32 位,使得对象头的大小从 16 字节降至 12 字节。当然,压缩指针不仅可以作用于对象头的类型指针,还可以作用于引用类型的字段,以及引用类型数组。

上面是官方的解释。我们弄明白几个问题:

32位操作系统可以寻址到多大内存

答:4g 因为 2^32=4 * 1024 * 1024=4g

64位呢?

2的64次方bai:18446744073709551616

这个数有点大,计算器一般算不出来,编程的话用long值才能计算到2的62次方

答:64位过长,给我们寻址带宽和对象内引用造成了负担

一个对象占用的字节数

对象头:
32位系统,占用 8 字节(markWord4字节+kclass4字节)
64位系统,开启 UseCompressedOops(压缩指针)时,占用 12 字节,否则是16字节(markWord8字节+kclass8字节,开启时markWord8字节+kclass4字节)

实例数据

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

引用类型

32位系统占4字节 (因为此引用类型要去方法区中找类信息,所以地址为32位即4字节同理64位是8字节)
64位系统,开启 UseCompressedOops时,占用4字节,否则是8字节

对齐填充

如果对象头+实例数据的值不是8的倍数,那么会补上一些,补够8的倍数

32位操作系统 花费的内存空间为
对象头-8字节 + 实例数据 int类型-4字节 + 引用类型-4字节+补充0字节(16是8的倍数) 16个字节

64位操作系统(未开启指针压缩)
对象头-16字节 + 实例数据 int类型-4字节 + 引用类型-8字节+补充4字节(28不是8的倍数补充4字节到达32字节) 32个字节

同样的对象需要将近两倍的容量,(实际平均1.5倍)

64位开启压缩指针

对象头-12字节 + 实例数据 int类型-4字节 + 引用类型-4字节+补充0字节=24个字节---减缓堆空间的压力(同样的内存更不容易发生oom)

JVM的实现方式是
不再保存所有引用,而是每隔8个字节保存一个引用。例如,原来保存每个引用0、1、2…,现在只保存0、8、16…。因此,指针压缩后,并不是所有引用都保存在堆中,而是以8个字节为间隔保存引用。
在实现上,堆中的引用其实还是按照0x0、0x1、0x2…进行存储。只不过当引用被存入64位的寄存器时,JVM将其左移3位(相当于末尾添加3个0),例如0x0、0x1、0x2…分别被转换为0x0、0x8、0x10。而当从寄存器读出时,JVM又可以右移3位,丢弃末尾的0。(oop在堆中是32位,在寄存器中是35位,2的35次方=32G。也就是说,使用32位,来达到35位oop所能引用的堆内存空间)

哪些信息会被压缩?

1.对象的全局静态变量(即类属性)
2.对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节
3.对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节
4.对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

哪些信息不会被压缩?

1.指向非Heap的对象指针
2.局部变量、传参、返回值、NULL指针

在JVM中(不管是32位还是64位),对象已经按8字节边界对齐了。对于大部分处理器,这种对齐方案都是最优的。所以,使用压缩的oop并不会带来什么损失,反而提升了性能。

看一个实例

class A {
    long l;
    int i;
}

class B extends A {
    long l;
    int i;
}


开启压缩指针 开启(-XX:+UseCompressedOops)  默认开启
> Task :TestFoo.main()
------------B---------------
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
top.zcwfeng.java.test.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           50 04 06 00 (01010000 00000100 00000110 00000000) (394320)
     12     4    int A.i                                       0
     16     8   long A.l                                       0
     24     8   long B.l                                       0
     32     4    int B.i                                       0
     36     4        (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


================

关闭压缩指针 关闭(-XX:-UseCompressedOops) 可以关闭压缩指针
> Task :TestFoo.main()
------------B---------------
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
top.zcwfeng.java.test.B object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 12 e0 a1 (01001000 00010010 11100000 10100001) (-1579150776)
     12     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     8   long A.l                                       0
     24     4    int A.i                                       0
     28     4        (alignment/padding gap)                  
     32     8   long B.l                                       0
     40     4    int B.i                                       0
     44     4        (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total

字段重排列

Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的。Java 虚拟机中有三种排列方法(对应 Java 虚拟机选项 -XX:FieldsAllocationStyle,默认值为 1),但都会遵循如下两个规则。

其一,如果一个字段占据 C 个字节,那么该字段的偏移量需要对齐至 NC。这里偏移量指的是字段地址与对象的起始地址差值。

以 long 类为例,它仅有一个 long 类型的实例字段。在使用了压缩指针的 64 位虚拟机中,尽管对象头的大小为 12 个字节,该 long 类型字段的偏移量也只能是 16,而中间空着的 4 个字节便会被浪费掉。

其二,子类所继承字段的偏移量,需要与父类对应字段的偏移量保持一致。

在具体实现中,Java 虚拟机还会对齐子类字段的起始位置。对于使用了压缩指针的 64 位虚拟机,子类第一个字段需要对齐至 4N;而对于关闭了压缩指针的 64 位虚拟机,子类第一个字段则需要对齐至 8N。

上面的分析,加入了工具JOL的帮助

gradle 配置

implementation 'org.openjdk.jol:jol-core:0.14'

java 环境

java 11.0.10 2021-01-19 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.10+8-LTS-162)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.10+8-LTS-162, mixed mode)

当然Java8 版本有的也可以,我的失败了,为了方便所有我选择存在的环境11

然后调用可以分析:

 System.out.println("------------B---------------");
        B o = new B();
        String s = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(s);
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容