解析一个Java对象占用多少内存空间

说明: alignment, 对齐, 比如8字节的数据类型long, 在内存中的起始地址必须是8的整数倍。

padding, 补齐; 在对象所占据空间的末尾,如果有空白, 需要使用padding来补齐, 因为下一个对象的起始位置必须是4/8字节(32bit/64bit)的整数倍(这又是一种对齐)。

问题描述

一个对象具有100个属性, 与100个对象每个具有1个属性, 哪个占用的内存空间更大?

一个对象会分配多少内存?

每增加一个属性,对象占用的空间会增加多少?

答案1

参考 Mindprod , 可以发现事情并不简单:

JVM具体实现可以用任意形式来存储内部数据, 可以是大端字节序或者小端字节序(big or little endian), 还可以增加任意数量的补齐、或者开销, 尽管原生数据类型(primitives)的行为必须符合规范。
例如, JVM或者本地编译器可以决定是否将 boolean[] 存储为64bit的内存块中, 类似于 BitSet。 厂商可以不告诉你这些细节, 只要程序运行结果一致即可。

  • JVM可以在栈(stack)空间分配一些临时对象。
  • 编译器可能用常量来替换某些变量或方法调用。
  • JVM可能对方法和循环生成多个编译版本; 例如, 编译两种版本的方法, 针对某些情况调用其中的一个。

当然, 硬件平台和操作系统还会有多级缓存, 例如CPU内置的L1/L2/L3; SRAM缓存, DRAM缓存, 普通内存, 以及磁盘上的虚拟内存。 用户数据可能在多个层级的缓存中出现. 这么多复杂的情况、决定了我们只能对内存占用情况进行大致的估测。

测量方法

可以使用 Instrumentation.getObjectSize() 方法来估算一个对象占用的内存空间。

想要查看对象的实际布局(layout)、占用(footprint)、以及引用(reference), 可以使用OpenJDK提供的 JOL工具(Java Object Layout)

对象头和对象引用

在64位JVM中, 对象头占据的空间是 12-byte(=96bit=64+32), 但是以8字节对齐, 所以一个空类的实例至少占用16字节。

在32位JVM中, 对象头占8个字节, 以4的倍数对齐(32=4*8)。(请参考 Dmitry Spikhalskiy,Jayen的回答,以及JavaWorld网站)。

通常, 在32位JVM, 以及内存小于 -Xmx32G 的64位JVM上, 一个引用占的内存是4个字节。(指针压缩)

因此, 64位JVM一般需要多消耗 30%-50% 堆内存。(参考: Should I use a 32- or a 64-bit JVM?, 2012, JDK 1.7)

包装类型、数组和字符串

包装类型比原生数据类型消耗的内存要多, 参考 JavaWorld :

  • Integer: 占用16字节(8+4=12+补齐), 因为 int 部分占4个字节。 所以使用 Integer 比原生类型 int 要多消耗 300% 的内存。
  • Long: 一般占用16个字节(8+8=16): 当然, 对象的实际大小由底层平台的内存对齐确定, 具体由特定CPU平台的JVM实现决定。 看起来一个Long 类型的对象, 比起原生类型long多占用了8个字节。 相比之下, Integer有4字节的补齐, 很可能是因为JVM强制进行了8字节的边界对齐。

其他容器占用的空间也不小:

  • 多维数组: 这是另一个惊喜。
    在进行数值或科学计算时, 开发人员经常会使用 int[dim1][dim2] 这种构造方式。
    在二维数组 int[dim1][dim2] 中, 每个嵌套的数组 int[dim2] 都是一个单独的 Object, 会额外占用16字节的空间。某些情况下,这种开销是一种浪费。当数组维度更大时,这种开销特别明显。
    例如, int[128][2] 实例占用3600字节。 而 int[256] 实例则只占用1040字节。里面的有效存储空间是一样的, 3600比起1040多了246%的额外开销。在极端情况下, byte[256][1], 额外开销的比例是19倍! 而在 C/C++ 中, 同样的语法却不增加额外的存储开销。

  • String: String 对象的空间随着内部字符数组的增长而增长。当然, String 类的对象有24个字节的额外开销。

    对于10字符以内的非空 String, 增加的开销比起有效载荷(每个字符2字节 + 4个字节的length), 多占用了100%到400%的内存。

对齐(Alignment)

看下面的 示例对象:

class X {                      // 8 字节-指向class定义的引用
   int a;                      // 4 字节
   byte b;                     // 1 字节
   Integer c = new Integer();  // 4 字节的引用
}

新手可能会认为, 一个X类的实例占用17字节的空间。 但由于需要对齐,也可称为补齐(padding), JVM分配的内存是8字节的整数倍, 所以占用的空间不是17字节,而是24字节。

当然,运行JOL的示例之后,会发现JVM会依次先排列 parent-class 的fields, 然后到本class的字段时,也是先排列8字节的,排完了8字节的再排4字节的field,以此类推。当然,还会加塞子_ 尽量不浪费空间。

Java内置的序列化,也会基于这个布局,带来的坑就是加字段后就不兼容了。 只加方法不固定 serialVersionUID 也出问题。 所以有点经验的都不喜欢用内置序列化,例如自定义类型存到redis时。

JOL使用示例

JOL (Java Object Layout) 是分析JVM中内存布局的小工具, 通过 Unsafe, JVMTI, 以及 Serviceability Agent (SA) 来解码实际的对象布局,占用,引用。 所以 JOL 比起基于 heap dump, 或者基于规范的其他工具来得准确。

JOL的官网地址为: http://openjdk.java.net/projects/code-tools/jol/

从中可以看到:

参考:

本文最新翻译地址: https://github.com/cncounter/translation/blob/master/tiemao_2018/37_java_object-memory_consumption/37_java_object-memory_consumption.md

翻译日期: 2019年7月4日

翻译人员: 铁锚 https://renfufei.blog.csdn.net/

发布了110 篇原创文章 · 获赞 1637 · 访问量 526万+

猜你喜欢

转载自blog.csdn.net/renfufei/article/details/95758333