sizeof (Java.Object) = ?

 

怎么精确计算Java对象的大小?...



前几天,老王收到一个网友Ape的问题,关于java里new ArrayList()实际消占用了多少内存的问题。刚看到这个问题的时候,老王觉得还挺紧张的,因为之前也没有仔细去算过这个问题。接下来两天,老王赶紧掏出小本本,去研究&计算了一下他究竟占用了多少内存。

这个点虽然不大,但是却蛮有意思。可能好多写java的朋友,也没有深入去想过这个问题。所以,老王今天就跟大家分享分享。(在此先感谢Ape的问题~)。

学过c语言的朋友应该知道,在c语言里,有一个sizeof的关键字,他能度量一个c语言的类型或者变量的大小,比如:sizeof(int)= 4, sizeof(char) = 1等等。但是,在java里,却没有这样一个sizeof的东东,让我们很方便的计算对象占用的内存大小。那我们有没有办法去衡量一个java对象的大小呢?java对象的内存分配的原则又是怎么样的呢?接下来,老王就细细给大家介绍一下。

申明:今天所聊的东西,都是基于sun公司的Java HotSpot(TM)这个虚拟机的(就是我们平常使用最多的那个),其他虚拟机还没有研究过。

一、工具

老王一直比较赞同一个观点:工欲善其事,必先利其器。所以,在做这个事情之前,我们先把工具弄好,看看有没有办法用工具来度量,或者实现类似sizeof这样的功能。去搜索引擎查找,发现java自身是有提供相关接口的,不过呢,就是用起来麻烦些。我们也按照这个方法去弄吧。

1、java.lang.instrument.Instrumentation

先看第一个工具:Instrument。这是一个接口,提供一个getObjectSize的方法,可以实现类似sizeof的功能。不过,需要做三个工作:

a、写一个类,实现premain方法,获取启动时传入的Instrumentation对象;

b、将这个类打包成jar(需要带上MANIFEST.MF文件);

c、在java启动参数中加入javaagent参数,指向刚刚打包的jar:java -javaagent:meminfo.jar

以下就是老王的实现:





2、sun.misc.Unsafe

这个类提供objectFieldOffset这个方法,可以获取类对象里面每个属性在内存位置里的偏移量。但是呢,要获取这个对象比较麻烦(具体可以去看看相关源代码),要用到反射这个黑科技:



获取到这个Unsafe对象以后,我们就可以用他来得到某个对象的属性的内存偏移量:



好了,有了这两个工具以后,我们写一个最简单的程序测试一下:



运行:java -javaagent:jar/meminfo.jar -XX:-UseCompressedOops -cp bin com.simplemain.test_size.Test



好了,我们看到运行的结果:Integer对象占用24Bytes,Parent:24Bytes,Sub:48Bytes。那这些内存是怎么算出来的呢?

二、内存的计算

有了sizeof这个工具,我们还是需要了解一下计算的原理。java里有两种数据类型:基础类型(比如:int、long、short、char等等),以及对象。那接下来,我们就分两种类型来分别讲讲。

1、基础类型

基础类型似乎没有什么可以讲的,因为HotSpot已经规定好了,大体就是这样的。



其实,应该还要加一个类型:引用(类似c语言的指针)。Short s = new Short(); 这个s就是一个引用类型,他指向堆上一个Short类型的内存空间。那一个引用类型占多大空间呢?在32位虚拟机上是4Bytes,而64位是8Bytes。不过如果我们在启动参数中加入:-XX:+UseCompressedOops这个参数,就表明启用对象指针压缩,这个时候,一个引用只占用4Bytes。如果用的是-XX:-UseCompressedOops这个参数,则表明关闭压缩,仍然是8Bytes。据说,从Java SE 6u23之后的64位版本就默认打开了对象指针压缩。

2、对象

好了,现在进入正题:java的对象的大小怎么计算?

在这之前,我们得讲讲java对象的内存模型。我们知道,java的对象分为两种:普通对象&数组对象,他们在内存上有些不一样:



是不是看着很头疼?mark、klass这些都是些啥?padding又是啥?我们就一个个的来看。

head信息(mark + klass):

虚拟机为了要标明一个对象,需要给他做点标记,比如:是哪个类的实例、现在这个对象的锁的状态、gc状态等等额外的信息。其中,对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等就存储在那个mark里面。在32位虚拟机里面,mark占4字节;而64位则是8字节。

那怎么表明自己是那个类的对象呢?就需要有一个指针(或者叫做引用),指向类的定义空间,这个指针就放在klass字段里。那既然是一个引用类型,在32位虚拟机下就是4Bytes,64位则看是否开启压缩:开启状态下是4Bytes,不开启则是8Bytes。

对于普通对象而言,接下来就是要计算每一个字段的大小了。比如:

public static class Parent

{

int pInt1;

}

这个里面pInt1就是一个整型,占用4字节。

所以,理论上,在64位虚拟机上,这个Parent类,应该就是8字节(mark) + 8字节(klass) + 4字节(int pInt1) = 20字节大小,对吗?

但是我们看看之前用工具测试出来的结果:

sizeof(Parent) = 24

offset -> field-name

======================

16 -> pInt1

偏移量是正确的,pInt1在第16个字节开始(之前有mark+klass)。但是总大小不对,我们算出来是20字节,但是工具算出来是24字节。那这4字节差在哪儿呢?

原来,这里有几条规则(我们以64位虚拟机为标准):

规则1:除了对象整体需要按8字节对齐外,每个成员变量都尽量使本身的大小在内存中尽量对齐。比如 int 按 4 位对齐,long 按 8 位对齐。

所以,我们可以解释刚刚的24字节了。因为20字节不能按照8字节对齐,所以必须padding 4字节,即是对齐8字节,达到24字节。

规则2:类属性按照如下优先级进行排列:从长到短排列,引用排最后:  long/double --> int/float -->  short/char --> byte/boolean --> Reference;

据说这个顺序可以使用JVM参数:  -XX:FieldsAllocationSylte=0(默认是1)来改变。

所以,我们可以看到我们定义的

public static class Sub extends Parent

{

int sInt1;

String sString2;

long sLong3;

}

顺序是sInt1 -> sString2 -> sLong3,而工具最后打印出来的内存顺序是:

sizeof(Sub) = 48

offset -> field-name

======================

24 -> sLong3

32 -> sInt1

40 -> sString2

规则3:优先按照规则一和二处理父类中的成员,接着才是子类的成员;

也就是说,优先排列父类的成员,再排列子类的成员。

这就是为什么Sub类的成员是从24字节偏移量开始,而不是16字节开始的原因。

规则4:当父类中最后一个成员和子类第一个成员的间隔如果不够4个字节的话,就必须扩展到4个字节的基本单位;

规则5:如果子类第一个成员是一个双精度或者长整型,并且父类并没有用完8个字节,JVM会破坏规则2,按照整形(int),短整型(short),字节型(byte),引用类型(reference)的顺序,向未填满的空间填充。

是不是已经晕了?那就不用看了,反正知道有这样一个规则就好了。

而对于数组对象,就是按照我们之前的方法,mark + klass + length + type_len * count + padding 计算就行。

好了,有了上面这些知识理论,那我们来回答Ape提出的问题:newArrayList()占多大空间?

a、我们去看ArrayList的定义,可以发现:class ArrayList extends AbstractList

他是从AbstractList继承下来的,而这个父类,里面有一个成员:protected transient int modCount = 0; 再往上,就没有其他成员变量了。

b、而ArrayList自身有两个成员变量:transient Object[] elementData 和 private int size

好了,我们开始计算。这里用的64位虚拟机 + 不压缩指针大小(-XX:-UseCompressedOops):



我们再看看工具跑出来的结果:

sizeof(ArrayList) = 40

offset -> field-name

======================

24 -> size

32 -> elementData

跟我们的推算是一样的。

怎么样,今天的内容get到了嘛?


    关注 SimpleMain


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册