jvm两种方式获取对象所占用的内存

在开发过程中,我们有时需要来获取某个对象的大小,以方便我们参考,来决定开发的技术方案。jvm中提供了两种方式来获取一个对象的大小。

通过Instrumentation来计算对象的大小

  • 编写计算代码:
package com.java.basic;

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,应当去掉--teasp  
            // 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)   
            {// 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;  
    }  
}  

其中sizeof方法仅仅获取的是当前对象的大小,而该对象的如果存在对其他对象的引用,则不在计算范围以内,而fullsizeof则会计算整体的大小。

  • 将该java文件进行编译,并打成jar包
  1. com.java.basic.SizeOfAgent .java

jar cvf sizeOfAgent.jar com/java.basic/SizeOfAgent .class

  • 修改META-INF/MANIFEST.MF文件内容
    Premain-Class: com.java.basic.SizeOfAgent

Boot-Class-Path:
Can-Redefine-Classes: false
注意:每个冒号后面都有一个空格,且最后一行会有一个换行

  • 将该jar包导入项目
  • 添加启动参数:-javaagent:E:sizeOfAgent.jar
    我这边是将该jar包放在e盘,这里填写绝对路径。

这样我们就可以通过调用该类中的sizeOf方法或者fullSizeOf方法即可。

使用Unsafe类来获取对象大小

unsafe对象可以获取到一个对象中各个属性的内存指针的偏移量,可以利用其来计算一个对象的大小。

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.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

import sun.misc.Unsafe;

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);
        }
    }

    public int getObjectRefSize() {
        return objectRefSize;
    }

    /** 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);

    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;
    }

    static 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实例,并调用其introspect方法,参数为需要计算大小的对象,就可以获取到ObjectInfo对象,这个对象中就包含了要计算的对象的各项信息(名字,类型,属性的偏移量等),想要获取对象的大小,我们只需要调用OjbectInfo的getDeepSiz即可。
ClassIntrospector中还定义了一个方法getObjectRefSize,这个方法的作用是获取当前虚拟机对象引用指针所占的空间,如果机器的内存在32G以下,则会默认开启指针压缩,占4个字节,否则占8个,可以使用参数-XX:-UseCompressedOops进行指针压缩

下面我们进行一个简单的验证:

首先我们先定义一个对象:

public class Person {

    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

测试代码:

public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(SizeOfAgent.fullSizeOf(person));
        ClassIntrospector cIntrospector = new ClassIntrospector();
        ObjectInfo oInfo = cIntrospector.introspect(person);
        System.out.println(oInfo.getDeepSize());
}

运行结果:

24
24

两种方法的运行结果一致,我们进行对象的手动计算,计算公式:
mark头(8字节)+oop指针(4字节)+对象属性
以person对象为例:
mark头(8字节)+oop指针(4字节)+name(String类型引用4字节)+age(int类型引用4字节)=20
jvm会对一个对象进行内存对齐,是的对象的大小为8的倍数,所以最终结果为24。

当然这两种计算方式都是对对象的一个大概计算,当一个对象引用String类型时,其实是有常量池的存在的,所以计算出来的只我们只能做个参考即可。

上一篇:从零学springboot——使用jdbcTemplate的方式自定义sql进行数据库操作


下一篇:《人月神话》(P2)错误的进度估计