本文使用的是32位的JVM ,jdk1.6。本文基本是翻译的,加上了一些自己的理解,原文见文章底下链接。
在本文中,我们讨论如何计算或者估计一个JAVA对象占多少内存空间。(注意,使用 Classmexer agent 或者VM insturmentation 可以查询到一个java对象占用了多少内存。)
一般来说,我们讨论一个在堆中的对象的内存,前提是在“正常状态”下。我们忽略下面两种情况。
- 在某些情况下,JVM 不一定会把对象放到堆中。例如,一个简单的线程本地对象可以存放在栈中。
- 一个对象占用的内存还依赖于它的当前状态:例如,这个对象的同步锁是否是处于竞争状态,或者 这个对象是否正在被GC回收。
计算消耗内存的一般方法
一般来说,在Hotspot中,一个JAVA对象的内存消耗包括以下四个方面:
- 对象头,包括一些对象的状态。一个实例对象在堆中的内存消耗不仅仅花在它的属性上,这个对象还需要一些额外的信息,例如,保存一个对象的类引用和判断这个对象是否可以到达的,当前同步锁的情况的状态标记符等。
在HotSpot中:(其他的JVM的情况也跟以下描述差不多。)
- 一个普通的对象需要8个字节的额外内存空间
- 一个数组需要12个字节(普通对象头的8个字节+4个)
- 基本类型的内存消耗,如int,long,float等
- 引用属性的内存消耗,每个引用消耗4个字节。
- 填充,一个对象可能有一小部分的消耗浪费在填充上面。在HotSpot中,给对象分配内存最小单位是8个字节所以每个对象的占的字节数都是可以被8整除的。如果一个对象所占的字节数不是8的倍数,那么向上取最接近的可以被8整除的数字。
例如:
一个空的实例对象占8个字节。
一个包含一个 boolean 属性的对象占用16个字节内存:对象头占了8个,boolean 属性占了1个,8+1=9字节,因为9不是8的倍数,向上取最小可以被8整除的数字16。
一个包含了8个 boolean 属性的对象也是占用16个字节内存:对象头8个,boolean 属性占了1*8个,一共16个,16可以被8整除,不需要填充字节。
一个包含了两个long属性,3个int属性和一个boolean的对象占用了40个字节内存:
- 对象头占用了8个字节
- 2个long占用了16字节
- 3个int占用了12个字节
- 1个boolean占用了1个字节
- 填充字节占用了3个字节。前面一共占用了37个字节,37不能被8整除,取40字节。
怎么去计算一个JAVA数组使用的内存
前面写到了怎么计算一个JAVA对象的内存消耗,下面我们讨论下特殊情况数组。我们知道:
- 在JAVA中,数组是一种特殊类型的对象
- 一个多维数组是一个简单数组的数组
例如,一个二维数组的每一行都是一个独立的数组对象
单个数组的内存使用情况
一个一维数组是一个对象,数组也有对象头,可是,这个对象头占用了12个字节,额外的4个字节用来存储数组的长度。具体每个元素需要多少字节,取决于元素的类型。每一个元素需要4个字节来存储它的引用。并且如果总的字节数不是8个倍数,同样需要填充字节。
二维数组的占用内存情况
在c语言中,二维数组(事实上是任何多维数组)本质上是一维数组通过指针操作来实现的。但是JAVA中并不是这样,JAVA中多维数组事实上是一系列的内嵌数组。这说明二维数组的每一行都有和对象一样的内存开销,因为他本质上就是一个独立的对象。
例如,有一个10×10的int数组,首先,“外部”数组有12个字节的对象头开销。然后有十个元素,每个元素存储了一个指向10个元素数组的引用。以上的开销是12+4*10=52字节。然后每一行中都有一个数组对象,一个数组对象的对象头开销为12字节,并且有10个int.开销10*4 = 40 字节,另外还有4个填充字节,所以每一行的数组对象占用56个字节。所以一共有56*10+52 = 612个字节,加上4个填充字节一共616个字节。所以,得出的字节总数会比如果只是考虑10*10*4=400 字节要多。 多维数组和二维数组同理可得。
JAVA String和 String 相关的对象的内存消耗
我们的应用基本都会用到字符串,如果你用到C语言,并且使用ASCII编码,字符串由数字,字母或者某些特殊符号组成。那么每个字符串的大小就是字符数目*1字节。但是JAVA不是这样的:
首先,每个对象都包含对象头,所以一个对象最低占用8个字节。
- 一个JAVA String 包含不止一个对象
- 一个JAVA char占用两个字节。
- 一个JAVA对象包含一些额外的变量。
怎么计算字符串内存的使用情况
首先一个占用最少字节的String,可以用以下公式计算。
8 * (int) ((((no chars) * 2) + 45) / 8)
或者,用这个方法计算:
- 把字符串的字符个数*2个字节
- 增加38
- 如果结果不是8的倍数,取比结果大并且最接近的可以被8整除的数。
这个结果就是String在堆中最小的占用内存字节数。
更进一步
一般来说,上面的公式可以用于“新建”的String ,可是,还有另外的情况如下:
- 如果一个String是另外一个String的子字符串,那么这个String会比上面说到的最小值要大。
- 一个子字符串可以共用同一个字符数组,所以总体来说,一个父字符串加上几个子字符的消耗要比用上面公式计算的总和要小。
理解String 是怎么占用内存的
来看一个每个String对象的各个属性,一个String包括如下的属性:
- 一个char数组(是个独立的对象用来存储字符串中的字符)
- 一个int 的offset属性(偏移量,用来指出字符串是从char数组中第几个字符开始的)
- 一个int 的count属性(字符串的长度)
- 最后一个int的hash属性(用来存储hashCode的值)
也就是说,即使一个String不包含任何字符,也需要在数组的引用上面消耗4个字节,再加上3个int类型的属性,3*4=12字节,加上对象头的8个字节,以上一共24个字节(目前还不需要加上填充字节)。然后,一个char数组的对象头需要12个字节,加上4个填充字节,一个空的String 消耗了40字节。
如果String 包含了17个字符,那么String 对象本身需要24个字节,但是现在17个字符的char数组要需12字节 加上 17*2=34字节,12+17*2=46字节,46不是8的倍数,加上填充字节46+2=48字节,那么17个字符的字符串会用到48+24 = 72 字节,可以看到在C语言中占据18个字节的String 在JAVA中占据了72个字节。
子字符串的内存消耗
你可能会感到奇怪为什么字符串会有一个offset的属性和count属性。为什么char数组中的字符直接对应整个字符串呢?是这样的,当你创建一个子字符串,新创建的子字符串的char数组其实是指向的父字符串的char数组的,也就是父子字符串共享一个char数组。(但是他们有不同的offset和count)。这是一件好事还是坏事依赖于你怎么用:
- 如果在创建完子字符串之后还需要用到父字符串的话,你可以省了些内存。
- 如果创建完子字符串之后就不再需要用到父字符串的话,那么就浪费了内存
例如以下的代码
String str = "Some longish string..."; str.substring(5, 4);
在这代码中str的char 数组不止是包括了4个字符,它包括了完整的字符串“Some longish string…”,但是str的offset,count属性改变了。如果这些内存的浪费在应用中是不可以接受的,那么我可以可以重新创建一个新的String :
String str = "Some longish string..."; str = new String(str.substring(5,4));
用这种方式创建一个新的String,char数组就不会再直接指向"Some longish string…",而是会创建一个适合的4个字符的char 数组对象。
资料:
http://www.javamex.com/tutorials/memory/object_memory_usage.shtml 介绍JAVA对象的内存占用http://www.javamex.com/tutorials/memory/array_memory_usage.shtml 介绍JAVA数组的内存占用http://www.javamex.com/tutorials/memory/string_memory_usage.shtml 介绍JAVA字符串的内存占用