blob: 24ef3039b91a4cc93ea548297956941579840bc6 [file] [log] [blame]
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.nashorn.internal.ir.debug;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Contains utility methods for calculating the memory usage of objects. It
* only works on the HotSpot JVM, and infers the actual memory layout (32 bit
* vs. 64 bit word size, compressed object pointers vs. uncompressed) from
* best available indicators. It can reliably detect a 32 bit vs. 64 bit JVM.
* It can only make an educated guess at whether compressed OOPs are used,
* though; specifically, it knows what the JVM's default choice of OOP
* compression would be based on HotSpot version and maximum heap sizes, but if
* the choice is explicitly overridden with the <tt>-XX:{+|-}UseCompressedOops</tt> command line
* switch, it can not detect
* this fact and will report incorrect sizes, as it will presume the default JVM
* behavior.
*/
public final class ObjectSizeCalculator {
/**
* Describes constant memory overheads for various constructs in a JVM implementation.
*/
public interface MemoryLayoutSpecification {
/**
* Returns the fixed overhead of an array of any type or length in this JVM.
*
* @return the fixed overhead of an array.
*/
int getArrayHeaderSize();
/**
* Returns the fixed overhead of for any {@link Object} subclass in this JVM.
*
* @return the fixed overhead of any object.
*/
int getObjectHeaderSize();
/**
* Returns the quantum field size for a field owned by an object in this JVM.
*
* @return the quantum field size for an object.
*/
int getObjectPadding();
/**
* Returns the fixed size of an object reference in this JVM.
*
* @return the size of all object references.
*/
int getReferenceSize();
/**
* Returns the quantum field size for a field owned by one of an object's ancestor superclasses
* in this JVM.
*
* @return the quantum field size for a superclass field.
*/
int getSuperclassFieldPadding();
}
private static class CurrentLayout {
private static final MemoryLayoutSpecification SPEC =
getEffectiveMemoryLayoutSpecification();
}
/**
* Given an object, returns the total allocated size, in bytes, of the object
* and all other objects reachable from it. Attempts to to detect the current JVM memory layout,
* but may fail with {@link UnsupportedOperationException};
*
* @param obj the object; can be null. Passing in a {@link java.lang.Class} object doesn't do
* anything special, it measures the size of all objects
* reachable through it (which will include its class loader, and by
* extension, all other Class objects loaded by
* the same loader, and all the parent class loaders). It doesn't provide the
* size of the static fields in the JVM class that the Class object
* represents.
* @return the total allocated size of the object and all other objects it
* retains.
* @throws UnsupportedOperationException if the current vm memory layout cannot be detected.
*/
public static long getObjectSize(final Object obj) throws UnsupportedOperationException {
return obj == null ? 0 : new ObjectSizeCalculator(CurrentLayout.SPEC).calculateObjectSize(obj);
}
// Fixed object header size for arrays.
private final int arrayHeaderSize;
// Fixed object header size for non-array objects.
private final int objectHeaderSize;
// Padding for the object size - if the object size is not an exact multiple
// of this, it is padded to the next multiple.
private final int objectPadding;
// Size of reference (pointer) fields.
private final int referenceSize;
// Padding for the fields of superclass before fields of subclasses are
// added.
private final int superclassFieldPadding;
private final Map<Class<?>, ClassSizeInfo> classSizeInfos = new IdentityHashMap<>();
private final Map<Object, Object> alreadyVisited = new IdentityHashMap<>();
private final Map<Class<?>, ClassHistogramElement> histogram = new IdentityHashMap<>();
private final Deque<Object> pending = new ArrayDeque<>(16 * 1024);
private long size;
/**
* Creates an object size calculator that can calculate object sizes for a given
* {@code memoryLayoutSpecification}.
*
* @param memoryLayoutSpecification a description of the JVM memory layout.
*/
public ObjectSizeCalculator(final MemoryLayoutSpecification memoryLayoutSpecification) {
Objects.requireNonNull(memoryLayoutSpecification);
arrayHeaderSize = memoryLayoutSpecification.getArrayHeaderSize();
objectHeaderSize = memoryLayoutSpecification.getObjectHeaderSize();
objectPadding = memoryLayoutSpecification.getObjectPadding();
referenceSize = memoryLayoutSpecification.getReferenceSize();
superclassFieldPadding = memoryLayoutSpecification.getSuperclassFieldPadding();
}
/**
* Given an object, returns the total allocated size, in bytes, of the object
* and all other objects reachable from it.
*
* @param obj the object; can be null. Passing in a {@link java.lang.Class} object doesn't do
* anything special, it measures the size of all objects
* reachable through it (which will include its class loader, and by
* extension, all other Class objects loaded by
* the same loader, and all the parent class loaders). It doesn't provide the
* size of the static fields in the JVM class that the Class object
* represents.
* @return the total allocated size of the object and all other objects it
* retains.
*/
public synchronized long calculateObjectSize(final Object obj) {
// Breadth-first traversal instead of naive depth-first with recursive
// implementation, so we don't blow the stack traversing long linked lists.
histogram.clear();
try {
for (Object o = obj;;) {
visit(o);
if (pending.isEmpty()) {
return size;
}
o = pending.removeFirst();
}
} finally {
alreadyVisited.clear();
pending.clear();
size = 0;
}
}
/**
* Get the class histogram
* @return class histogram element list
*/
public List<ClassHistogramElement> getClassHistogram() {
return new ArrayList<>(histogram.values());
}
private ClassSizeInfo getClassSizeInfo(final Class<?> clazz) {
ClassSizeInfo csi = classSizeInfos.get(clazz);
if(csi == null) {
csi = new ClassSizeInfo(clazz);
classSizeInfos.put(clazz, csi);
}
return csi;
}
private void visit(final Object obj) {
if (alreadyVisited.containsKey(obj)) {
return;
}
final Class<?> clazz = obj.getClass();
if (clazz == ArrayElementsVisitor.class) {
((ArrayElementsVisitor) obj).visit(this);
} else {
alreadyVisited.put(obj, obj);
if (clazz.isArray()) {
visitArray(obj);
} else {
getClassSizeInfo(clazz).visit(obj, this);
}
}
}
private void visitArray(final Object array) {
final Class<?> arrayClass = array.getClass();
final Class<?> componentType = arrayClass.getComponentType();
final int length = Array.getLength(array);
if (componentType.isPrimitive()) {
increaseByArraySize(arrayClass, length, getPrimitiveFieldSize(componentType));
} else {
increaseByArraySize(arrayClass, length, referenceSize);
// If we didn't use an ArrayElementsVisitor, we would be enqueueing every
// element of the array here instead. For large arrays, it would
// tremendously enlarge the queue. In essence, we're compressing it into
// a small command object instead. This is different than immediately
// visiting the elements, as their visiting is scheduled for the end of
// the current queue.
switch (length) {
case 0: {
break;
}
case 1: {
enqueue(Array.get(array, 0));
break;
}
default: {
enqueue(new ArrayElementsVisitor((Object[]) array));
}
}
}
}
private void increaseByArraySize(final Class<?> clazz, final int length, final long elementSize) {
increaseSize(clazz, roundTo(arrayHeaderSize + length * elementSize, objectPadding));
}
private static class ArrayElementsVisitor {
private final Object[] array;
ArrayElementsVisitor(final Object[] array) {
this.array = array;
}
public void visit(final ObjectSizeCalculator calc) {
for (final Object elem : array) {
if (elem != null) {
calc.visit(elem);
}
}
}
}
void enqueue(final Object obj) {
if (obj != null) {
pending.addLast(obj);
}
}
void increaseSize(final Class<?> clazz, final long objectSize) {
ClassHistogramElement he = histogram.get(clazz);
if(he == null) {
he = new ClassHistogramElement(clazz);
histogram.put(clazz, he);
}
he.addInstance(objectSize);
size += objectSize;
}
static long roundTo(final long x, final int multiple) {
return ((x + multiple - 1) / multiple) * multiple;
}
private class ClassSizeInfo {
// Padded fields + header size
private final long objectSize;
// Only the fields size - used to calculate the subclasses' memory
// footprint.
private final long fieldsSize;
private final Field[] referenceFields;
public ClassSizeInfo(final Class<?> clazz) {
long newFieldsSize = 0;
final List<Field> newReferenceFields = new LinkedList<>();
for (final Field f : clazz.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
final Class<?> type = f.getType();
if (type.isPrimitive()) {
newFieldsSize += getPrimitiveFieldSize(type);
} else {
f.setAccessible(true);
newReferenceFields.add(f);
newFieldsSize += referenceSize;
}
}
final Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
final ClassSizeInfo superClassInfo = getClassSizeInfo(superClass);
newFieldsSize += roundTo(superClassInfo.fieldsSize, superclassFieldPadding);
newReferenceFields.addAll(Arrays.asList(superClassInfo.referenceFields));
}
this.fieldsSize = newFieldsSize;
this.objectSize = roundTo(objectHeaderSize + newFieldsSize, objectPadding);
this.referenceFields = newReferenceFields.toArray(
new Field[newReferenceFields.size()]);
}
void visit(final Object obj, final ObjectSizeCalculator calc) {
calc.increaseSize(obj.getClass(), objectSize);
enqueueReferencedObjects(obj, calc);
}
public void enqueueReferencedObjects(final Object obj, final ObjectSizeCalculator calc) {
for (final Field f : referenceFields) {
try {
calc.enqueue(f.get(obj));
} catch (final IllegalAccessException e) {
final AssertionError ae = new AssertionError(
"Unexpected denial of access to " + f);
ae.initCause(e);
throw ae;
}
}
}
}
private static long getPrimitiveFieldSize(final Class<?> type) {
if (type == boolean.class || type == byte.class) {
return 1;
}
if (type == char.class || type == short.class) {
return 2;
}
if (type == int.class || type == float.class) {
return 4;
}
if (type == long.class || type == double.class) {
return 8;
}
throw new AssertionError("Encountered unexpected primitive type " +
type.getName());
}
// ALERT: java.lang.management is not available in compact 1. We need
// to use reflection to soft link test memory statistics.
static Class<?> managementFactory = null;
static Class<?> memoryPoolMXBean = null;
static Class<?> memoryUsage = null;
static Method getMemoryPoolMXBeans = null;
static Method getUsage = null;
static Method getMax = null;
static {
try {
managementFactory = Class.forName("java.lang.management.ManagementFactory");
memoryPoolMXBean = Class.forName("java.lang.management.MemoryPoolMXBean");
memoryUsage = Class.forName("java.lang.management.MemoryUsage");
getMemoryPoolMXBeans = managementFactory.getMethod("getMemoryPoolMXBeans");
getUsage = memoryPoolMXBean.getMethod("getUsage");
getMax = memoryUsage.getMethod("getMax");
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
// Pass thru, asserts when attempting to use.
}
}
/**
* Return the current memory usage
* @return current memory usage derived from system configuration
*/
public static MemoryLayoutSpecification getEffectiveMemoryLayoutSpecification() {
final String vmName = System.getProperty("java.vm.name");
if (vmName == null || !vmName.startsWith("Java HotSpot(TM) ")) {
throw new UnsupportedOperationException(
"ObjectSizeCalculator only supported on HotSpot VM");
}
final String dataModel = System.getProperty("sun.arch.data.model");
if ("32".equals(dataModel)) {
// Running with 32-bit data model
return new MemoryLayoutSpecification() {
@Override public int getArrayHeaderSize() {
return 12;
}
@Override public int getObjectHeaderSize() {
return 8;
}
@Override public int getObjectPadding() {
return 8;
}
@Override public int getReferenceSize() {
return 4;
}
@Override public int getSuperclassFieldPadding() {
return 4;
}
};
} else if (!"64".equals(dataModel)) {
throw new UnsupportedOperationException("Unrecognized value '" +
dataModel + "' of sun.arch.data.model system property");
}
final String strVmVersion = System.getProperty("java.vm.version");
final int vmVersion = Integer.parseInt(strVmVersion.substring(0,
strVmVersion.indexOf('.')));
if (vmVersion >= 17) {
long maxMemory = 0;
/*
See ALERT above. The reflection code below duplicates the following
sequence, and avoids hard coding of java.lang.management.
for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
maxMemory += mp.getUsage().getMax();
}
*/
if (getMemoryPoolMXBeans == null) {
throw new AssertionError("java.lang.management not available in compact 1");
}
try {
final List<?> memoryPoolMXBeans = (List<?>)getMemoryPoolMXBeans.invoke(managementFactory);
for (final Object mp : memoryPoolMXBeans) {
final Object usage = getUsage.invoke(mp);
final Object max = getMax.invoke(usage);
maxMemory += ((Long)max).longValue();
}
} catch (IllegalAccessException |
IllegalArgumentException |
InvocationTargetException ex) {
throw new AssertionError("java.lang.management not available in compact 1");
}
if (maxMemory < 30L * 1024 * 1024 * 1024) {
// HotSpot 17.0 and above use compressed OOPs below 30GB of RAM total
// for all memory pools (yes, including code cache).
return new MemoryLayoutSpecification() {
@Override public int getArrayHeaderSize() {
return 16;
}
@Override public int getObjectHeaderSize() {
return 12;
}
@Override public int getObjectPadding() {
return 8;
}
@Override public int getReferenceSize() {
return 4;
}
@Override public int getSuperclassFieldPadding() {
return 4;
}
};
}
}
// In other cases, it's a 64-bit uncompressed OOPs object model
return new MemoryLayoutSpecification() {
@Override public int getArrayHeaderSize() {
return 24;
}
@Override public int getObjectHeaderSize() {
return 16;
}
@Override public int getObjectPadding() {
return 8;
}
@Override public int getReferenceSize() {
return 8;
}
@Override public int getSuperclassFieldPadding() {
return 8;
}
};
}
}