changeset 1942:9db29a97e702

Add caching to heap analysis shell commands Backport of 6aea706e89e9 Reviewed-by: jerboaa, neugens, jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2015-December/017262.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-June/019793.html PR3043
author Andrew Azores <aazores@redhat.com>
date Fri, 18 Dec 2015 10:04:12 -0500
parents 1da17b854973
children bf03ca16dabf
files vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommand.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommand.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/HeapCommandHelper.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/LRUMap.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectCommandHelper.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectIdRequiredException.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommand.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/SearchTermRequiredException.java vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/locale/LocaleResources.java vm-heap-analysis/command/src/main/resources/com/redhat/thermostat/vm/heap/analysis/command/locale/strings.properties vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommandTest.java vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommandTest.java vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/HeapCommandHelperTest.java vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/LRUMapTest.java vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommandTest.java
diffstat 15 files changed, 673 insertions(+), 126 deletions(-) [+]
line wrap: on
line diff
--- a/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommand.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommand.java	Fri Dec 18 10:04:12 2015 -0500
@@ -39,6 +39,7 @@
 import java.util.Collection;
 import java.util.List;
 
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.ServiceReference;
@@ -58,12 +59,12 @@
 
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
-    private static final String HEAP_ID_ARG = "heapId";
-    private static final String LIMIT_ARG = "limit";
     private static final String HEADER_OBJECT_ID = translator.localize(LocaleResources.HEADER_OBJECT_ID).getContents();
     private static final String HEADER_TYPE = translator.localize(LocaleResources.HEADER_OBJECT_TYPE).getContents();
     private static final int DEFAULT_LIMIT = 10;
 
+    private static final String LIMIT_ARG = "limit";
+
     private BundleContext context;
 
     public FindObjectsCommand() {
@@ -88,30 +89,14 @@
     }
 
     private void run(CommandContext ctx, HeapDAO heapDAO) throws CommandException {
-        String heapId = ctx.getArguments().getArgument(HEAP_ID_ARG);
-        HeapInfo heapInfo = heapDAO.getHeapInfo(heapId);
-        if (heapInfo == null) {
-            throw new HeapNotFoundException(heapId);
-        }
-        HeapDump heapDump = heapDAO.getHeapDump(heapInfo);
-        if (heapDump == null) {
-            throw new HeapNotFoundException(heapId);
-        }
-
-        List<String> terms = ctx.getArguments().getNonOptionArguments();
-        if (terms.size() == 0) {
-            ctx.getConsole().getOutput().println(translator.localize(LocaleResources.SEARCH_TERM_REQUIRED).getContents());
-            return;
-        }
-        String searchTerm = terms.get(0);
-        if (searchTerm.trim().length() == 0) {
-            ctx.getConsole().getOutput().println(translator.localize(LocaleResources.SEARCH_TERM_REQUIRED).getContents());
-            return;
-        }
+        String searchTerm = getSearchTerm(ctx);
+        HeapCommandHelper helper = HeapCommandHelper.getHelper(ctx, heapDAO);
+        HeapDump heapDump = helper.getHeapDump();
 
         String limitArg = ctx.getArguments().getArgument(LIMIT_ARG);
         int limit = parseLimit(limitArg);
         Collection<String> results = heapDump.searchObjects(searchTerm, limit);
+
         TableRenderer table = new TableRenderer(2);
         table.printLine(HEADER_OBJECT_ID, HEADER_TYPE);
         for (String objectId : results) {
@@ -123,6 +108,23 @@
         table.render(ctx.getConsole().getOutput());
     }
 
+    private static String getSearchTerm(CommandContext ctx) throws SearchTermRequiredException {
+        assertSearchTermProvided(ctx);
+        List<String> terms = ctx.getArguments().getNonOptionArguments();
+        return terms.get(0);
+    }
+
+    private static void assertSearchTermProvided(CommandContext ctx) throws SearchTermRequiredException {
+        List<String> terms = ctx.getArguments().getNonOptionArguments();
+        if (terms.size() == 0) {
+            throw new SearchTermRequiredException();
+        }
+        String searchTerm = terms.get(0);
+        if (searchTerm.trim().length() == 0) {
+            throw new SearchTermRequiredException();
+        }
+    }
+
     private int parseLimit(String limitArg) throws CommandException {
         int limit = DEFAULT_LIMIT;
         if (limitArg != null) {
--- a/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommand.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommand.java	Fri Dec 18 10:04:12 2015 -0500
@@ -87,10 +87,10 @@
     }
 
     private void run(CommandContext ctx, HeapDAO heapDao) throws CommandException {
-        ObjectCommandHelper objCmdHelper = new ObjectCommandHelper(ctx, heapDao);
+        HeapCommandHelper objCmdHelper = HeapCommandHelper.getHelper(ctx, heapDao);
         HeapDump heapDump = objCmdHelper.getHeapDump();
         Snapshot snapshot = heapDump.getSnapshot();
-        JavaHeapObject obj = objCmdHelper.getJavaHeapObject();
+        JavaHeapObject obj = objCmdHelper.getJavaHeapObject(ctx);
         boolean findAll = ctx.getArguments().hasArgument(ALL_ARG);
         FindRoot findRoot = new FindRoot();
         Collection<HeapPath<JavaHeapObject>> pathsToRoot = findRoot.findShortestPathsToRoot(obj, findAll);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/HeapCommandHelper.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
+import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
+import com.sun.tools.hat.internal.model.JavaHeapObject;
+
+import java.lang.ref.SoftReference;
+import java.util.Map;
+import java.util.Objects;
+
+public class HeapCommandHelper {
+
+    public static final int MAXIMUM_CACHED_HELPERS = 5;
+
+    static final String HEAP_ID_ARG = "heapId";
+    static final String OBJECT_ID_ARG = "objectId";
+
+    protected static final Object getLock = new Object();
+    protected static final Map<HeapDumpIdentifier, SoftReference<HeapCommandHelper>> helperCache = new LRUMap<>(MAXIMUM_CACHED_HELPERS);
+
+    protected HeapDAO dao;
+    protected String heapId;
+    protected HeapDump heapDump;
+
+    // package private for testing only
+    HeapCommandHelper(HeapDumpIdentifier identifier) {
+        this.heapId = identifier.heapId;
+        this.dao = identifier.dao;
+    }
+
+    public static HeapCommandHelper getHelper(CommandContext ctx, HeapDAO heapDAO) {
+        synchronized (getLock) {
+            HeapDumpIdentifier heapDumpIdentifier = HeapDumpIdentifier.makeIdentifier(ctx, heapDAO);
+            HeapCommandHelper helper;
+            if (!helperCache.containsKey(heapDumpIdentifier)) {
+                helper = new HeapCommandHelper(heapDumpIdentifier);
+                helperCache.put(heapDumpIdentifier, new SoftReference<>(helper));
+            } else {
+                HeapCommandHelper _h = helperCache.get(heapDumpIdentifier).get();
+                if (_h == null) {
+                    helper = new HeapCommandHelper(heapDumpIdentifier);
+                    helperCache.put(heapDumpIdentifier, new SoftReference<>(helper));
+                } else {
+                    helper = _h;
+                }
+            }
+            return helper;
+        }
+    }
+
+    protected void loadHeapDump() throws HeapNotFoundException {
+        HeapInfo heapInfo = dao.getHeapInfo(heapId);
+        if (heapInfo == null) {
+            throw new HeapNotFoundException(heapId);
+        }
+        HeapDump heapDump = dao.getHeapDump(heapInfo);
+        if (heapDump == null) {
+            throw new HeapNotFoundException(heapId);
+        }
+        this.heapDump = heapDump;
+    }
+
+    public HeapDump getHeapDump() throws HeapNotFoundException {
+        if (heapDump == null) {
+            loadHeapDump();
+        }
+        return heapDump;
+    }
+
+    public JavaHeapObject getJavaHeapObject(CommandContext ctx) throws CommandException {
+        Arguments arguments = ctx.getArguments();
+        if (!arguments.hasArgument(OBJECT_ID_ARG)) {
+            throw new ObjectIdRequiredException();
+        }
+        HeapDump heapDump = getHeapDump();
+        String objectId = arguments.getArgument(OBJECT_ID_ARG);
+        JavaHeapObject obj = heapDump.findObject(objectId);
+        if (obj == null) {
+            throw new ObjectNotFoundException(objectId);
+        }
+        return obj;
+    }
+
+    protected static class HeapDumpIdentifier {
+
+        protected final String heapId;
+        protected final HeapDAO dao;
+
+        // package private for testing only
+        HeapDumpIdentifier(String heapId, HeapDAO dao) {
+            this.heapId = Objects.requireNonNull(heapId);
+            this.dao = Objects.requireNonNull(dao);
+        }
+
+        public static HeapDumpIdentifier makeIdentifier(CommandContext ctx, HeapDAO dao) {
+            String heapId = ctx.getArguments().getArgument(HEAP_ID_ARG);
+            return new HeapDumpIdentifier(heapId, dao);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            HeapDumpIdentifier heapDumpIdentifier = (HeapDumpIdentifier) o;
+
+            return heapId.equals(heapDumpIdentifier.heapId) && dao.equals(heapDumpIdentifier.dao);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = heapId.hashCode();
+            result = 31 * result + dao.hashCode();
+            return result;
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/LRUMap.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class LRUMap<K, V> extends LinkedHashMap<K, V> {
+
+    private final int maximumSize;
+
+    public LRUMap(int maximumSize) {
+        super(maximumSize + 1, 0.75f, true);
+        this.maximumSize = maximumSize;
+    }
+
+    @Override
+    public boolean removeEldestEntry(Map.Entry eldest) {
+        return size() > maximumSize;
+    }
+
+}
--- a/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectCommandHelper.java	Fri Jun 24 11:14:59 2016 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/*
- * Copyright 2012-2016 Red Hat, Inc.
- *
- * This file is part of Thermostat.
- *
- * Thermostat is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published
- * by the Free Software Foundation; either version 2, or (at your
- * option) any later version.
- *
- * Thermostat 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 for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Thermostat; see the file COPYING.  If not see
- * <http://www.gnu.org/licenses/>.
- *
- * Linking this code with other modules is making a combined work
- * based on this code.  Thus, the terms and conditions of the GNU
- * General Public License cover the whole combination.
- *
- * As a special exception, the copyright holders of this code give
- * you permission to link this code with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also
- * meet, for each linked independent module, the terms and conditions
- * of the license of that module.  An independent module is a module
- * which is not derived from or based on this code.  If you modify
- * this code, you may extend this exception to your version of the
- * library, but you are not obligated to do so.  If you do not wish
- * to do so, delete this exception statement from your version.
- */
-
-package com.redhat.thermostat.vm.heap.analysis.command.internal;
-
-import com.redhat.thermostat.common.cli.Arguments;
-import com.redhat.thermostat.common.cli.CommandContext;
-import com.redhat.thermostat.common.cli.CommandException;
-import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
-import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
-import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
-import com.sun.tools.hat.internal.model.JavaHeapObject;
-
-public class ObjectCommandHelper {
-
-    private static final String OBJECT_ID_ARG = "objectId";
-    private static final String HEAP_ID_ARG = "heapId";
-
-    private CommandContext ctx;
-    private HeapDAO dao;
-    private HeapDump heapDump;
-
-    public ObjectCommandHelper(CommandContext ctx, HeapDAO dao) {
-        this.ctx = ctx;
-        this.dao = dao;
-    }
-
-    public HeapDump getHeapDump() throws CommandException {
-        if (heapDump == null) {
-            loadHeapDump();
-        }
-        return heapDump;
-    }
-
-    private void loadHeapDump() throws CommandException {
-        Arguments args = ctx.getArguments();
-        String heapId = args.getArgument(HEAP_ID_ARG);
-        HeapInfo heapInfo = dao.getHeapInfo(heapId);
-        if (heapInfo == null) {
-            throw new HeapNotFoundException(heapId);
-        }
-        heapDump = dao.getHeapDump(heapInfo);
-    }
-
-    public JavaHeapObject getJavaHeapObject() throws CommandException {
-        HeapDump heapDump = getHeapDump();
-        Arguments args = ctx.getArguments();
-        String objectId = args.getArgument(OBJECT_ID_ARG);
-        JavaHeapObject obj = heapDump.findObject(objectId);
-        if (obj == null) {
-            throw new ObjectNotFoundException(objectId);
-        }
-        return obj;
-    }
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectIdRequiredException.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.heap.analysis.command.locale.LocaleResources;
+
+public class ObjectIdRequiredException extends CommandException {
+
+    private static final Translate<LocaleResources> tr = LocaleResources.createLocalizer();
+
+    ObjectIdRequiredException() {
+        super(tr.localize(LocaleResources.OBJECT_ID_REQUIRED));
+    }
+
+}
--- a/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommand.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommand.java	Fri Dec 18 10:04:12 2015 -0500
@@ -62,7 +62,6 @@
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
     private final BundleContext context;
-    private Snapshot snapshot;
 
     public ObjectInfoCommand() {
         this(FrameworkUtil.getBundle(ObjectInfoCommand.class).getBundleContext());
@@ -86,31 +85,30 @@
     }
 
     private void run(CommandContext ctx, HeapDAO heapDao) throws CommandException {
-        ObjectCommandHelper objCmdHelper = new ObjectCommandHelper(ctx, heapDao);
+        HeapCommandHelper objCmdHelper = HeapCommandHelper.getHelper(ctx, heapDao);
         HeapDump heapDump = objCmdHelper.getHeapDump();
-        snapshot = heapDump.getSnapshot();
-        JavaHeapObject obj = objCmdHelper.getJavaHeapObject();
+        Snapshot snapshot = heapDump.getSnapshot();
+        JavaHeapObject obj = objCmdHelper.getJavaHeapObject(ctx);
         TableRenderer table = new TableRenderer(2);
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_OBJECT_ID).getContents(), obj.getIdString());
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_TYPE).getContents(), obj.getClazz().getName());
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_SIZE).getContents(), String.valueOf(obj.getSize()) + " bytes");
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_HEAP_ALLOCATED).getContents(), String.valueOf(obj.isHeapAllocated()));
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_REFERENCES).getContents(), "");
-        printReferences(table, obj);
+        printReferences(table, obj, snapshot);
         table.printLine(translator.localize(LocaleResources.COMMAND_OBJECT_INFO_REFERRERS).getContents(), "");
-        printReferrers(table, obj);
+        printReferrers(table, obj, snapshot);
 
         PrintStream out = ctx.getConsole().getOutput();
         table.render(out);
-
     }
 
-    private void printReferences(final TableRenderer table, final JavaHeapObject obj) {
+    private void printReferences(final TableRenderer table, final JavaHeapObject obj, final Snapshot snapshot) {
         JavaHeapObjectVisitor v = new JavaHeapObjectVisitor() {
             
             @Override
             public void visit(JavaHeapObject ref) {
-                table.printLine("", describeReference(obj, ref) + " -> " + PrintObjectUtils.objectToString(ref));
+                table.printLine("", describeReference(obj, ref, snapshot) + " -> " + PrintObjectUtils.objectToString(ref));
             }
             
             @Override
@@ -126,15 +124,15 @@
         obj.visitReferencedObjects(v);
     }
 
-    private void printReferrers(TableRenderer table, JavaHeapObject obj) {
+    private void printReferrers(TableRenderer table, JavaHeapObject obj, Snapshot snapshot) {
         Enumeration<?> referrers = obj.getReferers();
         while (referrers.hasMoreElements()) {
             JavaHeapObject ref = (JavaHeapObject) referrers.nextElement();
-            table.printLine("", PrintObjectUtils.objectToString(ref) + " -> " + describeReference(ref, obj));
+            table.printLine("", PrintObjectUtils.objectToString(ref) + " -> " + describeReference(ref, obj, snapshot));
         }
     }
 
-    private String describeReference(JavaHeapObject from, JavaHeapObject to) {
+    private String describeReference(JavaHeapObject from, JavaHeapObject to, Snapshot snapshot) {
         return "[" + from.describeReferenceTo(to, snapshot) + "]";
     }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/internal/SearchTermRequiredException.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.heap.analysis.command.locale.LocaleResources;
+
+public class SearchTermRequiredException extends CommandException {
+
+    private static final Translate<LocaleResources> tr = LocaleResources.createLocalizer();
+
+    SearchTermRequiredException() {
+        super(tr.localize(LocaleResources.SEARCH_TERM_REQUIRED));
+    }
+
+}
--- a/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/locale/LocaleResources.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/main/java/com/redhat/thermostat/vm/heap/analysis/command/locale/LocaleResources.java	Fri Dec 18 10:04:12 2015 -0500
@@ -57,6 +57,7 @@
     HEAP_ID_NOT_FOUND,
     HEAP_ID_REQUIRED,
     SEARCH_TERM_REQUIRED,
+    OBJECT_ID_REQUIRED,
     HEAP_DUMP_ERROR,
     
     COMMAND_HEAP_DUMP_DONE,
--- a/vm-heap-analysis/command/src/main/resources/com/redhat/thermostat/vm/heap/analysis/command/locale/strings.properties	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/main/resources/com/redhat/thermostat/vm/heap/analysis/command/locale/strings.properties	Fri Dec 18 10:04:12 2015 -0500
@@ -16,6 +16,7 @@
 HEAP_ID_NOT_FOUND = Heap ID not found: {0}
 HEAP_ID_REQUIRED = Heap ID required
 SEARCH_TERM_REQUIRED = A search term is required
+OBJECT_ID_REQUIRED = Object ID required
 HEAP_DUMP_ERROR = Error dumping heap (agent: {0}, vm: {1})
 
 COMMAND_HEAP_DUMP_DONE = Done
@@ -29,7 +30,7 @@
 COMMAND_OBJECT_INFO_REFERENCES = References:
 COMMAND_OBJECT_INFO_REFERRERS = Referrers:
 
-OBJECT_NOT_FOUND_MESSAGE = Object not found: 
+OBJECT_NOT_FOUND_MESSAGE = Object not found: {0}
 ERROR_READING_HISTOGRAM_MESSAGE = Unexpected error while reading histogram for heap ID: {0}
 
 COMMAND_SAVE_HEAP_DUMP_SAVED_TO_FILE = Saved heap dump to {0}
--- a/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommandTest.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindObjectsCommandTest.java	Fri Dec 18 10:04:12 2015 -0500
@@ -45,6 +45,7 @@
 
 import java.util.Arrays;
 
+import com.redhat.thermostat.common.cli.Arguments;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -182,6 +183,7 @@
         TestCommandContextFactory factory = new TestCommandContextFactory();
         SimpleArguments args = new SimpleArguments();
         args.addArgument("heapId", INVALID_HEAP_ID);
+        args.addNonOptionArgument("fluff");
 
         try {
             cmd.run(factory.createContext(args));
@@ -190,5 +192,14 @@
             assertEquals("Heap ID not found: " + INVALID_HEAP_ID, e.getMessage());
         }
     }
+
+    @Test(expected = SearchTermRequiredException.class)
+    public void testSearchWithNoSearchTerm() throws CommandException {
+        TestCommandContextFactory factory = new TestCommandContextFactory();
+        SimpleArguments args = new SimpleArguments();
+        args.addArgument("heapId", HEAP_ID);
+
+        cmd.run(factory.createContext(args));
+    }
 }
 
--- a/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommandTest.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/FindRootCommandTest.java	Fri Dec 18 10:04:12 2015 -0500
@@ -265,6 +265,7 @@
 
     }
 
+    @Test
     public void testHeapNotFound() throws CommandException {
         TestCommandContextFactory factory = new TestCommandContextFactory();
         SimpleArguments args = new SimpleArguments();
@@ -279,6 +280,7 @@
         }
     }
 
+    @Test
     public void testObjectNotFound() throws CommandException {
         TestCommandContextFactory factory = new TestCommandContextFactory();
         SimpleArguments args = new SimpleArguments();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/HeapCommandHelperTest.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import com.redhat.thermostat.common.cli.Arguments;
+import com.redhat.thermostat.common.cli.CommandContext;
+import com.redhat.thermostat.common.cli.CommandException;
+import com.redhat.thermostat.common.cli.Console;
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
+import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
+import com.sun.tools.hat.internal.model.JavaHeapObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class HeapCommandHelperTest {
+
+    CommandContext ctx;
+    Arguments args;
+    HeapDAO dao;
+    HeapInfo heapInfo;
+    HeapDump heapDump;
+    JavaHeapObject heapObject;
+    Console console;
+    HeapCommandHelper helper;
+    public static final String HEAP_ID = "fooId";
+    public static final String OBJECT_ID = "objectId";
+
+    @Before
+    public void setup() {
+        ctx = mock(CommandContext.class);
+        args = mock(Arguments.class);
+        dao = mock(HeapDAO.class);
+        heapInfo = mock(HeapInfo.class);
+        heapDump = mock(HeapDump.class);
+        heapObject = mock(JavaHeapObject.class);
+        console = mock(Console.class);
+
+        when(ctx.getArguments()).thenReturn(args);
+        when(ctx.getConsole()).thenReturn(console);
+
+        when(args.hasArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(true);
+        when(args.getArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(HEAP_ID);
+        when(args.hasArgument(HeapCommandHelper.OBJECT_ID_ARG)).thenReturn(true);
+        when(args.getArgument(HeapCommandHelper.OBJECT_ID_ARG)).thenReturn(OBJECT_ID);
+
+        when(dao.getHeapInfo(HEAP_ID)).thenReturn(heapInfo);
+        when(dao.getHeapDump(heapInfo)).thenReturn(heapDump);
+
+        when(heapDump.findObject(OBJECT_ID)).thenReturn(heapObject);
+
+        helper = new HeapCommandHelper(new HeapCommandHelper.HeapDumpIdentifier(HEAP_ID, dao));
+    }
+
+    @Test
+    public void testGetHeapDumpReturnsExpectedDump() throws HeapNotFoundException {
+        HeapDump dump = helper.getHeapDump();
+        assertThat(dump, is(heapDump));
+    }
+
+    @Test
+    public void verifyHeapDumpsAreCached() throws HeapNotFoundException {
+        HeapDump dump = helper.getHeapDump();
+        verify(dao).getHeapInfo(HEAP_ID);
+        verify(dao).getHeapDump(heapInfo);
+        assertThat(dump, is(heapDump));
+        HeapDump dump2 = helper.getHeapDump();
+        assertThat(dump2, is(dump));
+        // verify no additional DAO accesses due to second getHeapDump call
+        verify(dao).getHeapInfo(HEAP_ID);
+        verify(dao).getHeapDump(heapInfo);
+    }
+
+    @Test
+    public void testGetJavaHeapObject() throws CommandException {
+        JavaHeapObject obj = helper.getJavaHeapObject(ctx);
+        assertThat(obj, is(heapObject));
+        verify(heapDump).findObject(OBJECT_ID);
+    }
+
+    @Test
+    public void verifyGetJavaHeapObjectIsCached() throws CommandException {
+        JavaHeapObject obj = helper.getJavaHeapObject(ctx);
+        verify(dao).getHeapInfo(HEAP_ID);
+        verify(dao).getHeapDump(heapInfo);
+        assertThat(obj, is(heapObject));
+        JavaHeapObject obj2 = helper.getJavaHeapObject(ctx);
+        assertThat(obj2, is(obj));
+        // verify no additional DAO accesses due to second getJavaHeapObject call
+        verify(dao).getHeapInfo(HEAP_ID);
+        verify(dao).getHeapDump(heapInfo);
+    }
+
+    @Test(expected = ObjectNotFoundException.class)
+    public void assertExceptionWhenGetJavaHeapObjectSuppliedInvalidId() throws CommandException {
+        when(args.getArgument(OBJECT_ID)).thenReturn("fluff");
+        helper.getJavaHeapObject(ctx);
+    }
+
+    @Test
+    public void testHelperCaching() throws HeapNotFoundException {
+        when(dao.getHeapInfo(any(String.class))).thenReturn(heapInfo);
+        when(dao.getHeapDump(any(HeapInfo.class))).thenReturn(heapDump);
+
+        List<HeapCommandHelper> helpers = new ArrayList<>(HeapCommandHelper.MAXIMUM_CACHED_HELPERS);
+        for (int i = 0; i < HeapCommandHelper.MAXIMUM_CACHED_HELPERS; i++) {
+            String heapId = "heapId-" + i;
+            when(args.getArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(heapId);
+            HeapCommandHelper helper = HeapCommandHelper.getHelper(ctx, dao);
+            helpers.add(helper);
+        }
+
+        // verify no duplicate helpers were returned to us
+        Set<HeapCommandHelper> helperSet = new HashSet<>(helpers);
+        assertThat(helpers.size(), is(equalTo(helperSet.size())));
+
+        // check each helper so far generated no dao accesses (we haven't accessed any dumps or heap objects yet)
+        verifyZeroInteractions(dao);
+
+        // use the helpers in order to ensure that they have cached their results and that the 1st is LRU
+        int accessCount = 0;
+        for (int i = 0; i < HeapCommandHelper.MAXIMUM_CACHED_HELPERS; i++) {
+            String heapId = "heapId-" + i;
+            when(args.getArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(heapId);
+            HeapCommandHelper helper = HeapCommandHelper.getHelper(ctx, dao);
+            helper.getHeapDump();
+            accessCount++;
+        }
+
+        verify(dao, times(accessCount)).getHeapInfo(anyString());
+        verify(dao, times(accessCount)).getHeapDump(any(HeapInfo.class));
+
+        // this should evict the old 1st helper which was LRU
+        String newHeapId = "heapId-" + HeapCommandHelper.MAXIMUM_CACHED_HELPERS;
+        when(args.getArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(newHeapId);
+        HeapCommandHelper newHelper = HeapCommandHelper.getHelper(ctx, dao);
+
+        newHelper.getHeapDump();
+        accessCount++;
+        verify(dao, times(accessCount)).getHeapInfo(anyString());
+        verify(dao, times(accessCount)).getHeapDump(any(HeapInfo.class));
+
+        // ask for the evicted helper again and ensure that this produces dao accesses the first time again (only),
+        // in other words ensure that it really was evicted but is subsequently re-cached
+        String evictedId = "heapId-0";
+        when(args.getArgument(HeapCommandHelper.HEAP_ID_ARG)).thenReturn(evictedId);
+        HeapCommandHelper evictedHelper = HeapCommandHelper.getHelper(ctx, dao);
+
+        evictedHelper.getHeapDump();
+        accessCount++;
+        verify(dao, times(accessCount)).getHeapInfo(anyString());
+        verify(dao, times(accessCount)).getHeapDump(any(HeapInfo.class));
+
+        assertThat(HeapCommandHelper.getHelper(ctx, dao), is(evictedHelper));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/LRUMapTest.java	Fri Dec 18 10:04:12 2015 -0500
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.command.internal;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class LRUMapTest {
+
+    LRUMap<String, String> map;
+
+    @Before
+    public void setup() {
+        map = new LRUMap<>(3);
+    }
+
+    @Test
+    public void testDoesNotExceedMaximumSize() {
+        map.put("a", "1");
+        map.put("b", "2");
+        map.put("c", "3");
+        map.put("d", "4");
+        assertThat(map.size(), is(3));
+    }
+
+    @Test
+    public void testIsInitiallyEmpty() {
+        assertThat(map.isEmpty(), is(true));
+    }
+
+    @Test
+    public void testEvictsLeastRecentlyUsed() {
+        map.put("a", "1");
+        map.put("b", "2");
+        map.put("c", "3");
+
+        map.get("b");
+        map.get("c");
+        // "a" is now LRU entry
+
+        map.put("d", "4");
+
+        Map<String, String> expected = new HashMap<>();
+        expected.put("b", "2");
+        expected.put("c", "3");
+        expected.put("d", "4");
+
+        assertThat(map, is(equalTo(expected)));
+    }
+
+}
--- a/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommandTest.java	Fri Jun 24 11:14:59 2016 -0400
+++ b/vm-heap-analysis/command/src/test/java/com/redhat/thermostat/vm/heap/analysis/command/internal/ObjectInfoCommandTest.java	Fri Dec 18 10:04:12 2015 -0500
@@ -177,6 +177,7 @@
 
     }
 
+    @Test
     public void testHeapNotFound() throws CommandException {
         StubBundleContext context = new StubBundleContext();
         context.registerService(HeapDAO.class, dao, null);
@@ -195,6 +196,7 @@
         }
     }
 
+    @Test
     public void testObjectNotFound() throws CommandException {
         StubBundleContext context = new StubBundleContext();
         context.registerService(HeapDAO.class, dao, null);