本文單個對象占用記憶體大小計算的思路(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());
}