移动 前端 Python Android JavaJVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

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

2021-03-07  本文已影响0人  zcwfeng
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);
上一篇 下一篇

猜你喜欢

热点阅读