由于在项目中需要大致计算一下对象的内存占用率(Hadoop中的Reduce端内存占用居高不下却又无法解释),因此深入学习了一下如何准确计算对象的大小。

使用system.gc()和java.lang.Runtime类中的freeMemory(),totalMemory(),maxMemory()这几个方法测量Java对象的大小,这种方法的优点是数据类型大小无关的,不同的操作系统,都可以得到占用的内存,但经常我们手动调用的GC并未起到预期的效果,计算得不够精确。

又有人想将对象进行序列化之后的byte[]输出,根据这个大小得到对象在内存中的大小,这种方法是错误的,序列化的结果是种特定格式的数据,这种格式在多种JVM之间兼容,但是这种格式的数据在内存中占用的空间大小与序列化后的结果无关。

参考了几个大牛们的blog,例如:

有些时候还是需要自己动手来观察一下所有的对象在内存中的状态,还是优先考虑使用Instrument的方式,但是Instrument方式仅返回某个对象的大小而不包括其成员变量所引用的对象。

Instrumentation.getObjectSize()会计算的对象中的基本类型,以及引用的长度,包括数组,但不会计算其中包含的对象类型里面的对象类型内容(会计算其中内部的基本类型)。

这种方式要求创建一个带有public static void premain(String[] args, Instrumentation inst)方法,不能直接在IDE中直接调用该方法,只能通过构建jar包的方式,MANIFEST.MF中加入这一行:

Premain-Class: com.clamaa.serialization.test.SizeOfObject

使用下面的方式进行调用:

java -javaagent:*.jar

JVM在调用时注入的执行类时,会调用到premain方法,传入Instrumentation对象,这时就可以使用Instrumentation.getObjectSize(Object)来计算对象占用的内存大小。

我这里直接使用了maven的方式进行调用,在maven构建时指定加入对应的MANIFEST.MF模版文件。

maven-assembly-plugin
2.4.1
true
jar-with-dependencies
src/main/java/MANIFEST.MF
make-assembly
package
single

在本机运行时,由于是64位JVM,对比一下使用参数:XX+(-)UseCompressedOops。

在启用指针压缩后,测试的一些数据结果:

Bytes used by object: 16
Bytes used by int 2120121: 16
Bytes used by Integer 202323 : 16
Bytes used by new byte[3] : 24
Bytes used by new byte[30] : 48
Bytes used by string a : 24
Bytes used by string aaaabcsdsd : 24
Bytes used by new Object[100] : 416
Bytes used by new HashMap(100) : 48
Bytes used by new HashMap(1000) : 48

可以看到对应String,HashMap对象,无论其内容多大,用Instrument计算出来的只是对象的大小,但是数组不同,随着数组的大小增大,其内存占用率提高很大。

对象的大小如何计算?这篇blog讲的非常好:

原生类型(primitive type)的内存占用如下:

Primitive Type
Memory Required(bytes)
boolean
1
byte
1
short
2
char
2
int
4
float
4
long
8
double
8

reference类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes。

在Hotspot中对象数据的计算方式:

(对象头 + 实例数据 + padding) % 8等于0且0 <= padding < 8

这里就不再赘述如何计算单个对象引用的方式,可以查看上面介绍的blog。

上述的blog中提到了一个工具:

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
/**
* 对象占用字节大小工具类
*
* @author tianmai.fh
* @date 2014-03-18 11:29
*/
public class SizeOfObject {
static Instrumentation inst;
public static void premain(String args, Instrumentation instP) {
inst = instP;
}
/**
* 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、
* 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;
* 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 
*
* @param obj
* @return
*/
public static long sizeOf(Object obj) {
return inst.getObjectSize(obj);
}
/**
* 递归计算当前对象占用空间总大小,包括当前类和超类的实例字段大小以及实例字段引用对象大小
*
* @param objP
* @return
* @throws IllegalAccessException
*/
public static long fullSizeOf(Object objP) throws IllegalAccessException {
Set visited = new HashSet();
Deque toBeQueue = new ArrayDeque();
toBeQueue.add(objP);
long size = 0L;
while (toBeQueue.size() > 0) {
Object obj = toBeQueue.poll();
//sizeOf的时候已经计基本类型和引用的长度,包括数组
size += skipObject(visited, obj) ? 0L : sizeOf(obj);
Class> tmpObjClass = obj.getClass();
if (tmpObjClass.isArray()) {
//[I , [F 基本类型名字长度是2
if (tmpObjClass.getName().length() > 2) {
for (int i = 0, len = Array.getLength(obj); i < len; i++) {
Object tmp = Array.get(obj, i);
if (tmp != null) {
//非基本类型需要深度遍历其对象
toBeQueue.add(Array.get(obj, i));
}
}
}
} else {
while (tmpObjClass != null) {
Field[] fields = tmpObjClass.getDeclaredFields();
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers()) //静态不计
|| field.getType().isPrimitive()) { //基本类型不重复计
continue;
}
field.setAccessible(true);
Object fieldValue = field.get(obj);
if (fieldValue == null) {
continue;
}
toBeQueue.add(fieldValue);
}
tmpObjClass = tmpObjClass.getSuperclass();
}
}
}
return size;
}
/**
* String.intern的对象不计;计算过的不计,也避免死循环
*
* @param visited
* @param obj
* @return
*/
static boolean skipObject(Set visited, Object obj) {
if (obj instanceof String && obj == ((String) obj).intern()) {
return true;
}
return visited.contains(obj);
}
}

这个工具就是用于解决刚才说明的Instrumentation.getObjectSize()只能够计算对象的大小的问题。SizeOfObject中提供了两个方法,sizeOf仍然是直接计算对象大小,而fullSizeOf提供了一个用于递归计算当前对象占用大小。 递归计算当前对象占用大小时,大致根据下面的算法计算:

递归列出当前对象的所有字段,跳过被执行过intern的String;

如果是引用类型的数组,遍历该引用类型;

如果是引用对象,列出所有的非基本类型/非static字段,并列出父类的字段;

每个字段进行再次遍历;

最终得到计算后的对象大小结果。

但是有时候使用这种调用java -javaagent的方式也是不太方便的,尤其是不容易控制java调用的时候,比如在hadoop中启动的map/reduce任务,虽然可以通过参数来控制其选项,但不容易控制调用的java命令,也无法加入java -javaagent。

另外一种方式虽然没有Instrument那样精确,但是也是可以接受的,也不用特殊的方式启动。

这种方法使用一个MemorySizes:

public class MemorySizes {
private final Map primitiveSizes = new IdentityHashMap() {
{
put(boolean.class, new Integer(1));
put(byte.class, new Integer(1));
put(char.class, new Integer(2));
put(short.class, new Integer(2));
put(int.class, new Integer(4));
put(float.class, new Integer(4));
put(double.class, new Integer(8));
put(long.class, new Integer(8));
}
};
public int getPrimitiveFieldSize(Class clazz) {
return ((Integer) primitiveSizes.get(clazz)).intValue();
}
public int getPrimitiveArrayElementSize(Class clazz) {
return getPrimitiveFieldSize(clazz);
}
public int getPointerSize() {
return 4;
}
public int getClassSize() {
return 8;
}
}

然后定义一个MemoryCount来对对象的大小进行计算:

/**
* This class can estimate how much memory an Object uses. It is
* fairly accurate for JDK 1.4.2. It is based on the newsletter #29.
*/
public final class MemoryCounter {
private static final MemorySizes sizes = new MemorySizes();
private final Map visited = new IdentityHashMap();
private final Stack stack = new Stack();
public synchronized long estimate(Object obj) {
assert visited.isEmpty();
assert stack.isEmpty();
long result = _estimate(obj);
while (!stack.isEmpty()) {
result += _estimate(stack.pop());
}
visited.clear();
return result;
}
private boolean skipObject(Object obj) {
if (obj instanceof String) {
// this will not cause a memory leak since
// unused interned Strings will be thrown away
if (obj == ((String) obj).intern()) {
return true;
}
}
return (obj == null)
|| visited.containsKey(obj);
}
private long _estimate(Object obj) {
if (skipObject(obj)) {
return 0;
}
visited.put(obj, null);
long result = 0;
Class clazz = obj.getClass();
if (clazz.isArray()) {
return _estimateArray(obj);
}
while (clazz != null) {
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (!Modifier.isStatic(fields[i].getModifiers())) {
if (fields[i].getType().isPrimitive()) {
result += sizes.getPrimitiveFieldSize(
fields[i].getType());
} else {
result += sizes.getPointerSize();
fields[i].setAccessible(true);
try {
Object toBeDone = fields[i].get(obj);
if (toBeDone != null) {
stack.add(toBeDone);
}
} catch (IllegalAccessException ex) {
assert false;
}
}
}
}
clazz = clazz.getSuperclass();
}
result += sizes.getClassSize();
return roundUpToNearestEightBytes(result);
}
private long roundUpToNearestEightBytes(long result) {
if ((result % 8) != 0) {
result += 8 - (result % 8);
}
return result;
}
protected long _estimateArray(Object obj) {
long result = 16;
int length = Array.getLength(obj);
if (length != 0) {
Class arrayElementClazz = obj.getClass().getComponentType();
if (arrayElementClazz.isPrimitive()) {
result += length *
sizes.getPrimitiveArrayElementSize(arrayElementClazz);
} else {
for (int i = 0; i < length; i++) {
result += sizes.getPointerSize() +
_estimate(Array.get(obj, i));
}
}
}
return result;
}
}

经过我们的代码实际计算:

System.gc();
SysOutLogger.info("Total memory: " + Runtime.getRuntime().totalMemory());
SysOutLogger.info("Free memory: " + Runtime.getRuntime().freeMemory());
long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
SysOutLogger.info("Used memory current: " + usedMemory);
SysOutLogger.info(String.format("Current Stat: %s, Used Memory: %s", currentStat.toString(), new MemoryCounter().estimate(currentStat)));

证明与实际占用的内存差距不大,也是可以接受的:

[INFO] 2014-10-22 19:52:53 : Total memory: 2327707648
[INFO] 2014-10-22 19:52:53 : Free memory: 774631832
[INFO] 2014-10-22 19:52:53 : Used memory current: 1553075816
[INFO] 2014-10-22 19:53:19 : Current Stat, Used Memory: 1479780762