如何使用sun.misc.Unsafe及反射对内存进行内省(introspection)2015-02-01对于一个有经验的JAVA程序员来说,了解一个或者其它的JAVA对象占用了多少内存,这将会非常有用。你可能已经听说过我们所生活的世界,存储容量将不再是一个问题,这个对于你的文本编辑器来说可能是对的(不过,打开一个包含大量的图片以及图表的文档,看看你的编辑器会消耗多少内存),对于一个专用服务器软件来说也可能是对的(至少在你的企业成长到足够大或者是在同一台服务器运行其它的软件之前),对于基于云的软件来说也可能是对的,如果你足够的富有可以花足够的钱可以买顶级的服务器硬件。然而,现实是你的软件如果是受到了内存限制,需要做的是花钱优化它而不是尝试获取更好的硬件(原文:in the real world your software will once reach a point where it makes sense to spend money in its optimization rather than trying to obtain an even better hardware)(目前你可以获取到的最好的商业服务器是64G内存),此时你不得不分析你的应用程序来找出是哪个数据结构消耗了大部份的内存。对于这种分析任务,最好的工具就是一个好的性能分析工具,但是你可以在刚开始的时候,使用分析你代码中的对象这种用廉价的方式。这篇文章描述了使用基于 Oracle JDK的ClassIntrospector类,来分析你的应用程序内存消耗。我曾经在文章字符串包装第1部分:将字符转换为字节中提到了JAVA对象内存结构,例如我曾经写过,在JAVA1.7.0_06以前,一个具有28个字符的字符串会占用104个字节,事实上,我在写这篇文章的时候,通过自己的性能分析器证实了我的计算结果。现在我们使用Oracle JDK中特殊类sun.misc.Unsafe,通过纯JAVA来实现一个JAVA对象内省器(introspector)。我们使用sun.misc.Unsafe的以下方法:
//获取字节对象中非静态方法的偏移量(get offset of a non-static field in the object in bytespublic native long objectFieldOffset(java.lang.reflect.Field field);//获取数组中第一个元素的偏移量(get offset of a first element in the array)public native int arrayBaseOffset(java.lang.Class aClass);//获取数组中一个元素的大小(get size of an element in the array)public native int arrayIndexScale(java.lang.Class aClass);//获取JVM中的地址值(get address size for your JVM)public native int addressSize();
在sun.misc.Unsafe中有两个额外的内省方法:staticFieldBase及staticFieldOffset,但是在这篇文章中不会使用到。这两个方法对于非安全的读写静态方法会有用。我们应如何找到一个对象的内存布局?1、循环的在分析类及父类上调用Class.getDeclaredFields,获取所有对象的字段,包括其父类中的字段;2、针对非静态字段(通过Field.getModifiers() & Modifiers.STATIC判断静态字段),通过使用Unsafe.objectFieldOffset在其父类中获取一个字段的偏移量以及该字段的shallow(注:shallow指的是当前对象本身的大小)大小:基础类型的默认值及4个或8个字节的对象引用(更多看下面);3、对数组来说,调用Unsafe.arrayBaseOffset及Unsafe.arrayIndexScale,数组的整个shallow大小将会是 当前数组的偏移量+每个数组的大小*数组的长度(原文是:offset + scale * Array.getLength(array)),当然了也包括对数组本身引用的大小(看前面提到的);4、别忘了对象图的循环引用,因而就需要对前面已经分析过的对象进行跟踪记录(针对这些情况,推荐使用IdentityHashMap)Java对象引用大小是一个非常不确定的值(原文:Java Object reference size is quite a virtual value),它可能是4个字节或者是8个字节,这个取决于你的JVM设置以及给了多少内存给JVM,针对32G以上的堆,它就总是8个字节,但是针对小一点的堆就是4个字节除非你在JVM设置里关掉设置-XX:-UseCompressedOops(我不确定这个功能是在JVM的哪个版本加进来的,或者是默认是打开的)。结果就是,安全的方式获取对像引用的大小就是找到Object[]数组中一个元素的大小:unsafe.arrayIndexScale( Object[].class ),针对这种情况,Unsafe.addressSize倒不实用了。针对32G以下堆内存中例用4字节引用的一点小小注意。一个正常的4个字节的指针可以定位到4G地址空间任何地址。如果我们假设所有已分配的对象将通过8 字节边界对齐,在我们的32位指针中我们将不再需要最低3位(这些位将总是等于零)。这意味着我们可以存储35位地址在32位中。(这一节附上原文如下:A small implementation note on 4 byte references on under 32G heaps. A normal 4 byte pointer could addressany byte in 4G address space. If we will assume that all allocated objects will be aligned by 8 bytes boundary, we won’t need 3 lowest bits in our 32 bit pointers anymore (these bits will always be equal to zeroes). This means that we can store 35 bit addresses in 32 bit value:)32_bit_reference = ( int ) ( actual_64_bit_pointer >> 3 )35位允许寻址 32位*8=4G*8=32G地址空间。写这个工具时发现的其它的一些有趣的事情1、要打印数组的内容,必须使用Arrays.toString(包括基本类型及对象数组);2、你必须要小心 - 内省方法(introspection method)只接受对象作为字段值,因此你最终可能处在无限循环中:整型打包成整数,以便传递到内省的方法。里面你会发现一个 Integer.value字段,并尝试再次内省了 - 瞧,你又回到了开始的地方!3、要内省(introspect)对象数组中所有非空的值 - 这仅仅是间接的对象图中的外部level(原文:this is just an extra level of indirection in the object graph)如何使用ClassIntrospector类?仅需要实例化它并且在你的任意的对象中调用它的实例内省(introspect)方法,它会返回一个ObjectInfo对象,这个对象与你的“根‘对象有关,这个对象将指向它的所有子项,我想这可能是足够的打印其toString方法的结果和/或调用ObjectInfo.getDeepSize方法(原文:I think it may be sufficient to print its toString method result and/or to call ObjectInfo.getDeepSize method),它将通过你的”根“对象引用,返回你的所有对象的总内存消耗。ClassIntrospector不是线程安全的,但是你可以在同一个线程中任意多次调用内省(introspect)方法。