转载于:http://www.developersky.net/thread-97-1-1.html
在我的编程工作中,经常需要大致的计算我们的程序、数组、集合到底需要占用多少内存,以便决定我们的设计方案,尤其是在企业级编程中,由于涉及的数据量巨大,这样的决定更加频繁。但是我们经常要面临的问题是:一个Java对象到底占据多数空间?可能不少的读者,并不清楚Java对象到底占居多少的空间(单位:字节=8比特)。本文会使用JDK 6 update 7自带的Profile工具-Java VisualVM来分析对象的大小。
首先,我们必须要区分Java对象和Java类元信息,其中,JVM把所有的Java对象放到Java Heap中,而类的元信息是放在方法区的,通俗地说,在Java源代码中,定义的字段和方法等变量标示(比如字段名称和类型等)。请注意,元信息所引用的对象还是在Java Heap里面。那么,本章主要针对的是Java Heap。
其实对于Java对象的大小,最开始的想法是使用程序来计算,以前外国一个哥们(http://www.javaworld.com/javaworld/javatips/jw-javatip130.html)也写个一个程序计算对象大小,它的计算如下:
public class Sizeof
{
public static void main (String [] args) throws Exception
{
// Warm up all classes/methods we will use
runGC ();
usedMemory ();
// Array to keep strong references to allocated objects
final int count = 100000;
Object [] objects = new Object [count];
long heap1 = 0;
// Allocate count+1 objects, discard the first one
for (int i = -1; i < count; ++ i)
{
Object object = null;
// Instantiate your data here and assign it to object
object = new Object ();
if (i >= 0)
objects = object;
else
{
object = null; // Discard the warm up object
runGC ();
heap1 = usedMemory (); // Take a before heap snapshot
}
}
runGC ();
long heap2 = usedMemory (); // Take an after heap snapshot:
final int size = Math.round (((float)(heap2 - heap1))/count);
System.out.println ("'before' heap: " + heap1 +
", 'after' heap: " + heap2);
System.out.println ("heap delta: " + (heap2 - heap1) +
", {" + objects [0].getClass () + "} size = " + size + " bytes");
for (int i = 0; i < count; ++ i) objects = null;
objects = null;
}
private static void runGC () throws Exception
{
// It helps to call Runtime.gc()
// using several method calls:
for (int r = 0; r < 4; ++ r) _runGC ();
}
private static void _runGC () throws Exception
{
long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE;
for (int i = 0; (usedMem1 < usedMem2) && (i < 500); ++ i)
{
s_runtime.runFinalization ();
s_runtime.gc ();
Thread.currentThread ().yield ();
usedMem2 = usedMem1;
usedMem1 = usedMemory ();
}
}
private static long usedMemory ()
{
return s_runtime.totalMemory () - s_runtime.freeMemory ();
}
private static final Runtime s_runtime = Runtime.getRuntime ();
} // End of class
(代码1)
通过改变红色区域,来切换测试对象。先运行出结果,以下结果是在Windows XP x86 ,SUN JDK 1.6.0 update 7,并且Console信息部分被截断:
{class java.lang.Object} size = 8 bytes
{class java.lang.Integer} size = 16 bytes
{class java.lang.Long} size = 16 bytes
{class java.lang.Byte} size = 16 bytes
{class [Ljava.lang.Object;} size = 16 bytes // 长度为0的Object类型数组
{class [Ljava.lang.Object;} size = 16 bytes // 长度为1的Object类型数组
{class [Ljava.lang.Object;} size = 24 bytes // 长度为2的Object类型数组
现在,这个结论有问题,因为,从Byte、Long、Integer源代码的角度,不可能对象空间大小相同,那么接下来需要借助于Java VisualVM来做了。在测试之前,需要一段辅助程序,帮助执行。实现如下:
public class MainClass {
/**
* 启动方法
* @param args
*/
public static void main(String[] args) throws Exception {
Object object = new Object();
neverStop();
neverGC(object);
}
private static void neverGC(Object object) {
System.out.println(object);
}
private static void neverStop() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
}
(代码2)
这个程序保证对象不会被GC掉,并且不会停止。
首先,在 Java VisualVM上面,选择正确的程序进行监控,然后做一个Heap Dump。
Dump之后,点击Classes,然后过滤出java.lang.Object,如图所示:
![c5702cae-82ff-3682-9fc3-86a12e9b83a7.png c5702cae-82ff-3682-9fc3-86a12e9b83a7.png](http://www.developersky.net/data/attachment/forum/month_1007/1007122127701d14de19e600e3.png)
![d886bf53-6bcc-375d-bb76-fee36b6cb70d.png d886bf53-6bcc-375d-bb76-fee36b6cb70d.png](http://www.developersky.net/data/attachment/forum/month_1007/10071221278191a5ffb98a4dbb.png)
图3中过滤出三个结果,暂时不看数组对象,选择java.lang.Object,然后点击instances按钮,或者双击java.lang.Object。
![ec470db1-4cb9-3657-ba20-8db3bd1e1269.png ec470db1-4cb9-3657-ba20-8db3bd1e1269.png](http://www.developersky.net/data/attachment/forum/month_1007/10071221269b0a9961f6aeb9f9.png)
结果发现,Object对象的大小总是8个字节。再看看Integer、Long和Byte。Sizeof类的计算结果不可信,分析错误原因放一下,后面会提到。先来分析Integer、Long和Byte。从源代码的角度来分析,Integer包含了一个int的value,其他的也有对应类型。而知晓,int占用4个字节,long则是8个字节,byte占用1个字节。看图说话:
![bfcbf3a5-b898-3dc9-99ac-d76c41f755b8.png bfcbf3a5-b898-3dc9-99ac-d76c41f755b8.png](http://www.developersky.net/data/attachment/forum/month_1007/10071221272365f07c1f144611.png)
![78c4cd80-819c-3ca8-8d3f-bc9f7a3bfd81.png 78c4cd80-819c-3ca8-8d3f-bc9f7a3bfd81.png](http://www.developersky.net/data/attachment/forum/month_1007/10071221260f2088b948b73de8.png)
![af4ef1e8-8686-362a-9bd9-9e4add13e0ae.png af4ef1e8-8686-362a-9bd9-9e4add13e0ae.png](http://www.developersky.net/data/attachment/forum/month_1007/10071221272066b9d22b41a238.png)
Integer对象空间大小是12字节(见图5),Long对象空间则是16个字节(见图6),Byte空间则是9个字节(见图7)。那么除去对象自身的状态value,得出的结论是,最后“空壳”对象都8字节?不能确定,因为还有类(静态)字段没有考虑,这些字段是否属于对象实例的一部分呢?很多书上面再三强调类的字段不属于对象实例,对于这种说法需要保持怀疑的态度?下面做一个“空壳”对象实验,代码如下:
/**
* 和Object一样,没有任何添加对象状态。
* @author mercy
*/
public class SameAsObject extends Object {
}
(代码3)
修改MainClass程序,new 一个SameAsObject对象,重新Heap Dump,结果图:
![bc86fd4b-e2f4-36da-a382-79da3afc121c.png bc86fd4b-e2f4-36da-a382-79da3afc121c.png](http://www.developersky.net/data/attachment/forum/month_1007/1007122127b9501feef58736c5.png)
图8中表明,证明了“空壳”对象空间大小8字节,和java.lang.Object类似。目前还不能证明Integer的情况,因为Integer类的层次是Integer <- Number <- Object。虽然Number没有对象状态,可是也不能证明出去value的Integer对象空间大小等于Object的。为了证明这一点,再扩展一下SameAsObject,使其成为父类,创建一个子类ExtSameAsObject。同样的方法,获取大小:
![a003d43c-d4ad-3458-9efb-511ce848f29e.png a003d43c-d4ad-3458-9efb-511ce848f29e.png](http://www.developersky.net/data/attachment/forum/month_1007/1007122126dcef28be8c5f40a7.png)
图9中,ExtSameAsObject的空间大小还是8字。证明了空壳对象大小等同于java.lang.Object的。那么自然地可以推导出java.lang.Double也是16字节(8字节空壳对象+8字节的double类型value)。
细心的读者会发现,图8和9中,笔者标记了红色区域,都有Java frame的标识。而在MainClass的main方法中,有一句:Object object = new ExtSameAsObject();这个对象是局部变量。说明什么问题呢?两个问题:第一,正因为在方法内部执行,这个语句就是一个frame,而frame是Java Stack的组成单位。这个frame被压入栈,并且类型是ExtSameAsObject,同时带有一个对象的地址#1(当然这里不是实际地址,只是一个引用标识)。第二,即使是局部对象,对象仍然分配在Java Heap中。
既然“空壳”的Integer对象,占用8个字节的大小,那么类的成员就不应该归入对象实例之中。从实验中,我们可以得出结论,某个类的任何类成员的常量和变量,都不会分配(或计算)到该类的对象实例。
回到Sizeof类,为什么Sizeof会计算出问题?虽然Sizeof类计算有误,不过它的思想还是几点值得借鉴:
第一、作者深入了了解了Runtime#gc()方法和Runtime#runFinalization方法的语义。这两个方法并不是实时执行,而是建议JVM执行,执行与否程序怎么知道呢?在—_runGC方法中,作者试图通过内存的变化来判断是否GC(GC后,肯定会变化的),确实有道理。
第二、通过N次,求得平均数,比单次测试要精确很多。
第三、没有开辟其他对象,不影响结果。
同时,不过作者忽略了如下情况:
第一、GC不只是针对需要测试的对象,而是整个JVM。因此在GC的时候,有可能是JVM启动后,把其他没有使用的对象给GC了,这样造成了使用空间的变大,而Heap空间变小。
第二、Runtime#freeMemory()方法,返回的数据是近视值,不一定能够保证正确性。因此在后面的累计、求平均数来带了误差。
第三、使用了Math#round()方法,又带来了误差。
由于Sizeof的不精确,不能作为测试基准。
那么,更多的疑问产生了。前面测试的都是单一对象,那么数组对象是如何分配的?
修改程序如下:
public static void main(String[] args) throws Exception {
//Object object = new ExtSameAsObject();
Object[] object = new Object[0];
...
(代码4)
重新Heap Dump:
![87b42143-e06d-3d16-bf25-06bfda5e61a6.png 87b42143-e06d-3d16-bf25-06bfda5e61a6.png](http://www.developersky.net/data/attachment/forum/month_1007/1007122126167a3be94b5dad01.png)
其他的内容请参看附件