锁定老帖子 主题:深入Java对象大小
该帖已经被评为新手帖
|
|
---|---|
作者 | 正文 |
发表时间:2010-07-12
最后修改:2010-08-12
在大规模Java 应用开发中,总会遇到内存泄漏的问题。通常的做法,通过 Profile 工具,分析 Java Heap ,一般能够发现哪些对象内存占用巨大,而引起的泄漏问题。为了更好地深入了解问题的本质,以及从另外一个角度来分析问题,特写这篇文章。
可能不少的读者,并不清楚Java 对象到底占居多少的空间(单位:字节 =8 比特)。文章中会使用 JDK 6 update 7 自带的 Profile 工具 -Java VisualVM 。引入 Profile 工具的目的正是为了分析对象的大小。
首先,要区别Java 对象和 Java 类元信息,其中, JVM 把所有的 Java 对象放到 Java Heap 中,而类的元信息是放在方法区的,通俗地说,在 Java 源代码中,定义的字段和方法等变量标示(比如字段名称和类型等)。请注意,元信息所引用的对象还是在 Java Heap 里面。那么,本章主要 针对 的是Java Heap 。
早几天,看到了JavaEye 上面提问 -http://www.iteye.com/problems/45423 。问题的本质,和主题一样,不过它想要通过 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 [i] = 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 [i] = 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 ,如图所示:
(图3)
图 3中 过滤出三个结果,暂时不看数组对象,选择java.lang.Object ,然后点击 instances 按钮,或者双击 java.lang.Object 。
( 图 4) 结果发现,Object 对象的大小总是 8 个字节。再看看 Integer 、 Long 和 Byte 。 Sizeof 类的计算结果不可信,分析错误原因放一下,后面会提到。先来分析 Integer 、 Long 和 Byte 。从源代码的角度来分析, Integer 包含了一个 int 的 value, 其他的也有对应类型。而知晓, int 占用 4 个字节, long 则是 8 个字节, byte 占用 1 个字节。看图说话:
(图5)
(图6) (图7)
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,结果图:
(图8)
图8 中表明,证明了“空壳”对象空间大小 8 字节,和 java.lang.Object 类似。目前还不能证明 Integer 的情况,因为 Integer 类的层次是 Integer <- Number <- Object 。虽然 Number 没有对象状态,可是也不能证明出去 value 的 Integer 对象空间大小等于 Object 的。为了证明这一点,再扩展一下 SameAsObject, 使其成为父类,创建一个子类 ExtSameAsObject 。同样的方法,获取大小:
(图9)
图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: (图10)
余下内容,看附件。错误内容已经修正,请下载Fix1文档 。
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-07-13
看来这帖子过于深入了,大家都不来水拉,支持下,写得得不错
|
|
返回顶楼 | |
发表时间:2010-07-13
写的不错,我们项目中内存泄露都没有这样精密的计算过,研究对象的大小还没深入到这块,看来要想LZ多多学习了
|
|
返回顶楼 | |
发表时间:2010-07-13
最后修改:2010-07-13
zhao103804 写道 写的不错,我们项目中内存泄露都没有这样精密的计算过,研究对象的大小还没深入到这块,看来要想LZ多多学习了
没有办法,在项目优化和代码重构时,花费了大量的精力来培训和底层工作。 |
|
返回顶楼 | |
发表时间:2010-07-13
恩 挺有意思
之前在dzone上看过一篇相关的文章 http://www.dzone.com/links/the_memory_structure_of_java_objects.html |
|
返回顶楼 | |
发表时间:2010-07-13
zhao103804 写道 写的不错,我们项目中内存泄露都没有这样精密的计算过,研究对象的大小还没深入到这块,看来要想LZ多多学习了
看来使用的时候要多注意了。 |
|
返回顶楼 | |
发表时间:2010-07-13
最后修改:2010-07-13
长度为1的int[]数组空间大小也是20字节,那么它的元素大小自然也是4字节。这个证明更进一步说明了数组的本质: |
|
返回顶楼 | |
发表时间:2010-07-13
最后修改:2010-07-13
yunzhiyifeng 写道
长度为1的int[]数组空间大小也是20字节,那么它的元素大小自然也是4字节。这个证明更进一步说明了数组的本质:
谢谢你的勘误,我说的时候忘记了原生类型。修改文档已经修正。 |
|
返回顶楼 | |
发表时间:2010-07-13
感谢LZ,最近正好要用这方面的东西
|
|
返回顶楼 | |
发表时间:2010-07-13
swordice 写道 感谢LZ,最近正好要用这方面的东西
欢迎指正哦。 |
|
返回顶楼 | |