ObjectSizeCalculator.java revision 1423:c13179703f65
1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.ir.debug;
27
28import java.lang.reflect.Array;
29import java.lang.reflect.Field;
30import java.lang.reflect.InvocationTargetException;
31import java.lang.reflect.Method;
32import java.lang.reflect.Modifier;
33import java.util.ArrayDeque;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Deque;
37import java.util.IdentityHashMap;
38import java.util.LinkedList;
39import java.util.List;
40import java.util.Map;
41import java.util.Objects;
42
43/**
44 * Contains utility methods for calculating the memory usage of objects. It
45 * only works on the HotSpot JVM, and infers the actual memory layout (32 bit
46 * vs. 64 bit word size, compressed object pointers vs. uncompressed) from
47 * best available indicators. It can reliably detect a 32 bit vs. 64 bit JVM.
48 * It can only make an educated guess at whether compressed OOPs are used,
49 * though; specifically, it knows what the JVM's default choice of OOP
50 * compression would be based on HotSpot version and maximum heap sizes, but if
51 * the choice is explicitly overridden with the <tt>-XX:{+|-}UseCompressedOops</tt> command line
52 * switch, it can not detect
53 * this fact and will report incorrect sizes, as it will presume the default JVM
54 * behavior.
55 */
56public final class ObjectSizeCalculator {
57
58    /**
59     * Describes constant memory overheads for various constructs in a JVM implementation.
60     */
61    public interface MemoryLayoutSpecification {
62
63        /**
64         * Returns the fixed overhead of an array of any type or length in this JVM.
65         *
66         * @return the fixed overhead of an array.
67         */
68        int getArrayHeaderSize();
69
70        /**
71         * Returns the fixed overhead of for any {@link Object} subclass in this JVM.
72         *
73         * @return the fixed overhead of any object.
74         */
75        int getObjectHeaderSize();
76
77        /**
78         * Returns the quantum field size for a field owned by an object in this JVM.
79         *
80         * @return the quantum field size for an object.
81         */
82        int getObjectPadding();
83
84        /**
85         * Returns the fixed size of an object reference in this JVM.
86         *
87         * @return the size of all object references.
88         */
89        int getReferenceSize();
90
91        /**
92         * Returns the quantum field size for a field owned by one of an object's ancestor superclasses
93         * in this JVM.
94         *
95         * @return the quantum field size for a superclass field.
96         */
97        int getSuperclassFieldPadding();
98    }
99
100    private static class CurrentLayout {
101        private static final MemoryLayoutSpecification SPEC =
102                getEffectiveMemoryLayoutSpecification();
103    }
104
105    /**
106     * Given an object, returns the total allocated size, in bytes, of the object
107     * and all other objects reachable from it.  Attempts to to detect the current JVM memory layout,
108     * but may fail with {@link UnsupportedOperationException};
109     *
110     * @param obj the object; can be null. Passing in a {@link java.lang.Class} object doesn't do
111     *          anything special, it measures the size of all objects
112     *          reachable through it (which will include its class loader, and by
113     *          extension, all other Class objects loaded by
114     *          the same loader, and all the parent class loaders). It doesn't provide the
115     *          size of the static fields in the JVM class that the Class object
116     *          represents.
117     * @return the total allocated size of the object and all other objects it
118     *         retains.
119     * @throws UnsupportedOperationException if the current vm memory layout cannot be detected.
120     */
121    public static long getObjectSize(final Object obj) throws UnsupportedOperationException {
122        return obj == null ? 0 : new ObjectSizeCalculator(CurrentLayout.SPEC).calculateObjectSize(obj);
123    }
124
125    // Fixed object header size for arrays.
126    private final int arrayHeaderSize;
127    // Fixed object header size for non-array objects.
128    private final int objectHeaderSize;
129    // Padding for the object size - if the object size is not an exact multiple
130    // of this, it is padded to the next multiple.
131    private final int objectPadding;
132    // Size of reference (pointer) fields.
133    private final int referenceSize;
134    // Padding for the fields of superclass before fields of subclasses are
135    // added.
136    private final int superclassFieldPadding;
137
138    private final Map<Class<?>, ClassSizeInfo> classSizeInfos = new IdentityHashMap<>();
139
140
141    private final Map<Object, Object> alreadyVisited = new IdentityHashMap<>();
142    private final Map<Class<?>, ClassHistogramElement> histogram = new IdentityHashMap<>();
143
144    private final Deque<Object> pending = new ArrayDeque<>(16 * 1024);
145    private long size;
146
147    /**
148     * Creates an object size calculator that can calculate object sizes for a given
149     * {@code memoryLayoutSpecification}.
150     *
151     * @param memoryLayoutSpecification a description of the JVM memory layout.
152     */
153    public ObjectSizeCalculator(final MemoryLayoutSpecification memoryLayoutSpecification) {
154        Objects.requireNonNull(memoryLayoutSpecification);
155        arrayHeaderSize = memoryLayoutSpecification.getArrayHeaderSize();
156        objectHeaderSize = memoryLayoutSpecification.getObjectHeaderSize();
157        objectPadding = memoryLayoutSpecification.getObjectPadding();
158        referenceSize = memoryLayoutSpecification.getReferenceSize();
159        superclassFieldPadding = memoryLayoutSpecification.getSuperclassFieldPadding();
160    }
161
162    /**
163     * Given an object, returns the total allocated size, in bytes, of the object
164     * and all other objects reachable from it.
165     *
166     * @param obj the object; can be null. Passing in a {@link java.lang.Class} object doesn't do
167     *          anything special, it measures the size of all objects
168     *          reachable through it (which will include its class loader, and by
169     *          extension, all other Class objects loaded by
170     *          the same loader, and all the parent class loaders). It doesn't provide the
171     *          size of the static fields in the JVM class that the Class object
172     *          represents.
173     * @return the total allocated size of the object and all other objects it
174     *         retains.
175     */
176    public synchronized long calculateObjectSize(final Object obj) {
177        // Breadth-first traversal instead of naive depth-first with recursive
178        // implementation, so we don't blow the stack traversing long linked lists.
179        histogram.clear();
180        try {
181            for (Object o = obj;;) {
182                visit(o);
183                if (pending.isEmpty()) {
184                    return size;
185                }
186                o = pending.removeFirst();
187            }
188        } finally {
189            alreadyVisited.clear();
190            pending.clear();
191            size = 0;
192        }
193    }
194
195    /**
196     * Get the class histogram
197     * @return class histogram element list
198     */
199    public List<ClassHistogramElement> getClassHistogram() {
200        return new ArrayList<>(histogram.values());
201    }
202
203    private ClassSizeInfo getClassSizeInfo(final Class<?> clazz) {
204        ClassSizeInfo csi = classSizeInfos.get(clazz);
205        if(csi == null) {
206            csi = new ClassSizeInfo(clazz);
207            classSizeInfos.put(clazz, csi);
208        }
209        return csi;
210    }
211
212    private void visit(final Object obj) {
213        if (alreadyVisited.containsKey(obj)) {
214            return;
215        }
216        final Class<?> clazz = obj.getClass();
217        if (clazz == ArrayElementsVisitor.class) {
218            ((ArrayElementsVisitor) obj).visit(this);
219        } else {
220            alreadyVisited.put(obj, obj);
221            if (clazz.isArray()) {
222                visitArray(obj);
223            } else {
224                getClassSizeInfo(clazz).visit(obj, this);
225            }
226        }
227    }
228
229    private void visitArray(final Object array) {
230        final Class<?> arrayClass = array.getClass();
231        final Class<?> componentType = arrayClass.getComponentType();
232        final int length = Array.getLength(array);
233        if (componentType.isPrimitive()) {
234            increaseByArraySize(arrayClass, length, getPrimitiveFieldSize(componentType));
235        } else {
236            increaseByArraySize(arrayClass, length, referenceSize);
237            // If we didn't use an ArrayElementsVisitor, we would be enqueueing every
238            // element of the array here instead. For large arrays, it would
239            // tremendously enlarge the queue. In essence, we're compressing it into
240            // a small command object instead. This is different than immediately
241            // visiting the elements, as their visiting is scheduled for the end of
242            // the current queue.
243            switch (length) {
244            case 0: {
245                break;
246            }
247            case 1: {
248                enqueue(Array.get(array, 0));
249                break;
250            }
251            default: {
252                enqueue(new ArrayElementsVisitor((Object[]) array));
253            }
254            }
255        }
256    }
257
258    private void increaseByArraySize(final Class<?> clazz, final int length, final long elementSize) {
259        increaseSize(clazz, roundTo(arrayHeaderSize + length * elementSize, objectPadding));
260    }
261
262    private static class ArrayElementsVisitor {
263        private final Object[] array;
264
265        ArrayElementsVisitor(final Object[] array) {
266            this.array = array;
267        }
268
269        public void visit(final ObjectSizeCalculator calc) {
270            for (final Object elem : array) {
271                if (elem != null) {
272                    calc.visit(elem);
273                }
274            }
275        }
276    }
277
278    void enqueue(final Object obj) {
279        if (obj != null) {
280            pending.addLast(obj);
281        }
282    }
283
284    void increaseSize(final Class<?> clazz, final long objectSize) {
285        ClassHistogramElement he = histogram.get(clazz);
286        if(he == null) {
287            he = new ClassHistogramElement(clazz);
288            histogram.put(clazz, he);
289        }
290        he.addInstance(objectSize);
291        size += objectSize;
292    }
293
294    static long roundTo(final long x, final int multiple) {
295        return ((x + multiple - 1) / multiple) * multiple;
296    }
297
298    private class ClassSizeInfo {
299        // Padded fields + header size
300        private final long objectSize;
301        // Only the fields size - used to calculate the subclasses' memory
302        // footprint.
303        private final long fieldsSize;
304        private final Field[] referenceFields;
305
306        public ClassSizeInfo(final Class<?> clazz) {
307            long newFieldsSize = 0;
308            final List<Field> newReferenceFields = new LinkedList<>();
309            for (final Field f : clazz.getDeclaredFields()) {
310                if (Modifier.isStatic(f.getModifiers())) {
311                    continue;
312                }
313                final Class<?> type = f.getType();
314                if (type.isPrimitive()) {
315                    newFieldsSize += getPrimitiveFieldSize(type);
316                } else {
317                    f.setAccessible(true);
318                    newReferenceFields.add(f);
319                    newFieldsSize += referenceSize;
320                }
321            }
322            final Class<?> superClass = clazz.getSuperclass();
323            if (superClass != null) {
324                final ClassSizeInfo superClassInfo = getClassSizeInfo(superClass);
325                newFieldsSize += roundTo(superClassInfo.fieldsSize, superclassFieldPadding);
326                newReferenceFields.addAll(Arrays.asList(superClassInfo.referenceFields));
327            }
328            this.fieldsSize = newFieldsSize;
329            this.objectSize = roundTo(objectHeaderSize + newFieldsSize, objectPadding);
330            this.referenceFields = newReferenceFields.toArray(
331                    new Field[newReferenceFields.size()]);
332        }
333
334        void visit(final Object obj, final ObjectSizeCalculator calc) {
335            calc.increaseSize(obj.getClass(), objectSize);
336            enqueueReferencedObjects(obj, calc);
337        }
338
339        public void enqueueReferencedObjects(final Object obj, final ObjectSizeCalculator calc) {
340            for (final Field f : referenceFields) {
341                try {
342                    calc.enqueue(f.get(obj));
343                } catch (final IllegalAccessException e) {
344                    final AssertionError ae = new AssertionError(
345                            "Unexpected denial of access to " + f);
346                    ae.initCause(e);
347                    throw ae;
348                }
349            }
350        }
351    }
352
353    private static long getPrimitiveFieldSize(final Class<?> type) {
354        if (type == boolean.class || type == byte.class) {
355            return 1;
356        }
357        if (type == char.class || type == short.class) {
358            return 2;
359        }
360        if (type == int.class || type == float.class) {
361            return 4;
362        }
363        if (type == long.class || type == double.class) {
364            return 8;
365        }
366        throw new AssertionError("Encountered unexpected primitive type " +
367                type.getName());
368    }
369
370    // ALERT: java.lang.management is not available in compact 1.  We need
371    // to use reflection to soft link test memory statistics.
372
373    static Class<?>  managementFactory    = null;
374    static Class<?>  memoryPoolMXBean     = null;
375    static Class<?>  memoryUsage          = null;
376    static Method    getMemoryPoolMXBeans = null;
377    static Method    getUsage             = null;
378    static Method    getMax               = null;
379    static {
380        try {
381            managementFactory    = Class.forName("java.lang.management.ManagementFactory");
382            memoryPoolMXBean     = Class.forName("java.lang.management.MemoryPoolMXBean");
383            memoryUsage          = Class.forName("java.lang.management.MemoryUsage");
384
385            getMemoryPoolMXBeans = managementFactory.getMethod("getMemoryPoolMXBeans");
386            getUsage             = memoryPoolMXBean.getMethod("getUsage");
387            getMax               = memoryUsage.getMethod("getMax");
388        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
389            // Pass thru, asserts when attempting to use.
390        }
391    }
392
393    /**
394     * Return the current memory usage
395     * @return current memory usage derived from system configuration
396     */
397    public static MemoryLayoutSpecification getEffectiveMemoryLayoutSpecification() {
398        final String vmName = System.getProperty("java.vm.name");
399        if (vmName == null || !vmName.startsWith("Java HotSpot(TM) ")) {
400            throw new UnsupportedOperationException(
401                    "ObjectSizeCalculator only supported on HotSpot VM");
402        }
403
404        final String dataModel = System.getProperty("sun.arch.data.model");
405        if ("32".equals(dataModel)) {
406            // Running with 32-bit data model
407            return new MemoryLayoutSpecification() {
408                @Override public int getArrayHeaderSize() {
409                    return 12;
410                }
411                @Override public int getObjectHeaderSize() {
412                    return 8;
413                }
414                @Override public int getObjectPadding() {
415                    return 8;
416                }
417                @Override public int getReferenceSize() {
418                    return 4;
419                }
420                @Override public int getSuperclassFieldPadding() {
421                    return 4;
422                }
423            };
424        } else if (!"64".equals(dataModel)) {
425            throw new UnsupportedOperationException("Unrecognized value '" +
426                    dataModel + "' of sun.arch.data.model system property");
427        }
428
429        final String strVmVersion = System.getProperty("java.vm.version");
430        final int vmVersion = Integer.parseInt(strVmVersion.substring(0,
431                strVmVersion.indexOf('.')));
432        if (vmVersion >= 17) {
433            long maxMemory = 0;
434
435            /*
436               See ALERT above.  The reflection code below duplicates the following
437               sequence, and avoids hard coding of java.lang.management.
438
439               for (MemoryPoolMXBean mp : ManagementFactory.getMemoryPoolMXBeans()) {
440                   maxMemory += mp.getUsage().getMax();
441               }
442            */
443
444            if (getMemoryPoolMXBeans == null) {
445                throw new AssertionError("java.lang.management not available in compact 1");
446            }
447
448            try {
449                final List<?> memoryPoolMXBeans = (List<?>)getMemoryPoolMXBeans.invoke(managementFactory);
450                for (final Object mp : memoryPoolMXBeans) {
451                    final Object usage = getUsage.invoke(mp);
452                    final Object max = getMax.invoke(usage);
453                    maxMemory += ((Long)max);
454                }
455            } catch (IllegalAccessException |
456                     IllegalArgumentException |
457                     InvocationTargetException ex) {
458                throw new AssertionError("java.lang.management not available in compact 1");
459            }
460
461            if (maxMemory < 30L * 1024 * 1024 * 1024) {
462                // HotSpot 17.0 and above use compressed OOPs below 30GB of RAM total
463                // for all memory pools (yes, including code cache).
464                return new MemoryLayoutSpecification() {
465                    @Override public int getArrayHeaderSize() {
466                        return 16;
467                    }
468                    @Override public int getObjectHeaderSize() {
469                        return 12;
470                    }
471                    @Override public int getObjectPadding() {
472                        return 8;
473                    }
474                    @Override public int getReferenceSize() {
475                        return 4;
476                    }
477                    @Override public int getSuperclassFieldPadding() {
478                        return 4;
479                    }
480                };
481            }
482        }
483
484        // In other cases, it's a 64-bit uncompressed OOPs object model
485        return new MemoryLayoutSpecification() {
486            @Override public int getArrayHeaderSize() {
487                return 24;
488            }
489            @Override public int getObjectHeaderSize() {
490                return 16;
491            }
492            @Override public int getObjectPadding() {
493                return 8;
494            }
495            @Override public int getReferenceSize() {
496                return 8;
497            }
498            @Override public int getSuperclassFieldPadding() {
499                return 8;
500            }
501        };
502    }
503}
504