blog icon indicating copy to clipboard operation
blog copied to clipboard

如何计算Java对象所占内存的大小

Open TFdream opened this issue 4 years ago • 0 comments

转载自 【阿里云云栖号】如何计算Java对象所占内存的大小:https://segmentfault.com/a/1190000015009289

本文以如何计算Java对象占用内存大小为切入点,在讨论计算Java对象占用堆内存大小的方法的基础上,详细讨论了Java对象头格式并结合JDK源码对对象头中的协议字段做了介绍,涉及内存模型、锁原理、分代GC、OOP-Klass模型等内容。最后推荐JDK自带的Hotspot Debug工具——HSDB,来查看对象在内存中的具体存在形式,以论证文中所述内容。

背景

目前我们系统的业务代码中大量使用了LocalCache的方式做本地缓存,而且cache的maxSize通常设的比较大,比如10000。我们的业务系统中就使用了size为10000的15个本地缓存,所以最坏情况下将可缓存15万个对象。这会消耗掉不菲的本地堆内存,而至于实际上到底应该设多大容量的缓存、运行时这大量的本地缓存会给堆内存带来多少压力,实际占用多少内存大小,会不会有较高的缓存穿透风险,目前并不方便知悉。考虑到对缓存实际占用内存的大小能有个更直观和量化的参考,需要对运行时指定对象的内存占用进行评估和计算。

要计算Java对象占用内存的大小,首先需要了解Java对象在内存中的实际存储方式和存储格式。

另一方面,大家都了解Java对象的存储总得来说会占用JVM内存的堆内存、栈内存及方法区,但由于栈内存中存放的数据可以看做是运行时的临时数据,主要表现为本地变量、操作数、对象引用地址等。这些数据会在方法执行结束后立即回收掉,不会驻留。对存储空间空间的占用也只是执行函数指令时所必须的空间。通常不会造成内存的瓶颈。而方法区中存储的则是对象所对应的类信息、函数表、构造函数、静态常量等,这些信息在类加载时(按需)只会在方法区中存储一份,不会产生额外的存储空间。因此本文所要讨论的主要目标是Java对象对堆内存的占用。

内存占用计算方法

如果读者关心对象在JVM中的存储原理,可阅读本文后边几个小节中关于对象存储原理的介绍。如果不关心对象存储原理,而只想直接计算内存占用的话,其实并不难,笔者这里总结了三种方法以供参考:

  • 使用java.lang.instrument.Instrumentation.getObjectSize()方法,可以很方便的计算任何一个运行时对象的大小,返回该对象本身及其间接引用的对象在内存中的大小。
  • 使用sun.misc.Unsafe类,有一个objectFieldOffset(Field f)方法,表示获取指定字段在所在实例中的起始地址偏移量,如此可以计算出指定的对象中每个字段的偏移量,值为最大的那个就是最后一个字段的首地址,加上该字段的实际大小,就能知道该对象整体的大小。
  • 通过OpenJDK官方提供的JOL(Java Object Layout)工具,用来分析JVM中对象布局的工具,它可以帮我们在运行时计算某个对象的大小。
  • 使用第三方工具:这里要介绍的是lucene提供的专门用于计算堆内存占用大小的工具类:RamUsageEstimator

Java 对象内存布局

HotSpot JVM 使用名为 oops (Ordinary Object Pointers) 的数据结构来表示对象。这些 oops 等同于本地 C 指针。 instanceOops 是一种特殊的 oop,表示 Java 中的对象实例。

image

在 Hotspot VM 中,对象在内存中的存储布局分为 3 块区域:

  • 对象头(Instance Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

对象头又包括三部分:MarkWord、元数据指针、数组长度。

  • MarkWord:用于存储对象运行时的数据,好比 HashCode、锁状态标志、GC分代年龄等。这部分在 64 位操作系统下占 8 字节,32 位操作系统下占 4 字节。
  • 指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。 这部分就涉及到指针压缩的概念,在开启指针压缩的状况下占 4 字节,未开启状况下占 8 字节。
  • 数组长度:这部分只有是数组对象才有,若是是非数组对象就没这部分。这部分占 4 字节。

实例数据就不用说了,用于存储对象中的各类类型的字段信息(包括从父类继承来的)。

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

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

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

参考资料

TFdream avatar Oct 19 '21 02:10 TFdream