Java-对象到底占多少个字节?计算规则是什么?

Java
229
0
0
2023-11-12
标签   Java基础

Java 对象模型

我们先了解一下,一个JAVA对象的存储结构。在Hotspot虚拟机中,对象在内存中的存储布局分为 3 块区域:对象头( header )、实例数据(Instance Data)和对齐填充( padding )。

java 对象的大小默认是按照 8 字节对齐 ,也就是说 Java 对象的大小必须是 8 字节的倍数。若是算到最后不够 8 字节的话,那么就会进行对齐填充。

那么为何非要进行 8 字节对齐呢?这样岂不是浪费了空间资源?

其实不然,由于 CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好也是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行的情况,这叫做 缓存行污染。

当然,也不是所有的指针都会压缩,一些特殊类型的指针 JVM 不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头(Header)

对象头,又包括三部分:Mark Word、(Klass Word)元数据指针、数组长度。

MarkWord:用于存储对象运行时的数据,比如 HashCode 、锁状态标志、GC分代年龄等。这部分在64位操作系统下,占8字节(64bit),在32位操作系统下,占4字节(32bit)。

Mark Word布局:

Klass Word:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

这部分就涉及到一个指针压缩的概念,在开启指针压缩的情况下,占4字节(32bit),未开启情况下,占8字节(64bit),现在JVM在1.6之后,在64位操作系统下都是默认开启的。

数组长度:这部分只有是数组对象才有(int[]),如果是非数组对象,就没这部分了,这部分占4字节(32bit)。

内存对齐

想要知道为什么虚拟机要进行对齐填充,我们需要了解什么是内存对齐?在开发人员眼中,我们看到的内存是这样的:

上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反, CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。如下图:

假设一个32位平台的 CPU,那它就会以4字节为粒度去读取内存块。那为什么需要内存对齐呢?主要有两个原因:

平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况。

性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作。

下面用图例来说明 CPU 访问非内存对齐的过程:

在上图中,假设CPU 是一次读取4字节,在这个连续的8字节的内存空间中,如果我的数据没有对齐,存储的内存块在地址1,2,3,4中,那CPU的读取就会需要进行两次读取,另外还有额外的计算操作:

1、CPU 首次读取未对齐地址的第一个内存块,读取 0-3 字节。并移除不需要的字节 0。

2、CPU 再次读取未对齐地址的第二个内存块,读取 4-7 字节。并移除不需要的字节 5、6、7 字节。

3、合并 1-4 字节的数据。

4、合并后放入 寄存器 。

所以,没有进行内存对齐就会导致CPU进行额外的读取操作,并且需要额外的计算。如果做了内存对齐,CPU可以直接从地址0开始读取,一次就读取到想要的数据,不需要进行额外读取操作和运算操作,节省了运行时间。我们用了空间换时间,这就是为什么我们需要内存对齐。

回到Java空对象填充了4个字节的问题,因为原字节头是12字节,64位机器下,内存对齐的话就是128位,也就是16字节,所以我们还需要填充4个字节。(64位机器一次读取8字节,因为64位下填充为8字节的整数倍,这里12字节,显然填充到16字节效果最佳。)

内存消耗演示

 <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>.9</version>
        </dependency> 

空对象(16 Byte )

 public  static  class Dog {
 
    }

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

明明是12为啥变成16了, 这个就是使用了内存对齐了, 8的倍数

基本数据类型(1~8byte)

 public static class Dog {
 
        int a;
        long a;
        double a;
         char  a3;
         Float  a4;
         Boolean  a5;
         short  a6;
        byte a;
    }

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

引用地址(4字节)

 public static class Dog {
 
        Date a;
        String a;
        Dog a;

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

任何非基本类型的变量都存储的是对象的引用地址,而在java中地址是使用4字节存储的

包装类型和 String (16~24byte)

 public static void main(String[] args) {
 
         Integer  a=1;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Long a=12313123L;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Double a=1.1;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Character a='a';
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Float a=1.1F;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Boolean a=true;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Short a=1;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        Byte a=1;
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
        String a="xxxx";
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

    } 
 java.lang.Integer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4        (object header)                           67 22 00 f8 (01100111 00100010 00000000 11111000) (-134208921)
     4    int Integer.value                             1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 0 bytes total

 java.lang .Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4        (object header)                           ae 22 00 f8 (10101110 00100010 00000000 11111000) (-134208850)
     4        (alignment/padding gap)                  
     8   long Long.value                                12313123
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 4 bytes total

java.lang.Double object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE     4          (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     4          (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4          (object header)                           92 21 00 f8 (10010010 00100001 00000000 11111000) (-134209134)
     4          (alignment/padding gap)                  
     8   double Double.value                              1.1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 4 bytes total

java.lang.Character object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4        (object header)                           c6 20 00 f8 (11000110 00100000 00000000 11111000) (-134209338)
     2   char Character.value                           a
     2        (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 2 bytes external = 2 bytes total

java.lang.Float object internals:
 OFFSET  SIZE    TYPE DESCRIPTION                               VALUE     4         (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     4         (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4         (object header)                           4b 21 00 f8 (01001011 00100001 00000000 11111000) (-134209205)
     4   float Float.value                               1.1
Instance size: bytes
Space losses: bytes internal + 0 bytes external = 0 bytes total

java.lang.Boolean object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4           (object header)                           85 20 00 f8 (10000101 00100000 00000000 11111000) (-134209403)
     1   boolean Boolean.value                             true
     3           (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 3 bytes external = 3 bytes total

java.lang.Short object internals:
 OFFSET  SIZE    TYPE DESCRIPTION                               VALUE     4         (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4         (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4         (object header)                           20 22 00 f8 (00100000 00100010 00000000 11111000) (-134208992)
     2   short Short.value                               1
     2         (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 2 bytes external = 2 bytes total

java.lang.Byte object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4        (object header)                           d9 21 00 f8 (11011001 00100001 00000000 11111000) (-134209063)
     1   byte Byte.value                                1
     3        (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 3 bytes external = 3 bytes total

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE     4          (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     4          (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     4          (object header)                           da 02 00 f8 (11011010 00000010 00000000 11111000) (-134216998)
     4   char[] String.value                              [x, x, x, x]
     4      int String.hash                               0
     4          (loss due to the next object alignment)
Instance size: bytes
Space losses: bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 

Byte,Short,Boolean,Float,Character,Integer= 16字节

String,Double,Long=24字节

基本类型数组(16byte)

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

基本类型的数组会比空对象多一个4字节,用于存储长度, 数组长度每加1那么大小就加一个类型字节

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

容器(List Map Set)(16~48byte)

 public static void main(String[] args) {
 
        List<Integer> a=new ArrayList<>();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

        Set<Integer> a=new HashSet<>();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());

        Map<String,String> a=new HashMap<>();
        System.out.println(ClassLayout.parseInstance(a).toPrintable());
    } 

List集合(24字节): 会比对象多2个intl类型用于计算长度和个数 ,内部属于存储在Object[]

Set集合(16字节): 内部数据存储在HashMap的key中

map集合(48字节): List集合大一倍 每一个项,key(24) ,value(24),因为多一个hash值

各种数组和容器的计算规则

基本类型计算

就是类型对应的字节大小进行相加就行

基本类型数组

数组本身16+(长度*类型字节)=最终占用内存大小

 int[] a=new int[]; 

16+(10*4)=56(字节)

对象数组

16+(长度*4字节引用地址)+(长度*每个对象内部的大小)=最终占用内存大小

 Object[] o=new Object[]; 

16+(10*4)+(10*16)=816(字节)

和对象数组的计算方式一样

List = 24+(长度*4字节引用地址)+(长度*每个对象内部的大小)=最终占用内存大小

 List<Object> list = new ArrayList<>();
        for (int i =; i < 10; i++) {
 
            list.add(new Object());
        } 

24+(10 4)+(10 16)=824(字节)

计算对象

 public static class Dog {
 
        int a; //
        Boolean b; //
        String c; //
        List<String> d; //
        Map<String, String> e;//
    } 

12+4+4+4+4+4=36 (因为内存对齐的原因空对象才会是16)

然后我们给对象赋值

 Dog dog = new Dog();
        dog.a=;  
        dog.b= true;
        dog.c= "xxxxx";
        dog.d= Arrays.asList("xxxxx","xxxxx");
        dog.e= new HashMap(){
 {
 put("a","a");put("b","b");put("c","c");}}; 

因为基本数据类型本身就是他的大小,在上面我们已经加进去了,我们只需要算引用类型对象的大小就行了

36+16+16+(24+2 16)+(48+2 (24+24))=268字节

然后我们将对象放入到容器中

 public static void main(String[] args) {
 
        ArrayList<Object> objects = new ArrayList<>();
        for (int i =; i < 10; i++) {
 
            Dog dog = new Dog();
            dog.a=;
            dog.b= true;
            dog.c= "xxxxx";
            dog.d= Arrays.asList("xxxxx","xxxxx");
            dog.e= new HashMap(){
 {
 put("a","a");put("b","b");put("c","c");}};
            objects.add(dog);
        }
    } 

24+(4*10) + (10*268)=2.744(kb)

假设有100万条数据那么

24+(4*1000000) +(1000000*268)=272000024(字节)=272(mb)

学会计算java的内存会极大地降低内存溢出的风险,以及最大化利用内存来达到最好的效率 ,建议学一下 Java-垃圾收回机制 ,因为只会计算对象大小也不行的,还需要结合JVM虚拟机的GC机制和实际需求进行计算,假设你堆大小2G你在某一时间创建了2G的对象,那么会溢出吗? 可能会,也可能不会, 主要看这些对象在gc的时候能否被收回,那么如何知道这些对象满足了收回的条件,就要研究GC机制了…书山有路勤为径,学海无涯苦作舟