Java编程中对象内存空间占用分析

Java编程中,了解对象的内存开销非常重要。本文主要分析Java语言中对象的内存占用。

1、Java对象内存占用简介

java对象在内存中占用的空间分为3类:

  1. 对象头(Header);
  2. 实例数据(Instance Data);
  3. 对齐填充(Padding)。

我们常说的基础数据类型大小,如byte占1字节、int占4字节、long占8字节等,主要是指第二类实例数据。下图是Java对象内存占用的示意图。


Java内存占用示意图

1.1 对象头

HotSpot虚拟机的对象头包括两部分信息:markword和klass 。

1.1.1 对象头组成介绍

第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度,也就是一个int类型的对象,占4字节。

1.1.2 对象头占用空间

在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
在64位开启指针压缩的情况下-XX:+UseCompressedOops,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。
如果对象是数组,那么额外增加4个字节,这块儿用来记录数组的长度。

1.2 实例数据

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
这部分空间的占用,就等于对象各个成员变量的空间占用加和。

1.3 对齐填充

对齐填充空间并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。这是由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。
很多其它编程语言在内存管理时,也有对齐填充的概念。

2、对象内存占用的例子

// 对象A: 对象头12B + 内部对象s引用 4B + 内部对象i 基础类型int 4B + 对齐 4B = 24B
// 内部对象s 对象头12B + 2个内部的int类型8B + 内部的char[]引用 4B + 对齐0B = 24B
// 内部对象str的内部对象char数组 对象头12B + 数组长度4B + 对齐0B = 16B
// 总: 对象A 24+ 内部对象s 24B + 内部对象s的内部对象char数组 16B =64B
class A {
  String s = new String();
  int i = 0;
}

// 对象B:对象头12B + 内部对象s引用 4B + 内部对象i 基础类型int 4B + 对齐 4B = 24B
// s没有被分配堆内存空间
// 总: 对象B 24B
class B {
  String s;
  int i = 0;
}

3、如何用程序验证Java对象在运行时的内存占用

上面介绍了Java对象在内存中存储的空间消耗情况,接下来要考虑能否在Java程序中统计某个对象的实际内存占用。主要有下面几种方法。

3.1 通过监测JVM内存的使用情况

这种方法是最简单的方法,也就是在对象创建之前统计下当前JVM内存的使用情况,再创建某个对象之后,再统计一遍,这两者的差值就是创建该对象使用的内存消耗。
这种方法,可能不太准确,因为粒度总体来说比较粗,除了新建对象的占用之外,JVM可能也有其他内存开销。所以,这块儿只适用于粗略的统计。
下面是一个示例代码,统计1到50000作为字符串用+拼接时的内存消耗:

import java.io.IOException;
import java.util.Date;
public class Test {
    public static void main(String args[]) throws IOException{
        Runtime run = Runtime.getRuntime();
        run.gc();//让JVM执行一次内存回收,这样统计会更准确
        Date startTime = new Date();
        System.out.println("time: " + startTime);
        //获取开始时内存使用量
        long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );

        String str = "";
        for(int i=0; i<50000; ++i){
            str += I;
        }

        Date endTime = new Date();
        System.out.println("time: " + endTime);
        //获取结束时内存使用量
        long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
        //统计内存开销和时间开销
        System.out.println("memory difference:" + (endMem-startMem) + "M");
        System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
    }
}

运行结果如下:

time: Sat Mar 23 23:06:24 CST 2019
memory> total:123M free:120M used:3M
time: Sat Mar 23 23:06:32 CST 2019
memory> total:545M free:435M used:110M
memory difference:107M
Time difference:7181ms

Process finished with exit code 0

从结果上可以看出,+连接字符串的内存开销和内存确实超乎想象。下面再试下通过StringBuffer和StringBuffer做字符串拼接时的情况:
(1) StringBuilder拼接字符串的时空开销统计

import java.io.IOException;
import java.util.Date;
public class Test {
    public static void main(String args[]) throws IOException{
        Runtime run = Runtime.getRuntime();
        run.gc();//让JVM执行一次内存回收,这样统计会更准确
        Date startTime = new Date();
        System.out.println("time: " + startTime);
        //获取开始时内存使用量
        long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );

        StringBuilder sb = new StringBuilder();
        for(int i=0; i<50000; ++i){
            sb.append(String.valueOf(i));
        }
        String str = sb.toString();


        Date endTime = new Date();
        System.out.println("time: " + endTime);
        //获取结束时内存使用量
        long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
        //统计内存开销和时间开销
        System.out.println("memory difference:" + (endMem-startMem) + "M");
        System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
    }
}

运行结果:

time: Sat Mar 23 23:14:30 CST 2019
memory> total:123M free:120M used:3M
time: Sat Mar 23 23:14:30 CST 2019
memory> total:123M free:116M used:7M
memory difference:4M
Time difference:44ms

Process finished with exit code 0

(2) StringBuffer拼接字符串的时空开销统计

import java.io.IOException;
import java.util.Date;
public class Test {
    public static void main(String args[]) throws IOException{
        Runtime run = Runtime.getRuntime();
        run.gc();//让JVM执行一次内存回收,这样统计会更准确
        Date startTime = new Date();
        System.out.println("time: " + startTime);
        //获取开始时内存使用量
        long startMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + startMem + "M" );

        StringBuffer sb = new StringBuffer();
        for(int i=0; i<50000; ++i){
            sb.append(String.valueOf(i));
        }
        String str = sb.toString();


        Date endTime = new Date();
        System.out.println("time: " + endTime);
        //获取结束时内存使用量
        long endMem = run.totalMemory() / (1024*1024)-run.freeMemory() / (1024*1024);
        System.out.println("memory> total:" + run.totalMemory() / (1024*1024) + "M free:" + run.freeMemory() / (1024*1024) + "M used:" + endMem + "M" );
        //统计内存开销和时间开销
        System.out.println("memory difference:" + (endMem-startMem) + "M");
        System.out.println("Time difference:" + (endTime.getTime()-startTime.getTime()) + "ms");
    }
}

运行结果:

time: Sat Mar 23 23:11:45 CST 2019
memory> total:123M free:119M used:4M
time: Sat Mar 23 23:11:45 CST 2019
memory> total:123M free:115M used:8M
memory difference:4M
Time difference:62ms

Process finished with exit code 0

从上面的例子可以看出,StringBuffer和StringBuilder在字符串拼接方面的表现要明显优于+。所以,大家在编程时一定要注意,尽量避免使用+拼接字符串。

3.2 使用Unsafe类

3.2.1 Unsafe类介绍

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

3.2.2 统计内存占用的方法

这里我们使用Unsafe类,主要是用到这个类的objectFieldOffset()方法。这个方法可以返回某个类对象的每一个field在内存中的offset(这个偏移是相对于类对象所占内存区域起始位置的偏移)。
这样,我们遍历成员变量的offset,然后,将它们从小到大排序,这样就得到了对象在内存中位置最靠后的field的offset,加上这个field所占的空间大小,然后在加上对齐填充(Padding,这部分不一定有),就得到了Java对象在内存中的占用空间大小。不过,这个大小可以说是对象内存占用浅空间大小(Shallow Size),因为有的field可能是一个对象引用,在对象的内存区域中只是存了一个指针。如果要获得一个Java对象的深度空间占用(Deep Size),就需要对这些指针进行递归遍历,将得到的空间求总和。
对于数组的处理稍微有些不同:

Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。

为了防止双向链表这样的结构在内存递归统计的过程中,出现循环递归,重复统计,这里需要记录下已经统计过内存占用的对象。下面是代码(这块儿代码来自连接:计算java对象的内存占用):
ObjectInfo.java:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * This class contains object info generated by ClassIntrospector tool
 */
public class ObjectInfo {
    /** Field name */
    public final String name;
    /** Field type name */
    public final String type;
    /** Field data formatted as string */
    public final String contents;
    /** Field offset from the start of parent object */
    public final int offset;
    /** Memory occupied by this field */
    public final int length;
    /** Offset of the first cell in the array */
    public final int arrayBase;
    /** Size of a cell in the array */
    public final int arrayElementSize;
    /** Memory occupied by underlying array (shallow), if this is array type */
    public final int arraySize;
    /** This object fields */
    public final List<ObjectInfo> children;

    public ObjectInfo(String name, String type, String contents, int offset, int length, int arraySize,
                      int arrayBase, int arrayElementSize) {
        this.name = name;
        this.type = type;
        this.contents = contents;
        this.offset = offset;
        this.length = length;
        this.arraySize = arraySize;
        this.arrayBase = arrayBase;
        this.arrayElementSize = arrayElementSize;
        children = new ArrayList<ObjectInfo>(1);
    }

    public void addChild(final ObjectInfo info) {
        if (info != null) {
            children.add(info);
        }
    }

    /**
     * Get the full amount of memory occupied by a given object. This value may be slightly less than
     * an actual value because we don't worry about memory alignment - possible padding after the last object field.
     *
     * The result is equal to the last field offset + last field length + all array sizes + all child objects deep sizes
     *
     * @return Deep object size
     */
    public long getDeepSize() {
        //return length + arraySize + getUnderlyingSize( arraySize != 0 );
        return addPaddingSize(arraySize + getUnderlyingSize(arraySize != 0));
    }

    long size = 0;

    private long getUnderlyingSize(final boolean isArray) {
        //long size = 0;
        for (final ObjectInfo child : children)
            size += child.arraySize + child.getUnderlyingSize(child.arraySize != 0);
        if (!isArray && !children.isEmpty()) {
            int tempSize = children.get(children.size() - 1).offset + children.get(children.size() - 1).length;
            size += addPaddingSize(tempSize);
        }

        return size;
    }

    private static final class OffsetComparator implements Comparator<ObjectInfo> {
        @Override
        public int compare(final ObjectInfo o1, final ObjectInfo o2) {
            return o1.offset - o2.offset; //safe because offsets are small non-negative numbers
        }
    }

    //sort all children by their offset
    public void sort() {
        Collections.sort(children, new OffsetComparator());
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        toStringHelper(sb, 0);
        return sb.toString();
    }

    private void toStringHelper(final StringBuilder sb, final int depth) {
        depth(sb, depth).append("name=").append(name).append(", type=").append(type)
                .append(", contents=").append(contents).append(", offset=").append(offset)
                .append(", length=").append(length);
        if (arraySize > 0) {
            sb.append(", arrayBase=").append(arrayBase);
            sb.append(", arrayElemSize=").append(arrayElementSize);
            sb.append(", arraySize=").append(arraySize);
        }
        for (final ObjectInfo child : children) {
            sb.append('\n');
            child.toStringHelper(sb, depth + 1);
        }
    }

    private StringBuilder depth(final StringBuilder sb, final int depth) {
        for (int i = 0; i < depth; ++i)
            sb.append("\t");
        return sb;
    }

    private long addPaddingSize(long size) {
        if (size % 8 != 0) {
            return (size / 8 + 1) * 8;
        }
        return size;
    }

}

ClassIntrospector.java

import java.lang.reflect.Array;
import java.lang.reflect.Field;  
import java.lang.reflect.Modifier;  
import java.util.ArrayList;  
import java.util.Arrays;  
import java.util.Collections;  
import java.util.HashMap;  
import java.util.IdentityHashMap;  
import java.util.List;  
import java.util.Map;  

import sun.misc.Unsafe;  

/** 
 * This class could be used for any object contents/memory layout printing. 
 */  
public class ClassIntrospector {  

    private static final Unsafe unsafe;  
    /** Size of any Object reference */  
    private static final int objectRefSize;  
    static {  
        try {  
            Field field = Unsafe.class.getDeclaredField("theUnsafe");  
            field.setAccessible(true);  
            unsafe = (Unsafe) field.get(null);  

            objectRefSize = unsafe.arrayIndexScale(Object[].class);  
        } catch (Exception e) {  
            throw new RuntimeException(e);  
        }  
    }  

    /** Sizes of all primitive values */  
    private static final Map<Class, Integer> primitiveSizes;  

    static {  
        primitiveSizes = new HashMap<Class, Integer>(10);  
        primitiveSizes.put(byte.class, 1);  
        primitiveSizes.put(char.class, 2);  
        primitiveSizes.put(int.class, 4);  
        primitiveSizes.put(long.class, 8);  
        primitiveSizes.put(float.class, 4);  
        primitiveSizes.put(double.class, 8);  
        primitiveSizes.put(boolean.class, 1);  
    }  

    /** 
     * Get object information for any Java object. Do not pass primitives to 
     * this method because they will boxed and the information you will get will 
     * be related to a boxed version of your value. 
     *  
     * @param obj 
     *            Object to introspect 
     * @return Object info 
     * @throws IllegalAccessException 
     */  
    public ObjectInfo introspect(final Object obj)  
            throws IllegalAccessException {  
        try {  
            return introspect(obj, null);  
        } finally { // clean visited cache before returning in order to make  
                    // this object reusable  
            m_visited.clear();  
        }  
    }  

    // we need to keep track of already visited objects in order to support  
    // cycles in the object graphs  
    private IdentityHashMap<Object, Boolean> m_visited = new IdentityHashMap<Object, Boolean>(100); //这里的100是初始大小,超过会自增

    private ObjectInfo introspect(final Object obj, final Field fld)  
            throws IllegalAccessException {  
        // use Field type only if the field contains null. In this case we will  
        // at least know what's expected to be  
        // stored in this field. Otherwise, if a field has interface type, we  
        // won't see what's really stored in it.  
        // Besides, we should be careful about primitives, because they are  
        // passed as boxed values in this method  
        // (first arg is object) - for them we should still rely on the field  
        // type.  
        boolean isPrimitive = fld != null && fld.getType().isPrimitive();  
        boolean isRecursive = false; // will be set to true if we have already  
                                        // seen this object  
        if (!isPrimitive) {
            if (m_visited.containsKey(obj)) {
                isRecursive = true;
            }
            m_visited.put(obj, true);  
        }  

        final Class type = (fld == null || (obj != null && !isPrimitive)) ? obj  
                .getClass() : fld.getType();  
        int arraySize = 0;  
        int baseOffset = 0;  
        int indexScale = 0;  
        if (type.isArray() && obj != null) {  
            baseOffset = unsafe.arrayBaseOffset(type);  
            indexScale = unsafe.arrayIndexScale(type);  
            arraySize = baseOffset + indexScale * Array.getLength(obj);  
        }  

        final ObjectInfo root;  
        if (fld == null) {  
            root = new ObjectInfo("", type.getCanonicalName(), getContents(obj,  
                    type), 0, getShallowSize(type), arraySize, baseOffset,  
                    indexScale);  
        } else {  
            final int offset = (int) unsafe.objectFieldOffset(fld);  
            root = new ObjectInfo(fld.getName(), type.getCanonicalName(),  
                    getContents(obj, type), offset, getShallowSize(type),  
                    arraySize, baseOffset, indexScale);  
        }  

        if (!isRecursive && obj != null) {  
            if (isObjectArray(type)) {  
                // introspect object arrays  
                final Object[] ar = (Object[]) obj;  
                for (final Object item : ar)
                    if (item != null) {
                        root.addChild(introspect(item, null));
                    }
            } else {  
                for (final Field field : getAllFields(type)) {  
                    if ((field.getModifiers() & Modifier.STATIC) != 0) {  
                        continue;  
                    }  
                    field.setAccessible(true);  
                    root.addChild(introspect(field.get(obj), field));  
                }  
            }  
        }  

        root.sort(); // sort by offset  
        return root;  
    }  

    // get all fields for this class, including all superclasses fields  
    private static List<Field> getAllFields(final Class type) {
        if (type.isPrimitive()) {
            return Collections.emptyList();
        }
        Class cur = type;  
        final List<Field> res = new ArrayList<Field>(10);  
        while (true) {  
            Collections.addAll(res, cur.getDeclaredFields());
            if (cur == Object.class) {
                break;
            }
            cur = cur.getSuperclass();  
        }  
        return res;  
    }  

    // check if it is an array of objects. I suspect there must be a more  
    // API-friendly way to make this check.  
    private static boolean isObjectArray(final Class type) {
        if (!type.isArray()) {
            return false;
        }
        if (type == byte[].class || type == boolean[].class
                || type == char[].class || type == short[].class
                || type == int[].class || type == long[].class
                || type == float[].class || type == double[].class) {
            return false;
        }
        return true;  
    }  

    // advanced toString logic  
    private static String getContents(final Object val, final Class type) {
        if (val == null) {
            return "null";
        }
        if (type.isArray()) {
            if (type == byte[].class) {
                return Arrays.toString((byte[]) val);
            } else if (type == boolean[].class) {
                return Arrays.toString((boolean[]) val);
            } else if (type == char[].class) {
                return Arrays.toString((char[]) val);
            } else if (type == short[].class) {
                return Arrays.toString((short[]) val);
            } else if (type == int[].class) {
                return Arrays.toString((int[]) val);
            } else if (type == long[].class) {
                return Arrays.toString((long[]) val);
            } else if (type == float[].class) {
                return Arrays.toString((float[]) val);
            } else if (type == double[].class) {
                return Arrays.toString((double[]) val);
            } else {
                return Arrays.toString((Object[]) val);
            }
        }  
        return val.toString();  
    }  

    // obtain a shallow size of a field of given class (primitive or object  
    // reference size)  
    private static int getShallowSize(final Class type) {
        if (type.isPrimitive()) {
            final Integer res = primitiveSizes.get(type);
            return res != null ? res : 0;
        } else {
            return objectRefSize;
        }
    }  
}

TestClassIntrospector.java:

/**
 * Created by chengxia on 2019/3/24.
 */
public class TestClassIntrospector {
    private static class ObjectA {
        String str;  // 4
        int i1; // 4
        byte b1; // 1
        byte b2; // 1
        int i2;  // 4
        byte b3;  // 1
        ObjectB obj; //4
    }

    private static class ObjectB {

    }
    public static void main(String []args){
        try {
            final ClassIntrospector ci = new ClassIntrospector();
            ObjectInfo res;
            System.out.println("int:" + ci.introspect(new Integer(2)).getDeepSize());
            System.out.println("str3:" + ci.introspect("abcd").getDeepSize());

            res = ci.introspect(new ObjectA());
            System.out.println("ObjectA:" + res.getDeepSize());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果:

int:16
str3:48
ObjectA:32

Process finished with exit code 0

3.3 通过Instrumentation来统计

要通过这种方式来统计对象内存占用的大小,首先要了解java代理。

3.3.1 java代理

JavaAgent是JDK 1.5以后引入的。也叫Java代理。它是运行在main方法之前的拦截器,它内定的方法名叫 premain,也就是说先执行premain方法然后再执行main方法。
agent的代码与你的main方法在同一个JVM中运行,并被同一个system classloader装载,被同一的安全策略(security policy)和上下文(context)所管理。
实现一个JavaAgent只需要增加premain方法即可。如下代码(来自于链接:JavaAgent 简单例子):

import java.lang.instrument.Instrumentation;
public class MyAgent {

    /**
     * 该方法在main方法之前运行,与main方法运行在同一个JVM中
     * 并被同一个System ClassLoader装载
     * 被统一的安全策略(security policy)和上下文(context)管理
     *
     * @param agentOps
     * @param inst
     */
    public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=========premain方法执行========");
        System.out.println(agentOps);
    }

    /**
     * 如果不存在 premain(String agentOps, Instrumentation inst) 
     * 则会执行 premain(String agentOps)
     *
     * @param agentOps
     */
    public static void premain(String agentOps) {
        System.out.println("=========premain方法执行2========");
        System.out.println(agentOps);
    }
}

但是,这个java代理运行起来比较费劲:
(1) 首先需要用包含如下内容(注意,有五行,每个冒号后面都有一个空格):

Manifest-Version: 1.0
Premain-Class: MyAgent
Boot-Class-Path: 
Can-Redefine-Classes: false

的MAINIFEST.MF文件,将上面的代码打为jar包,这里命名为“JavaAgentTest.jar”,并将其放在用户主目录下。
(2) 写一个测试类如下:

/**
 * Created by chengxia on 2019/3/24.
 */
public class Test {
    public static void main(String []args){
        System.out.println("This is main.");
    }
}

(3) 指定运行参数
直接运行上面的类,肯定不能调起Java Agent,需要在运行时添加如下参数(VM参数):

-javaagent:/Users/chengxia/JavaAgentTest.jar=Hello1 -javaagent:/Users/chengxia/JavaAgentTest.jar=Hello2 

运行的效果如下:

=========premain方法执行========
Hello1
=========premain方法执行========
Hello2
This is main.

Process finished with exit code 0

3.3.2 通过Instrumentation统计对象的内存占内存大小

通过Java代理可以获得Instrumentation类的对象,该对象有getObjectSize()方法,可以返回一个对象的ShallowSize。至于DeepSize的统计,采取和上面Unsafe类似的思路,实现一个名为SizeOfAgent的java代理。

import java.lang.instrument.Instrumentation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;

public class SizeOfAgent
{
    private static Instrumentation inst;

    /** initializes agent */
    public static void premain(String agentArgs, Instrumentation instP)
    {
        inst = instP;
    }

    /**
     * Returns object size without member sub-objects. 
     * @param o object to get size of 
     * @return object size 
     */
    public static long sizeOf(Object o)
    {
        if(inst == null)
        {
            throw new IllegalStateException("Can not access instrumentation environment.\n" +
                    "Please check if jar file containing SizeOfAgent class is \n" +
                    "specified in the java's \"-javaagent\" command line argument.");
        }
        return inst.getObjectSize(o);
    }

    /**
     * Calculates full size of object iterating over 
     * its hierarchy graph. 
     * @param obj object to calculate size of 
     * @return object size 
     */
    public static long fullSizeOf(Object obj)
    {
        Map<Object, Object> visited = new IdentityHashMap<Object, Object>();
        Stack<Object> stack = new Stack<Object>();

        long result = internalSizeOf(obj, stack, visited);
        while (!stack.isEmpty())
        {
            result += internalSizeOf(stack.pop(), stack, visited);
        }
        visited.clear();
        return result;
    }

    private static boolean skipObject(Object obj, Map<Object, Object> visited)
    {
        if (obj instanceof String) {//这个if是bug,应当去掉
            // skip interned string  
            if (obj == ((String) obj).intern()) {
                return true;
            }
        }
        return (obj == null) || visited.containsKey(obj);
    }

    @SuppressWarnings("rawtypes")
    private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited)
    {
        if (skipObject(obj, visited))
        {
            return 0;
        }
        visited.put(obj, null);

        long result = 0;
        // get size of object + primitive variables + member pointers   
        result += SizeOfAgent.sizeOf(obj);

        // process all array elements  
        Class clazz = obj.getClass();
        if (clazz.isArray())
        {
            if(clazz.getName().length() != 2)//如果是原生数组,返回[I,代表int型数组
            {// skip primitive type array  
                int length =  Array.getLength(obj);
                for (int i = 0; i < length; I++)
                {
                    stack.add(Array.get(obj, i));
                }
            }
            return result;
        }

        // process all fields of the object  
        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())
                    {
                        continue; // skip primitive fields  
                    }
                    else
                    {
                        fields[i].setAccessible(true);
                        try
                        {
                            // objects to be estimated are put to stack  
                            Object objectToAdd = fields[i].get(obj);
                            if (objectToAdd != null)
                            {
                                stack.add(objectToAdd);
                            }
                        }
                        catch (IllegalAccessException ex)
                        {
                            assert false;
                        }
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
        return result;
    }
}  

在运行程序的时候,加上该代理的参数,这样就可以在程序中调用SizeOfAgent.fullSizeOf()静态方法获得一个对象占用内存空间的大小了。如下是一个例子:

/**
 * Created by chengxia on 2019/3/24.
 */
public class TestSizeOfAgent {
    private static class ObjectA {
        String str;  // 4
        int i1; // 4
        byte b1; // 1
        byte b2; // 1
        int i2;  // 4
        byte b3;  // 1
        ObjectB obj; //4
    }

    private static class ObjectB {

    }
    public static void main(String []args){
        try {
            System.out.println("int:" + SizeOfAgent.fullSizeOf(new Integer(2)));
            System.out.println("str3:" + SizeOfAgent.fullSizeOf("abcd"));

            System.out.println("ObjectA:" + SizeOfAgent.fullSizeOf(new ObjectA()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果如下:

int:16
str3:0
ObjectA:32

Process finished with exit code 0

可以看出,和Unsafe得出的统计结果有些不一致。这是因为后面的该方法中,对于常量池中的字符串常量,没有统计内存占用(代码中有特殊处理,因为这种不同对象只存一份,如果都统计会有重复)。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,089评论 0 2
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,390评论 1 14
  • 一紫老师,陕西人,专栏作者,特邀主播,自由撰稿人。“一紫”自媒体品牌创始人。为写心中所想,守住这片小江湖中的净土,...
    天蓝蓝_b865阅读 1,116评论 13 14
  • 每周末,除了外出或特殊情况,我都会回趟娘家。以前奶奶还健在,会有更多的牵挂;现在奶奶离开我们了,回去也不外是...
    zyb月斌阅读 172评论 0 1
  • 依然是一篇耽美文。喜欢的主要原因是脑洞有意思。 记得以前看过讲些小说的文章。说些小说就是在回答一个个问题。这本小说...
    楚楚不哭阅读 1,564评论 0 1