// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.cache; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; /** * Size calculator for objects. * Thread safe. * @author vegardh * @see MemoryCounter by Dr H M Kabutz */ public class SizeCalculator { private static class ObjectSet { private final Map map = new IdentityHashMap<>(); public boolean had(Object obj) { if (map.containsKey(obj)) { return true; } map.put(obj, null); return false; } } private int getPointerSize() { return 4; } private int getClassSize() { return 8; } private int getArraySize() { return 16; } @SuppressWarnings("serial") private final IdentityHashMap, Integer> primitiveSizes = new IdentityHashMap, Integer>() { { put(boolean.class, 1); put(byte.class, 1); put(char.class, 2); put(short.class, 2); put(int.class, 4); put(float.class, 4); put(double.class, 8); put(long.class, 8); } }; // Only called on un-visited objects and only with array. private long sizeOfArray(Object a, ObjectSet visitedObjects) { long sum = getArraySize(); int length = Array.getLength(a); if (length == 0) { return sum; } Class elementClass = a.getClass().getComponentType(); if (elementClass.isPrimitive()) { sum += length * (primitiveSizes.get(elementClass)); return sum; } else { for (int i = 0; i < length; i++) { Object val = Array.get(a, i); sum += getPointerSize(); sum += sizeOfObject(val, visitedObjects); } return sum; } } private long getSumOfFields(Class clas, Object obj, ObjectSet visitedObjects) { long sum = 0; Field[] fields = clas.getDeclaredFields(); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) { if (field.getType().isPrimitive()) { sum += primitiveSizes.get(field.getType()); } else { sum += getPointerSize(); field.setAccessible(true); try { sum += sizeOfObject(field.get(obj), visitedObjects); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } } } } return sum; } // Skip literal strings private boolean isIntern(Object obj) { if (obj instanceof String) { if (obj == ((String) obj).intern()) { return true; } } return false; } // Only called on non-visited non-arrays. private long sizeOfNonArray(Class clas, Object obj, ObjectSet visitedObjects) { if (isIntern(obj)) { return 0; } long sum = getClassSize(); while (clas != null) { sum += getSumOfFields(clas, obj, visitedObjects); clas = clas.getSuperclass(); } return sum; } private long sizeOfObject(Object obj, ObjectSet visitedObjects) { if (obj == null) { return 0; } if (visitedObjects.had(obj)) { return 0; } Class clas = obj.getClass(); if (clas.isArray()) { return sizeOfArray(obj, visitedObjects); } return sizeOfNonArray(clas, obj, visitedObjects); } /** * Returns the heap size of an object/array * * @return Number of bytes for object, approximately */ public long sizeOf(Object value) { ObjectSet visitedObjects = new ObjectSet(); return sizeOfObject(value, visitedObjects); } /** * Returns the heap size of two objects/arrays, common objects counted only * once * * @return Number of bytes for objects, approximately */ public long sizeOf(Object value1, Object value2) { ObjectSet visitedObjects = new ObjectSet(); return sizeOfObject(value1, visitedObjects) + sizeOfObject(value2, visitedObjects); } /** * The approximate size in bytes for a list of objects, viewed as a closure, * ie. common objects are counted only once. * * @return total number of bytes */ public long sizeOf(List objects) { ObjectSet visitedObjects = new ObjectSet(); long sum = 0; for (Object o : objects) { sum += sizeOfObject(o, visitedObjects); } return sum; } }