资讯专栏INFORMATION COLUMN

聊聊Java对象在内存中的大小

tianren124 / 3118人阅读

摘要:聊聊对象在内存中的大小本文讨论的对象在内存中的大小指的是在堆中的大小未特殊说明,提到的地方都指的是,版本。而实际是运行方法会看到结果对象实例总大小,空间损失。数组也是对象,但数组的中包含有一个类型的值,又多占了的空间,所以数组的大小是。

聊聊Java对象在内存中的大小

本文讨论的Java对象在内存中的大小指的是在堆(Heap)中的大小;未特殊说明,提到JVM的地方都指的是:Java HotSpot(TM) 64-Bit Server VM,版本:1.8.0_131

Java中Object的组成:

Object = Header + Primitive Fields + Reference Fields + Alignment & Padding`

Header由两部分组成:标记部分(Mark Word)和原始对象引用(Klass Pointer/Object Original Pointer)- mark word & klass pointer。

标记部分的大小是一个word size(64-bit JVM上是8 bytes,32-bit JVM上是4 bytes),包括了该对象的identity hash code和一些标记(比如锁和年代信息)。

原始对象引用在32-bit JVM上的大小是4 bytes,在64-bit JVM上可以是4 bytes,也可以是8 bytes,由JVM参数“是否压缩原始对象”决定,在HotSpot中是UseCompressedOops参数(jdk1.8 和jdk1.9默认是开启的)。

Primitive Fields && Reference Fields

类型 大小
Object Reference word size
byte 1 byte
boolean 1 byte
char 2 bytes
short 2 bytes
int 4 bytes
float 4 bytes
double 8 bytes
long 8 bytes

对齐(Alignment)和补齐(Padding)

对齐,任何对象都是以8 bytes的粒度来对齐的

怎么理解这句话呢?请看一个例子,new Object()产生的对象的大小是多少呢?12 bytes的header,但对齐必须是8的倍数,还有4 bytes的alignment,所以对象的大小是16 bytes.

补齐,补齐的粒度是4 bytes

可以简单理解为,JVM分配内存空间一次最少分配8 bytes,对象中字段对齐的最小粒度为4 bytes

准备工作

本文使用Maven管理Jar包,源码在这里。

pom.xml中引入JOL(Java Object Layout, 使用实例 )依赖,用于展示对象在Heap中的分布(layout):


    org.openjdk.jol
    jol-core
    0.9

第一个测试:

public static void main(String[] args) {
    System.out.println(VM.current().details());
}

执行后,会输出:

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.  // 以 8 bytes的粒度对齐
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]    // 分别对应[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]的大小
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]    // 数组中元素的大小,分别对应的是[Oop(Object Original Pointer), boolean, byte, char, short, int, float, long, double]

对象在Heap中的分布遵循的规则:

重排序, JVM在Heap中给对象布局时,会对field进行重排序,以节省空间。

例-1,对于类:

public class Reorder {

    private byte a;

    private int b;

    private boolean c;
    
    private float d;

    private Object e;
    
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Reorder.class).toPrintable());
    }
}

如果没有重排序,对象的分布会是这个样子的:

objectsize.Reorder object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     1      byte Reorder.a                                 N/A
     13     3           (alignment/padding gap)                  
     16     4       int Reorder.b                                 N/A
     20     1   boolean Reorder.c                                 N/A
     21     3           (alignment/padding gap)                  
     24     4     float Reorder.d                                 N/A
     28     2      char Reorder.e                                 N/A
     30     2           (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 6 bytes internal + 2 bytes external = 8 bytes total

对象实例总大小:32 bytes,空间损失:8 bytes。

而实际是(运行main方法会看到结果):

objectsize.Reorder object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     4       int Reorder.b                                 N/A
     16     4     float Reorder.d                                 N/A
     20     2      char Reorder.e                                 N/A
     22     1      byte Reorder.a                                 N/A
     23     1   boolean Reorder.c                                 N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

对象实例总大小:24 bytes,空间损失:0 bytes。

为了避免空间浪费,一般情况下,field分配的优先依次顺序是:double > long > int > float > char > short > byte > boolean > object reference
注意到了没,这里有个基本的原则是:尽可能先分配占用空间大的类型(除了object reference)。这里的尽可能有两层含义:

在同等优先级情况下,按这个顺序分配。 例-2

public class Order {
    
    private int ignoreMeTentatively;
    
    private byte a;
    
    private boolean b;
    
    private char c;
    
    private short d;
    
    private int e;
    
    private float f;
    
    private double g;
    
    private long h;
    
    private Object i;
    
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(Order.class).toPrintable());
    }
}

这个类的实例在内存中分布是:

objectsize.Reorder object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0    12           (object header)                           N/A
     12     4       int Reorder.b                                 N/A
     16     4     float Reorder.d                                 N/A
     20     2      char Reorder.e                                 N/A
     22     1      byte Reorder.a                                 N/A
     23     1   boolean Reorder.c                                 N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

请先忽略ignoreMeTentatively字段,可以验证类型分配的顺序。

在考虑到补齐(Padding)的情况下,排在后面的类型有可能比排在前面的优先级更高。

回过头来看例-1例-2,会发现header后的字一个field(offset 12)都是int类型的。为什么呢?
这就是AlignmentPadding共同作用的结果。

JVM每次最少分配8 bytes的空间,而header的大小是12。
也就是说,已经分配了16 bytes的空间了,如果严格按照前面说的那个顺序,最先分配一个double类型的field,就需要在这之前先分配4 bytes的空间来补齐,也就这4 bytes的空间就白白浪费了。
这中情况下,<=Padding Size(4 bytes)的类型的优先级就高于大小>Padding Size的类型了。
而在所有大小<=Padding Size的类型中,int的优先级又是最高的,所以header后的第一个field是int

为了进一步理解,再来看个例子,例-3

public class Padding {
      
    private char a;
      
    private boolean b;
      
    private long c;
      
    private Object d;
  
    public static void main(String[] args) {
          System.out.println(ClassLayout.parseClass(Padding.class).toPrintable());
   }
}

这个类的实例在内存中分布是:

objectsize.Padding object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0    12                    (object header)                           N/A
     12     2               char Padding.a                                 N/A
     14     1            boolean Padding.b                                 N/A
     15     1                    (alignment/padding gap)                  
     16     8               long Padding.c                                 N/A
     24     4   java.lang.Object Padding.d                                 N/A
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 1 bytes internal + 4 bytes external = 5 bytes total

可以看到header后的4个bytes空间分配情况,在所有大小<=Padding Size的类型中,char的优先级最高,其次是boolean
这两个加起来只有3 bytes( 接下来,JVM再分配一个8 bytes大小的空间,很明显空间足够的情况下,long的优先级最高,也正好用完这8 bytes的空间。
然后,JVM继续分配一个8 bytes大小的空间,最后一个类型object reference(这里是Object)了,在开启UseCompressedOops的情况下,使用4 bytes的空间,还有4 bytes的空间只能用来对齐了。

子类和父类的field永远不会混合在一起,并且父类的field分配完之后才会给子类的field分配空间。

例-4

public class SuperA {
    
    long a;
    
    private int b;
    
    private float c;
        
    private char d;
        
    private short e;
}

public class SubA extends SuperA {
    
    private long d;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(SubA.class).toPrintable());
    }
    
}

SubA的实例在内存中的分布是:

objectsize.SubA object internals:
   OFFSET  SIZE    TYPE DESCRIPTION                               VALUE
        0    12         (object header)                           N/A
       12     4     int SuperA.b                                  N/A
       16     8    long SuperA.a                                  N/A
       24     4   float SuperA.c                                  N/A
       28     2    char SuperA.d                                  N/A
       30     2   short SuperA.e                                  N/A
       32     8    long SubA.d                                    N/A
Instance size: 40 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

父类SuperA中的field全部分配完后,才分配子类SubAfield

父类的的最后一个字段与子类的第一个字段以一个Padding Size(4 bytes)来对齐。

例-5

public class SuperB {
    
    private byte a;
    
    private int b;

}

public class SubB extends SuperB {
    
    private int a;
    
    private long b;

    public static void main(String[] args) {
        System.out.println(ClassLayout.parseClass(SubB.class).toPrintable());
    }
}

SubB的实例在内存中分布是:

objectsize.SubB object internals:
  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
       0    12        (object header)                           N/A
      12     4    int SuperB.b                                  N/A
      16     1   byte SuperB.a                                  N/A
      17     3        (alignment/padding gap)                  
      20     4    int SubB.a                                    N/A
      24     8   long SubB.b                                    N/A
Instance size: 32 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total

从offset 16的位置开始看,父类还有最后一个字段a未分配,这时JVM分配一个8 bytes的空间,a占用1 byte,
还有7 bytes未使用,而这7 bytes空间没有全部用于对齐,也就是说子类字段的分配并不是从offset 24 开始的。
实际上只用了3 bytes空间来对齐(凑够4 bytes的Padding Size),剩下的4 bytes分配给了子类的a字段。

数组也是对象,但数组的header中包含有一个int类型的length值,又多占了4 bytes的空间,所以数组的header大小是16 bytes。

例-6

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

长度为1的boolean数组的实例在内存的分布是:

[Z 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)                           05 00 00 f8 (00000101 00000000 00000000 11111000) (-134217723)
   12     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
   16     1   boolean [Z.                             N/A
   17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

可以看到,header占用了16 bytes,一个boolean元素占用了1 bytes,剩余7 bytes用于对齐。

参考资料

java-object-memory-structure/

java-object-memory-layout

what-is-the-memory-consumption-of-an-object-in-java)

openjdk-java object layout

java object layout examples

Java Object Size Calculations in 64-bit

mark word & klass pointer

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70761.html

相关文章

  • 聊聊企业级 Java 应用最重要的4个性能指标

    摘要:笔者多次参与银行运营商等大型企业的性能优化工作总结了企业级应用最应重视的个性能指标,主要包括商业事务,外部服务,垃圾回收以及应用布局。应用布局最后要探讨的性能指标是应用布局。另一个需要监测的是容器性能。 虽然很多人都曾预言 Java 将一蹶不振,但是不可否认的是,很多重要项目中,尤其是银行和政府一些大型项目,Java 仍在其中扮演着极其重要的角色。笔者多次参与银行、运营商等大型企业的性...

    sherlock221 评论0 收藏0
  • 聊聊GC

    摘要:复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。针对老年代老年代的特点是区域较大,对像存活率高。这种情况,存在大量存活率高的对像,复制算法明显变得不合适。 GC(Garbage Collection)即Java垃圾回收机制,是Java与C++的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C++程序...

    developerworks 评论0 收藏0
  • 深入理解Java虚拟机(自动内存管理机制)

    摘要:看来还是功力不够,索性拆成了六篇文章,分别从自动内存管理机制类文件结构类加载机制字节码执行引擎程序编译与代码优化高效并发六个方面来做更加细致的介绍。本文先说说虚拟机的自动内存管理机制。在类加载检查通过后,虚拟机将为新生对象分配内存。 欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 书籍真的是常读常新,古人说「书读百遍其义自见」还是蛮有道理的。周志明老师的这本《深入理解 Ja...

    yck 评论0 收藏0
  • 面试官问我JVM调优,我忍不住了!

    面试官:今天要不来聊聊JVM调优相关的吧?面试官:你曾经在生产环境下有过调优JVM的经历吗?候选者:没有面试官:...候选者:嗯...是这样的,我们一般优化系统的思路是这样的候选者:1. 一般来说关系型数据库是先到瓶颈,首先排查是否为数据库的问题候选者:(这个过程中就需要评估自己建的索引是否合理、是否需要引入分布式缓存、是否需要分库分表等等)候选者:2. 然后,我们会考虑是否需要扩容(横向和纵向都...

    不知名网友 评论0 收藏0
  • Java 桌面软件开发到底如何?就本人的经验聊聊

    摘要:桌面软件开发一直以来是程序员不敢轻易涉足的地方,原因有三丑慢难。打包还有一个人们关心的方面就是软件如何打包。这是如今很多软件的做法。但说到底桌面开发本身究竟如何我已经用做了将近两年的开发,我觉得已经可以满足桌面开发的基本需要。 Java FX 桌面软件开发一直以来是 Java 程序员不敢轻易涉足的地方,原因有三:丑、慢、难。而自从 Java 8.0 将 JavaFX 包含进来之后,情况...

    Jeff 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<