1. 程式人生 > >jvm兩種方式獲取物件所佔用的記憶體

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包
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型別時,其實是有常量池的存在的,所以計算出來的只我們只能做個參考即可。