2005-01-23

A Java equivalent of heap(1)

I read the claim recently that a lot of heap is wasted on zero-length arrays of primitive types. I was interested to know whether this was true, so I modified a program I've been meaning to mention for a while so that it could check whether this sweeping claim was true. (Update 2007-03-17: If you just want to use something like this, upgrade yourself to Java 6 or later and use "jmap -histo ". The private API below has changed a bit by Java 7, too.)

Some interesting stuff appeared in Java 1.5's tools.jar and sa-idl.jar (the latter may be sa-jdi.jar or simply part of tools.jar depending on your particular JVM/platform). I haven't seen it documented anywhere; unsurprisingly, since it's undocumented API. What I mean is, I haven't even seen it mentioned anywhere. And it's too cool not to mention. In case you didn't know, jps(1) can be written in a few lines of Java. Here's roughly what it might look like:

MonitoredHost localhost = MonitoredHost.getMonitoredHost("localhost");
Set ids = new TreeSet(localhost.activeVms());
for (Object id : ids) {
MonitoredVm vm = localhost.getMonitoredVm(new VmIdentifier("//" + id));
String name = MonitoredVmUtil.mainClass(vm, false);
System.err.println(id + "\t" + name);
}


More interesting than that, though, is that you can iterate over all the oops [Ordinary Object Pointers] on the Java heap. Your visitor's prologue method is invoked once at the beginning, then doObj is invoked once for every oop, and finally the epilogue method is invoked once when it's all over.

Here's a short program to do a job similar to Apple's heap(1) (though the output is slightly simpler, for the benefit of other programs):

import java.util.*;

// Found in "sa-jdi.jar"...
import sun.jvm.hotspot.bugspot.*;
import sun.jvm.hotspot.oops.*;
import sun.jvm.hotspot.runtime.*;

// Found in "tools.jar"...
import sun.jvmstat.monitor.*;

public class HeapDump implements HeapVisitor {
private BugSpotAgent agent;
private HashMap<Klass, KlassInfo> map;
private int objectCount;

private int zeroLengthPrimitiveArrayCount;

private long startTime;
private long endTime;

private HeapDump(int pid) {
agent = new BugSpotAgent();
agent.attach(pid);

VM vm = VM.getVM();
System.err.println("Running on: " + vm.getOS() + "/" + vm.getCPU());
System.err.println("VM release: " + vm.getVMRelease());
System.err.println("VM internal info: " + vm.getVMInternalInfo());

ObjectHeap heap = vm.getObjectHeap();
System.err.println("ObjectHeap = " + heap);

heap.iterate(this);

long duration = endTime - startTime;
System.err.println("Time taken: " + duration + "ms");
System.err.println("Speed: " + objectCount/duration + " objects/ms");
System.err.println("Objects on heap: " + objectCount);
System.err.println("Unique classes: " + map.size());

agent.detach();
}

public void prologue(long x) {
map = new HashMap<Klass, KlassInfo>();
objectCount = 0;
startTime = System.currentTimeMillis();
}

public void doObj(Oop oop) {
++objectCount;
Klass klass = oop.getKlass();
KlassInfo info = map.get(klass);
if (info == null) {
info = new KlassInfo(klass);
map.put(klass, info);
}
++info.instanceCount;
info.totalByteCount += oop.getObjectSize();
if (oop instanceof TypeArray) {
long arrayLength = ((TypeArray) oop).getLength();
if (arrayLength == 0) {
System.out.println(klass.signature() +
" array of length " + arrayLength +
" (" + oop.getObjectSize() + " bytes)");
++zeroLengthPrimitiveArrayCount;
}
}
}

public void epilogue() {
endTime = System.currentTimeMillis();
dumpPerKlassInfo();
}

private void dumpPerKlassInfo() {
long overallByteCount = 0;
for (KlassInfo info : map.values()) {
overallByteCount += info.totalByteCount;
System.out.println(info);
}
System.out.println("Overall bytes on heap: " + overallByteCount);
System.out.println("Zero-length primitive arrays: " +
zeroLengthPrimitiveArrayCount);
}

public static void main(String[] args) throws Throwable {
if (args.length == 0) {
System.err.println("usage: HeapDump <pid>");
MonitoredHost localhost = MonitoredHost.getMonitoredHost("localhost");
Set ids = new TreeSet(localhost.activeVms());
for (Object id : ids) {
MonitoredVm vm = localhost.getMonitoredVm(new VmIdentifier("//" + id));
String name = MonitoredVmUtil.mainClass(vm, false);
System.err.println(id + "\t" + name);
}
System.exit(0);
}
for (String pid : args) {
new HeapDump(Integer.parseInt(pid));
}
}

private static class KlassInfo {
public Klass klass;

public int instanceCount = 0;
public int totalByteCount = 0;

public KlassInfo(Klass klass) {
this.klass = klass;
}

public String toString() {
return instanceCount + "\t" + totalByteCount + "B\t" + name();
}

private String name() {
Symbol symbol = klass.getName();
return (symbol != null) ? symbol.asString() : "?";
}
}
}

The bad news is that it's slow. Whereas Apple's heap(1) returns almost instantaneously, when pointed at my editor (which had been running for a week at the time), HeapDump took about 40s on a fast Linux/x86 box. It manages about 30 objects/ms.

Here's the tail of the output when pointed to a smaller program, a simple dual time zone clock:

807 19368B java/awt/Rectangle
1 16B sun/font/CMap$NullCMapClass
27 1512B sun/nio/cs/StreamEncoder$CharsetSE
17 544B sun/font/FontStrikeDesc
1 24B java/lang/NullPointerException
31 744B java/awt/datatransfer/DataFlavor
9 288B sun/reflect/UnsafeQualifiedStaticIntegerFieldAccessorImpl
2 48B java/util/regex/Pattern$TreeInfo
1 8B sun/net/DefaultProgressMeteringPolicy
2 32B java/awt/ImageCapabilities
1 32B [Ljava/util/Map;
1 32B sun/awt/image/OffScreenImageSource
Overall bytes on heap: 18265616
Zero-length primitive arrays: 534
Time taken: 10468ms
Speed: 26 objects/ms
Objects on heap: 275170
Unique classes: 776

For that Java VM, a zero-length array of any primitive type takes 16 bytes. So that's just over 8KiB out of 18MiB. Even for my editor, only a few KiB were used by zero-length arrays of primitive types.

By the way, pointed at Keynote, Apple's heap(1) produces output like this (excerpted):

hydrogen:~$ time heap 2273
Process 2273: 1 zones
Zone DefaultMallocZone_0x400000: Overall size: 31340KB; 176007 nodes malloced for 21929KB (69% of capacity); largest unused: [0x01b0d400-5099KB]
All zones: 176007 nodes malloced - 21929KB

. . .

Found 3067 ObjC classes in process 2273

. . .

ADVersionHistory = 1 (16 bytes)
PBExQuickTimeMovieData = 1 (16 bytes)
_NSDrawingThreadData = 1 (16 bytes)


real 0m2.760s
user 0m1.720s
sys 0m0.380s

So heap(1) is ten times faster for a not incomparable process. I don't know if this is because heap(1) itself does something particularly clever, or maybe the Objective-C runtime does some book-keeping to make this easy, or maybe sun.jvm.hotspot.oops just isn't ready for the prime time yet. Which would explain why no-one seems to have mentioned it.

Bummer though; I was hoping to write something like Apple's ObjectAlloc, but this is way too slow for that.