准确计算Java中对象的大小

本文介绍如何精确计算Java对象的内存占用情况,探讨了使用Instrumentation和MemoryCounter等工具的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

 

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

 

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

 

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

http://www.cnblogs.com/yangjiandan/p/3534781.html

http://happyqing.iteye.com/blog/2013639

http://article.yeeyan.org/view/104091/62930

 

有些时候还是需要自己动手来观察一下所有的对象在内存中的状态,还是优先考虑使用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 <main class>

 

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

 

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

        <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.4.1</version>
                <configuration>
                    <appendAssemblyId>true</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestFile>src/main/java/MANIFEST.MF</manifestFile>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

 

在本机运行时,由于是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讲的非常好:

http://www.cnblogs.com/magialmoon/p/3757767.html

  

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

Primitive TypeMemory 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;  
    }  
  
    /** 
     * 直接计算当前对象占用空间大小,包括当前类及超类的基本类型实例字段大小、<br></br> 
     * 引用类型实例字段引用大小、实例基本类型数组总占用空间、实例引用类型数组引用本身占用空间大小;<br></br> 
     * 但是不包括超类继承下来的和当前类声明的实例引用字段的对象本身的大小、实例引用数组引用的对象本身的大小 <br></br> 
     * 
     * @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<Object> visited = new HashSet<Object>();  
        Deque<Object> toBeQueue = new ArrayDeque<Object>();  
        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<Object> 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
 
 
 
<think>好的,用户想知道如何在Java计算对象大小。我之前看过一些资料,记得主要有几种方法,比如使用InstrumentationAPI或者第三方库。用户可能是在优化内存使用,或者调试内存泄漏问题,所以需要准确测量对象占用的内存。首先,我得确认用户的需求是什么。他们可能已经知道基本的方法,但需要详细的步骤或不同的实现方式。比如,他们是否了解InstrumentationAPI的使用?或者是否需要示例代码?然后,根据提供的引用,用户提到了SizeOf库Runtime的方法。引用1中的代码展示了使用MemoryMeasurer来测量对象大小,而引用2则通过Runtime.freeMemory()来估算。这两种方法各有优缺点。InstrumentationAPI需要agent的方式启动,可能对新手不太友好,而Runtime的方法可能不够精确,但容易实现。我需要分步骤介绍不同的方法,并给出代码示例。比如,第一种方法使用Instrumentation API,需要创建premain方法,打包jar,然后通过命令行启动。这种方法虽然准确,但步骤较多,适合需要精确测量的场景。第二种方法使用第三方库,比如SizeOf,可能简单,但需要引入依赖。第三种方法使用Runtime的freeMemory(),通过GC后计算内存差,适合快速估算,但可能受其他对象影响。还要注意用户可能遇到的常见问题,比如如何避免其他对象干扰,或者如何正确触发GC。比如在引用2的代码中,用户显式调用System.gc(),但这并不能保证立即执行,所以测量可能有误差。需要提醒用户多次测量取平均值,或者确保测试环境干净。 另外,需要提到对象内存的结构,比如对象头、对齐填充等,让用户理解为什么一个简单的对象会占用比预期多的内存。比如,一个空对象在32位JVM64位JVM中的大小不同,引用类型的大小也不同。可能用户还会关心不同JDK版本的影响,比如Java8Java11是否有差异。或者不同JVM参数,比如压缩指针的使用,如何影响对象大小。这些需要简要说明,但可能不需要深入细节,除非用户特别问到。 最后,生相关问题,帮助用户进一步学习,比如如何分析内存泄漏,或者优化对象内存布局的方法。这些问题应该与当前主题相关,引导用户深入探索。</think>在Java计算对象内存大小主要有以下4种方法,结合代码示例说明实现步骤及注意事项: ### 一、Instrumentation API方法(推荐) 1. 创建`premain`类 ```java import java.lang.instrument.Instrumentation; public class ObjectSizeCalculator { private static Instrumentation instrumentation; public static void premain(String args, Instrumentation inst) { instrumentation = inst; } public static long getObjectSize(Object obj) { if (instrumentation == null) { throw new IllegalStateException("未初始化Instrumentation"); } return instrumentation.getObjectSize(obj); } } ``` 2. 打包为Agent Jar ```xml <!-- MANIFEST.MF配置 --> Premain-Class: ObjectSizeCalculator Can-Redefine-Classes: true ``` 3. 使用示例 ```java public class Main { public static void main(String[] args) { String str = "Hello"; System.out.println("对象大小:" + ObjectSizeCalculator.getObjectSize(str)); // 输出24字节 } } ``` *特点:精确测量单个对象实例大小,但无法计算对象引用的关联对象内存[^1]* ### 二、Runtime内存差计算 ```java public class RuntimeMemoryCalc { public static long calculateMemoryDiff(Runnable creator) { System.gc(); long start = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); creator.run(); System.gc(); long end = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); return end - start; } public static void main(String[] args) { long size = calculateMemoryDiff(() -> { new HashMap<Integer, String>(10000); }); System.out.println("HashMap占用内存:" + size/1024.0/1024.0 + "MB"); } } ``` *注意事项:需多次测量取平均值,确保测试环境无干扰[^2]* ### 三、第三方库实现(以Java Object Layout为例) 1. 添加依赖 ```xml <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.16</version> </dependency> ``` 2. 使用示例 ```java import org.openjdk.jol.vm.VM; public class JOLExample { public static void main(String[] args) { System.out.println(VM.current().details()); System.out.println(VM.current().sizeOf(new Integer(42))); // 输出16字节 } } ``` *优势:可分析对象内存布局,包含对象头、对齐填充等细节* ### 四、手动计算公式 对象总内存 = 对象头(12字节) + 实例数据 + 对齐填充(8字节倍数) 示例计算`String`对象: ``` 对象头:12字节 char[]引用:4字节(开启压缩指针) int hash:4字节 对齐填充:4字节 总大小 = 12 + 4 + 4 + 4 = 24字节 ``` ### 内存计算注意事项 1. 64位JVM默认开启指针压缩(-XX:+UseCompressedOops) 2. 对象内存占用包括: - 对象头:12字节(普通对象)/16字节(数组) - 实例字段:按类型占用(int=4,long=8,引用=4/8) - 对齐填充:保证总大小为8字节倍数 3. 数组对象额外存储数组长度(4字节)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值