/* * © Copyright IBM Corp. 2012-2013 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.ibm.commons.util.profiler; import java.io.PrintStream; import java.lang.instrument.Instrumentation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; import com.ibm.commons.util.StringUtil; import com.ibm.commons.util.TextUtil; /** * Estimate the size of an object in memory. This is just an estimation and is * used by profilers to give the developer a fair estimation of the memory. * * To get a better estimation a use of the Java 1.5 Instrumentation API can be used as well. * * @ibm-not-published */ public class MemoryInspector { private static final String ENTRY_TAG = "Object"; // $NON-NLS-1$ /** * Callback class used by the Inspector when inspecting an hierarchy of objects. * @author priand */ public interface Callback { public void begin(); public void end(long size); public Object startObject(Stack<Object> params, Object parent, Field parentField, Object object); public void endObject(Stack<Object> params, Object object, long objectSize, long childrenSize); } /** * Simple Stack Class. */ public interface Stack<T> { public boolean isEmpty(); public int size(); public T pop(); public void push(T o); public T get(); public T get(int idx); } @SuppressWarnings("unchecked") // $NON-NLS-1$ private static final class StackImpl<T> implements Stack<T> { private int count; private Object[] data; StackImpl() { data = new Object[128]; } public boolean isEmpty() { return count==0; } public int size() { return count; } public T pop() { return (T)data[--count]; } public void push(T o) { if(count==data.length) { Object[] nd = new Object[count+32]; System.arraycopy(data, 0, nd, 0, count); data = nd; } data[count++] = o; } public T get() { return (T)data[count-1]; } public T get(int idx) { return (T)data[count-idx-1]; } } /** * Callback implementation that retains all the entries in memory. * @author priand */ public static class CollectEntryCallBack implements Callback { public static class Entry { Entry parent; Entry next; Entry firstChild; Field parentField; Object object; long objectSize; long childrenSize; public Entry(Entry parent, Field parentField, Object object) { this.parent = parent; this.parentField = parentField; this.object = object; } public Entry getParent() { return parent; } public boolean isRoot() { return parent==null; } public Field getParentField() { return parentField; } public Object getObject() { return object; } public long getObjectSize() { return objectSize; } public long getChildrenSize() { return childrenSize; } public Entry getNext() { return next; } public Entry getFirstChild() { return firstChild; } void add(Entry child) { child.next = firstChild; firstChild = child; } } private Entry rootEntry; private Stack<Entry> stack = new StackImpl<Entry>(); public CollectEntryCallBack() { } public Entry getRootEntry() { return rootEntry; } public Stack<Entry> getEntryStack() { return stack; } public void begin() { this.rootEntry = createRootEntry(); stack.push(rootEntry); } protected Entry createRootEntry() { return new Entry(null,null,"<root>"); // $NON-NLS-1$ } public void end(long size) { this.rootEntry.childrenSize = size; } public Object startObject(Stack<Object> params, Object parent, Field parentField, Object object) { Entry e = createEntry(params,parent,parentField,object); if(isPersistent(params,parentField,object)) { stack.get().add(e); stack.push(e); } return e; } public void endObject(Stack<Object> params, Object object, long objectSize, long childrenSize) { Entry e = stack.get(); if(e.object==object) { e.objectSize = objectSize; e.childrenSize = childrenSize; stack.pop(); } } public Entry createEntry(Stack<Object> params, Object parent, Field parentField, Object object) { Entry e = new Entry((Entry)parent,parentField,object); return e; } public boolean isPersistent(Stack<Object> params, Field parentField, Object object) { return true; } } /** * Class that dumps a collection of entries. * * @author priand */ public static class CollectEntryDump { public enum Format { FORMAT_TEXT, FORMAT_XML } private CollectEntryCallBack callBack; private int initialLevel; private Format format; public CollectEntryDump(CollectEntryCallBack callBack, Format format) { this.callBack = callBack; this.format = format; } public int getInitialLevel() { return initialLevel; } public void setInitialLevel(int initialLevel) { this.initialLevel = initialLevel; } public CollectEntryCallBack getCallBack() { return callBack; } public void dump(PrintStream ps) { dump(ps,callBack.getRootEntry(),initialLevel); } protected void dump(PrintStream ps, CollectEntryCallBack.Entry entry, int level) { boolean p = shouldDump(ps,entry,level); if(p) { printEntryStart(ps, entry, level); level++; } for( CollectEntryCallBack.Entry c=entry.getFirstChild(); c!=null; c=c.getNext()) { dump(ps,c,level); } if(p) { level--; printEntryEnd(ps, entry, level); } } protected boolean shouldDump(PrintStream ps, CollectEntryCallBack.Entry entry, int level) { return true; } protected void printEntryStart(PrintStream ps, CollectEntryCallBack.Entry entry, int level) { printIndent(ps, level); StringBuilder b = new StringBuilder(); Object o = entry.getObject(); if(format==Format.FORMAT_TEXT) { String fn = getFieldName(entry); if(fn!=null) { b.append(fn); b.append(':'); } b.append(o.getClass().getSimpleName()); if(o.getClass().isArray()) { b.append('['); b.append(Integer.toString(Array.getLength(o))); b.append(']'); } b.append(", Size="); // $NON-NLS-1$ b.append(Long.toString(entry.getObjectSize())); b.append(", Total Size="); // $NON-NLS-1$ b.append(Long.toString(entry.getObjectSize()+entry.getChildrenSize())); appendObjectString(b, o); ps.println(b.toString()); } else if(format==Format.FORMAT_XML) { ps.print("<"); ps.print(ENTRY_TAG); if(!entry.isRoot()) { printXmlAttr(ps,"fieldName",getFieldName(entry)); // $NON-NLS-1$ String className = o.getClass().getSimpleName(); if(o.getClass().isArray()) { className += '[' + Integer.toString(Array.getLength(o)) + ']'; } printXmlAttr(ps,"class",className); // $NON-NLS-1$ printXmlAttr(ps,"size",Long.toString(entry.getObjectSize())); // $NON-NLS-1$ } printXmlAttr(ps,"totalSize",Long.toString(entry.getObjectSize()+entry.getChildrenSize())); // $NON-NLS-1$ appendObjectString(b, o); if(b.length()>0) { printXmlAttr(ps,"value",b.toString()); // $NON-NLS-1$ b.setLength(0); } if(entry.getFirstChild()==null) { ps.println("/>"); } else { ps.println(">"); } } } protected void printXmlAttr(PrintStream ps, String attrName, String attrValue) { if(StringUtil.isNotEmpty(attrValue)) { ps.print(" "); ps.print(attrName); ps.print("='"); ps.print(TextUtil.toXMLString(attrValue)); ps.print("'"); } } protected void printEntryEnd(PrintStream ps, CollectEntryCallBack.Entry entry, int level) { if(format==Format.FORMAT_XML) { if(entry.getFirstChild()!=null) { printIndent(ps, level); ps.print ("</"); ps.print (ENTRY_TAG); ps.println(">"); } } } protected String getFieldName(CollectEntryCallBack.Entry entry) { if(entry.getParentField()!=null) { return entry.getParentField().getName(); } return null; } protected void appendObjectString(StringBuilder b, Object o) { if(o instanceof String) { String s = format(o.toString()); if(b.length()>0) { b.append(", "); } b.append(s); return; } if(o instanceof Number) { String s = o.toString(); if(b.length()>0) { b.append(", "); } b.append(s); b.append(""); return; } if(o instanceof Map<?,?>) { if(b.length()>0) { b.append(", "); } b.append("count="); // $NON-NLS-1$ b.append(((Map<?,?>)o).size()); return; } if(o instanceof Map.Entry<?,?>) { if(b.length()>0) { b.append(", "); } b.append("key="); // $NON-NLS-1$ b.append(format(((Map.Entry<?,?>)o).getKey().toString())); return; } if(o instanceof List<?>) { if(b.length()>0) { b.append(", "); } b.append("count="); // $NON-NLS-1$ b.append(((List<?>)o).size()); return; } if(o instanceof Set<?>) { if(b.length()>0) { b.append(", "); } b.append("count="); // $NON-NLS-1$ b.append(((Set<?>)o).size()); return; } if(o.getClass().isPrimitive() ){ if(b.length()>0) { b.append(", "); } b.append(o.toString()); return; } } protected void printIndent(PrintStream ps, int level) { for(int i=0; i<level; i++) { ps.print(" "); } } protected String format(String s) { s = TextUtil.toJavaString(s,false); if(s.length()>96) { s = s.substring(0,96) + "..."; } return s; } } private Instrumentation instrumentation; private Map<Object, Integer> visited = new IdentityHashMap<Object, Integer>(); private IdentityHashMap<Class<?>, Field[]> fieldsCache = new IdentityHashMap<Class<?>, Field[]>(); public MemoryInspector() { this(new IdentityHashMap<Object, Integer>(), new IdentityHashMap<Class<?>, Field[]>()); } public MemoryInspector(Map<Object, Integer> visited, IdentityHashMap<Class<?>, Field[]> classCache) { this.instrumentation = JVMPIInterface.getInstrumentation(); this.fieldsCache = classCache; } public Instrumentation getInstrumentation() { return instrumentation; } public Map<Object, Integer> getVisited() { return visited; } public long inspect(Object object, Callback cb) throws IllegalAccessException { StackImpl<Object> params = new StackImpl<Object>(); long size = 0; cb.begin(); params.push(object); try { size = inspect(params, null, null, object, cb); return size; } finally { cb.end(size); params.pop(); } } protected long inspect(Stack<Object> params, Object parent, Field parentField, Object object, Callback cb) throws IllegalAccessException { // Look if the object should be skipped if(object == null) { return 0; } if(visited.containsKey(object)) { visited.put(object,Integer.valueOf(visited.get(object)+1)); return 0; } // Case of an intern if(isIntern(object)) { if(skipInterns(object)) { return 0; } } // Regular object long objectSize = 0; // Size of the object itself long childrenSize = 0; // Size of the contained children // Add it to the list of visited objects if(isStoreAsVisited(object)) { visited.put(object,Integer.valueOf(1)); } if(instrumentation!=null) { // Store the object size coming from the Instrumentation interface objectSize = instrumentation.getObjectSize(object); } Object current = cb.startObject(params, parent, parentField, object); params.push(object); try { // We calculate the size of this object by browsing its fields Class<?> clazz = object.getClass(); if(clazz.isArray()) { // Java Array Class<?> arrayClazz = clazz.getComponentType(); if(!arrayClazz.isPrimitive()) { int length = Array.getLength(object); for(int i=0; i<length; i++) { Object value = Array.get(object, i); if(value!=null) { if(isFieldValid(params, clazz, null, object, value)) { childrenSize += inspect(params, current, null, value, cb); } } } } } else { // Regular Object // We must browser the fields using getDeclaredFields as we need to access all the fields // including the private ones while (clazz != null) { Field[] fields = getFields(clazz); for (int i=0; i<fields.length; i++) { Field field = fields[i]; Object value = field.get(object); if(value!=null) { if(isFieldValid(params, clazz, field, object, value)) { childrenSize += inspect(params, current, field, value, cb); } } } clazz = clazz.getSuperclass(); } } return objectSize + childrenSize; } finally { cb.endObject(params, object, objectSize, childrenSize); params.pop(); } } private Field[] getFields(Class<?> clazz) { Field[] f = fieldsCache.get(clazz); if(f!=null) { return f; } ArrayList<Field> ff = new ArrayList<Field>(); Field[] fields = clazz.getDeclaredFields(); for (int i=0; i<fields.length; i++) { Field field = fields[i]; // Ignore static fields if (Modifier.isStatic(field.getModifiers())) { continue; } // If it is a primitive, ignore it if(field.getType().isPrimitive()) { continue; } // Ok, it is an object // Compute its size recursively if(!field.isAccessible()) { // Ensure we can access it field.setAccessible(true); } ff.add(field); } f = ff.toArray(new Field[ff.size()]); fieldsCache.put(clazz,f); return f; } protected boolean isStoreAsVisited(Object obj) { return true; } protected boolean isIntern(Object obj) { // Looks like comparable is a common interface to the following basic objects // Limit the checks here... if(obj instanceof Comparable) { if (obj instanceof Enum) { return true; } else if(obj==StringUtil.EMPTY_STRING || obj==StringUtil.EMPTY_STRING_ARRAY) { return true; } else if (obj instanceof String) { return (obj == ((String) obj).intern()); } else if (obj instanceof Boolean) { return (obj == Boolean.TRUE || obj == Boolean.FALSE); } else if (obj instanceof Integer) { return (obj == Integer.valueOf((Integer) obj)); } else if (obj instanceof Short) { return (obj == Short.valueOf((Short) obj)); } else if (obj instanceof Byte) { return (obj == Byte.valueOf((Byte) obj)); } else if (obj instanceof Long) { return (obj == Long.valueOf((Long) obj)); } else if (obj instanceof Character) { return (obj == Character.valueOf((Character) obj)); } } // Empty collections if(obj==Collections.EMPTY_LIST || obj==Collections.EMPTY_MAP || obj==Collections.EMPTY_SET) { return true; } return false; } protected boolean skipInterns(Object object) { return true; } protected boolean isFieldValid(Stack<Object> params, Class<?> fieldClass, Field field, Object obj, Object value) { return true; } }