天天看點

Java 單個對象占用記憶體大小的計算思路

作者:XMind

本文單個對象占用記憶體大小計算的思路(Shallow size),主要基于ehcache源碼了解。

一、使用Instrumentation特性

主體思路

利用java.lang.instrument.Instrumentation getObjectSize函數,可以實時測量對象大小。

  • 本思路關鍵在于通過Java agent得到Instrumentation對象即可,如下Java agent主要代碼:
public class SizeOfAgent {
    private static volatile Instrumentation instrumentation;
    public static void premain(String options, Instrumentation inst) {
        instrumentation = inst;
        registerSystemProperty();
    }

    public static void agentmain(String options, Instrumentation inst) {
        instrumentation = inst;
        registerSystemProperty();
    }

    public static Instrumentation getInstrumentation() {
        return instrumentation;
    }
    private SizeOfAgent() {
    }

    private static void registerSystemProperty() {
        System.getProperties().put("agent.instrumentation", instrumentation);        
    }
}           

如何加載Agent

通過Jvm啟動參數加載agent的方式就不介紹了,但是想要作為第三方jar提供的話,使用方一般懶得去設定JVM參數。比較有意思的嘗試是程式自己如何attach agent到自己程式。

步驟:

1、準備VirtualMachine Class和 attach、detach、loadAgent Method;

2、得到本程序PID;

3、VirtualMachine對象attach, loadAgent agent.jar,VirtualMachine detach;

僞代碼:

1、得到VirtualMachine相關

static {
        Method attach = null;
        Method detach = null;
        Method loadAgent = null;
        try {
            Class<?> virtualMachineClass = getVirtualMachineClass();
            attach = virtualMachineClass.getMethod("attach", String.class);
            detach = virtualMachineClass.getMethod("detach");
            loadAgent = virtualMachineClass.getMethod("loadAgent", String.class);
        } catch (Throwable e) {
        }
        VIRTUAL_MACHINE_ATTACH = attach;
        VIRTUAL_MACHINE_DETACH = detach;
        VIRTUAL_MACHINE_LOAD_AGENT = loadAgent;
    }           

2、嘗試通過MXBean得到本程序PID

String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.substring(0, name.indexOf('@'));           

3、Attach Agent

// 主要是上面的SizeOfAgent Class
File agent = getAgentFile();
Object vm = VIRTUAL_MACHINE_ATTACH.invoke(null, pid);
VIRTUAL_MACHINE_LOAD_AGENT.invoke(vm, agent.getAbsolutePath());
VIRTUAL_MACHINE_DETACH.invoke(vm);           

4、通過SystemProperties或者SizeOfAgent來拿到Instrumentation對象

static boolean agentIsAvailable() {
    try {
        if (instrumentation == null) {
            instrumentation = (Instrumentation)System.getProperties().get("agent.instrumentation");
        }
        if (instrumentation == null) {
            Class sizeOfAgentClass = ClassLoader.getSystemClassLoader().loadClass("SizeOfAgent");
            Method getInstrumentationMethod = sizeOfAgentClass.getMethod("getInstrumentation");
            instrumentation = (Instrumentation)getInstrumentationMethod.invoke(sizeOfAgentClass);
        }
        return instrumentation != null;
    } catch (Throwable e) {
        return false;
    }
}           

二、通過UnSafe特性計算記憶體偏移

基本思路是:利用UnSafe的objectFieldOffset功能得到對象相對最大的偏移,然後處理記憶體補齊就是對象占用記憶體。

數組類型計算

  • 利用arrayBaseOffset和arrayIndexScale函數計算偏移。
Class<?> klazz = obj.getClass();
int base = UNSAFE.arrayBaseOffset(klazz);
int scale = UNSAFE.arrayIndexScale(klazz);
long size = base + (scale * Array.getLength(obj));
size += CURRENT_JVM_INFORMATION.getFieldOffsetAdjustment();
if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
    size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
}
return Math.max(CURRENT_JVM_INFORMATION.getMinimumObjectSize(), size);           

複合類型

  • 利用UnSafe的objectFieldOffset函數計算偏移。
  • 這裡從本Class開始計算然後才是ParentClass,并且一旦存在屬性停止向上;原因是因為Class儲存順序由父到子,子類最大偏移即為整個Class最大偏移。
for (Class<?> klazz = obj.getClass(); klazz != null; klazz = klazz.getSuperclass()) {
    long lastFieldOffset = -1;
    for (Field f : klazz.getDeclaredFields()) {
        if (!Modifier.isStatic(f.getModifiers())) {
            lastFieldOffset = Math.max(lastFieldOffset, UNSAFE.objectFieldOffset(f));
        }
    }
    if (lastFieldOffset > 0) {
        lastFieldOffset += CURRENT_JVM_INFORMATION.getFieldOffsetAdjustment();
        lastFieldOffset += 1;
        if ((lastFieldOffset % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
            lastFieldOffset += CURRENT_JVM_INFORMATION.getObjectAlignment() -
                               (lastFieldOffset % CURRENT_JVM_INFORMATION.getObjectAlignment());
        }
        return Math.max(CURRENT_JVM_INFORMATION.getMinimumObjectSize(), lastFieldOffset);
    }
}

long size = CURRENT_JVM_INFORMATION.getObjectHeaderSize();
if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
    size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
}
return Math.max(CURRENT_JVM_INFORMATION.getMinimumObjectSize(), size);           

三、利用反射根據計算記憶體占用

基本思路是:得到對象以及父類繼承屬性,然後按照屬性類型得到占用記憶體大小然後“疊加”,同時還包括對象頭、執行個體資料、對齊填充、壓縮指針等。

數組類型計算

  • 得到對象頭,makrword、klass指針、array length 。
  • 得到數組長度然後疊加。
  • 然後計算補齊。

僞代碼:

long size = CURRENT_JVM_INFORMATION.getObjectHeaderSize() + INT.getSize();
int length = Array.getLength(obj);
if (length != 0) {
    Class<?> arrayElementClazz = obj.getClass().getComponentType();
    if (arrayElementClazz.isPrimitive()) {
        size += length * PrimitiveType.forType(arrayElementClazz).getSize();
    } else {
        size += length * PrimitiveType.getReferenceSize();
    }
}
if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
    size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
}           

複合對象

  • 得到本類以及父類所有定義的屬性(getDeclaredFields)。
  • 考慮到單個類内部會進行重排和優化,首先計算每個類中各種類型屬性的數量,而不是按照定義順序依次疊加。
    • 除了對象整體需要按8位元組對齊外,每個成員變量都盡量使本身的大小在記憶體中盡量對齊。比如 int 按 4 位對齊,long 按 8 位對齊。
    • 類屬性按照如下優先級進行排列:長整型和雙精度類型;整型和浮點型;字元和短整型;位元組類型和布爾類型,最後是引用類型。這些屬性都按照各自的機關對齊。
    • 優先按照規則一和二處理父類中的成員,接着才是子類的成員。
    • 當父類中最後一個成員和子類第一個成員的間隔如果不夠4個位元組的話,就必須擴充到4個位元組的基本機關。
    • 如果子類第一個成員是一個雙精度或者長整型,并且父類并沒有用完8個位元組,JVM會破壞規則2,按照整形(int),短整型(short),位元組型(byte),引用類型(reference)的順序,向未填滿的空間填充。
  • 對象級别計算頭(makrword、klass指針)和填充。

僞代碼:

  • 得到所有涉及的類
// 初始化對象頭大小
long size = CURRENT_JVM_INFORMATION.getObjectHeaderSize();
// 得到整個繼承結果上的Class
Stack<Class<?>> classStack = new Stack<Class<?>>();
for (Class<?> klazz = aClass; klazz != null; klazz = klazz.getSuperclass()) {
    classStack.push(klazz);
}           
  • 疊代按照規則計算屬性記憶體大小
// 從上往下處理。
Class<?> klazz = classStack.pop();
// 疊代單個類中定義屬性
Field f : klazz.getDeclaredFields()
    // 靜态忽略
    if (Modifier.isStatic(f.getModifiers())) {
        continue;
    }
    // 帶個按照類型計量
    if (f.getType().isPrimitive()) {
        switch (PrimitiveType.forType(f.getType())) {
            case BOOLEAN:
            case BYTE:
                bytes++;
                break;
            // SHORT,CHAR,INT,FLOAT,DOUBLE,LONG等依次case疊加
            default:
                throw new AssertionError();
        }
    } else {
        oops++;
    }
    if (doubles > 0 && (size % PrimitiveType.LONG.getSize()) != 0) {
        long length = PrimitiveType.LONG.getSize() - (size % PrimitiveType.LONG.getSize());
        size += PrimitiveType.LONG.getSize() - (size % PrimitiveType.LONG.getSize());

        while (length >= PrimitiveType.INT.getSize() && words > 0) {
            length -= PrimitiveType.INT.getSize();
            words--;
        }
        while (length >= PrimitiveType.SHORT.getSize() && shorts > 0) {
            length -= PrimitiveType.SHORT.getSize();
            shorts--;
        }
        while (length >= PrimitiveType.BYTE.getSize() && bytes > 0) {
            length -= PrimitiveType.BYTE.getSize();
            bytes--;
        }
        while (length >= PrimitiveType.getReferenceSize() && oops > 0) {
            length -= PrimitiveType.getReferenceSize();
            oops--;
        }
    }
    size += PrimitiveType.DOUBLE.getSize() * doubles;
    size += PrimitiveType.INT.getSize() * words;
    size += PrimitiveType.SHORT.getSize() * shorts;
    size += PrimitiveType.BYTE.getSize() * bytes;

    if (oops > 0) {
        if ((size % PrimitiveType.getReferenceSize()) != 0) {
            size += PrimitiveType.getReferenceSize() - (size % PrimitiveType.getReferenceSize());
        }
        size += oops * PrimitiveType.getReferenceSize();
    }

    if ((doubles + words + shorts + bytes + oops) > 0 && (size % PrimitiveType.getReferenceSize()) != 0) {
        size += PrimitiveType.getReferenceSize() - (size % PrimitiveType.getReferenceSize());
    }           
  • 整個類級别補齊
if ((size % CURRENT_JVM_INFORMATION.getObjectAlignment()) != 0) {
    size += CURRENT_JVM_INFORMATION.getObjectAlignment() - (size % CURRENT_JVM_INFORMATION.getObjectAlignment());
}           

繼續閱讀