8203682: Add jcmd "VM.classloaders" command to print out class loader hierarchy, details
authorstuefe
Thu, 07 Jun 2018 12:23:46 +0200
changeset 50443 8e56de95ce10
parent 50442 ab967988f850
child 50444 db65921e9a9b
8203682: Add jcmd "VM.classloaders" command to print out class loader hierarchy, details Reviewed-by: coleenp, dholmes
src/hotspot/share/classfile/classLoaderHierarchyDCmd.cpp
src/hotspot/share/classfile/classLoaderHierarchyDCmd.hpp
src/hotspot/share/runtime/vm_operations.hpp
src/hotspot/share/services/diagnosticCommand.cpp
test/hotspot/jtreg/serviceability/dcmd/vm/ClassLoaderHierarchyTest.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/hotspot/share/classfile/classLoaderHierarchyDCmd.cpp	Thu Jun 07 12:23:46 2018 +0200
@@ -0,0 +1,456 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 SAP SE. 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.
+ *
+ * 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.
+ *
+ */
+
+#include "precompiled.hpp"
+
+#include "classfile/classLoaderData.inline.hpp"
+#include "classfile/classLoaderHierarchyDCmd.hpp"
+#include "memory/allocation.hpp"
+#include "memory/resourceArea.hpp"
+#include "runtime/safepoint.hpp"
+#include "utilities/globalDefinitions.hpp"
+#include "utilities/ostream.hpp"
+
+
+ClassLoaderHierarchyDCmd::ClassLoaderHierarchyDCmd(outputStream* output, bool heap)
+  : DCmdWithParser(output, heap)
+  , _show_classes("show-classes", "Print loaded classes.", "BOOLEAN", false, "false")
+  , _verbose("verbose", "Print detailed information.", "BOOLEAN", false, "false") {
+  _dcmdparser.add_dcmd_option(&_show_classes);
+  _dcmdparser.add_dcmd_option(&_verbose);
+}
+
+
+int ClassLoaderHierarchyDCmd::num_arguments() {
+  ResourceMark rm;
+  ClassLoaderHierarchyDCmd* dcmd = new ClassLoaderHierarchyDCmd(NULL, false);
+  if (dcmd != NULL) {
+    DCmdMark mark(dcmd);
+    return dcmd->_dcmdparser.num_arguments();
+  } else {
+    return 0;
+  }
+}
+
+// Helper class for drawing the branches to the left of a node.
+class BranchTracker : public StackObj {
+  //       "<x>"
+  //       " |---<y>"
+  //       " |    |
+  //       " |   <z>"
+  //       " |    |---<z1>
+  //       " |    |---<z2>
+  //       ^^^^^^^ ^^^
+  //        A       B
+
+  // Some terms for the graphics:
+  // - branch: vertical connection between a node's ancestor to a later sibling.
+  // - branchwork: (A) the string to print as a prefix at the start of each line, contains all branches.
+  // - twig (B): Length of the dashed line connecting a node to its branch.
+  // - branch spacing: how many spaces between branches are printed.
+
+public:
+
+  enum { max_depth = 64, twig_len = 2, branch_spacing = 5 };
+
+private:
+
+  char _branches[max_depth];
+  int _pos;
+
+public:
+  BranchTracker()
+    : _pos(0) {}
+
+  void push(bool has_branch) {
+    if (_pos < max_depth) {
+      _branches[_pos] = has_branch ? '|' : ' ';
+    }
+    _pos ++; // beyond max depth, omit branch drawing but do count on.
+  }
+
+  void pop() {
+    assert(_pos > 0, "must be");
+    _pos --;
+  }
+
+  void print(outputStream* st) {
+    for (int i = 0; i < _pos; i ++) {
+      st->print("%c%.*s", _branches[i], branch_spacing, "          ");
+    }
+  }
+
+  class Mark {
+    BranchTracker& _tr;
+  public:
+    Mark(BranchTracker& tr, bool has_branch_here)
+      : _tr(tr)  { _tr.push(has_branch_here); }
+    ~Mark() { _tr.pop(); }
+  };
+
+}; // end: BranchTracker
+
+struct LoadedClassInfo : public ResourceObj {
+public:
+  LoadedClassInfo* _next;
+  Klass* const _klass;
+  const ClassLoaderData* const _cld;
+
+  LoadedClassInfo(Klass* klass, const ClassLoaderData* cld)
+    : _klass(klass), _cld(cld) {}
+
+};
+
+class LoaderTreeNode : public ResourceObj {
+
+  // We walk the CLDG and, for each CLD which is non-anonymous, add
+  // a tree node. To add a node we need its parent node; if it itself
+  // does not exist yet, we add a preliminary node for it. This preliminary
+  // node just contains its loader oop; later, when encountering its CLD in
+  // our CLDG walk, we complete the missing information in this node.
+
+  const oop _loader_oop;
+  const ClassLoaderData* _cld;
+
+  LoaderTreeNode* _child;
+  LoaderTreeNode* _next;
+
+  LoadedClassInfo* _classes;
+  int _num_classes;
+
+  LoadedClassInfo* _anon_classes;
+  int _num_anon_classes;
+
+  void print_with_childs(outputStream* st, BranchTracker& branchtracker,
+      bool print_classes, bool verbose) const {
+
+    ResourceMark rm;
+
+    if (_cld == NULL) {
+      // Not sure how this could happen: we added a preliminary node for a parent but then never encountered
+      // its CLD?
+      return;
+    }
+
+    // Retrieve information.
+    const Klass* const loader_klass = _cld->class_loader_klass();
+    const Symbol* const loader_name = _cld->class_loader_name();
+
+    branchtracker.print(st);
+
+    // e.g. "+--- jdk.internal.reflect.DelegatingClassLoader"
+    st->print("+%.*s", BranchTracker::twig_len, "----------");
+    if (_cld->is_the_null_class_loader_data()) {
+      st->print(" <bootstrap>");
+    } else {
+      if (loader_name != NULL) {
+        st->print(" \"%s\",", loader_name->as_C_string());
+      }
+      st->print(" %s", loader_klass != NULL ? loader_klass->external_name() : "??");
+      st->print(" {" PTR_FORMAT "}", p2i(_loader_oop));
+    }
+    st->cr();
+
+    // Output following this node (node details and child nodes) - up to the next sibling node
+    // needs to be prefixed with "|" if there is a follow up sibling.
+    const bool have_sibling = _next != NULL;
+    BranchTracker::Mark trm(branchtracker, have_sibling);
+
+    {
+      // optional node details following this node needs to be prefixed with "|"
+      // if there are follow up child nodes.
+      const bool have_child = _child != NULL;
+      BranchTracker::Mark trm(branchtracker, have_child);
+
+      // Empty line
+      branchtracker.print(st);
+      st->cr();
+
+      const int indentation = 18;
+
+      if (verbose) {
+        branchtracker.print(st);
+        st->print_cr("%*s " PTR_FORMAT, indentation, "Loader Data:", p2i(_cld));
+        branchtracker.print(st);
+        st->print_cr("%*s " PTR_FORMAT, indentation, "Loader Klass:", p2i(loader_klass));
+
+        // Empty line
+        branchtracker.print(st);
+        st->cr();
+      }
+
+      if (print_classes) {
+
+        if (_classes != NULL) {
+          for (LoadedClassInfo* lci = _classes; lci; lci = lci->_next) {
+            branchtracker.print(st);
+            if (lci == _classes) { // first iteration
+              st->print("%*s ", indentation, "Classes:");
+            } else {
+              st->print("%*s ", indentation, "");
+            }
+            st->print("%s", lci->_klass->external_name());
+            st->cr();
+            // Non-anonymous classes should live in the primary CLD of its loader
+            assert(lci->_cld == _cld, "must be");
+          }
+          branchtracker.print(st);
+          st->print("%*s ", indentation, "");
+          st->print_cr("(%u class%s)", _num_classes, (_num_classes == 1) ? "" : "es");
+
+          // Empty line
+          branchtracker.print(st);
+          st->cr();
+        }
+
+        if (_anon_classes != NULL) {
+          for (LoadedClassInfo* lci = _anon_classes; lci; lci = lci->_next) {
+            branchtracker.print(st);
+            if (lci == _anon_classes) { // first iteration
+              st->print("%*s ", indentation, "Anonymous Classes:");
+            } else {
+              st->print("%*s ", indentation, "");
+            }
+            st->print("%s", lci->_klass->external_name());
+            // For anonymous classes, also print CLD if verbose. Should be a different one than the primary CLD.
+            assert(lci->_cld != _cld, "must be");
+            if (verbose) {
+              st->print("  (CLD: " PTR_FORMAT ")", p2i(lci->_cld));
+            }
+            st->cr();
+          }
+          branchtracker.print(st);
+          st->print("%*s ", indentation, "");
+          st->print_cr("(%u anonymous class%s)", _num_anon_classes, (_num_anon_classes == 1) ? "" : "es");
+
+          // Empty line
+          branchtracker.print(st);
+          st->cr();
+        }
+
+      } // end: print_classes
+
+    } // Pop branchtracker mark
+
+    // Print children, recursively
+    LoaderTreeNode* c = _child;
+    while (c != NULL) {
+      c->print_with_childs(st, branchtracker, print_classes, verbose);
+      c = c->_next;
+    }
+
+  }
+
+public:
+
+  LoaderTreeNode(const oop loader_oop)
+    : _loader_oop(loader_oop), _cld(NULL)
+    , _child(NULL), _next(NULL)
+    , _classes(NULL), _anon_classes(NULL)
+    , _num_classes(0), _num_anon_classes(0) {}
+
+  void set_cld(const ClassLoaderData* cld) {
+    _cld = cld;
+  }
+
+  void add_child(LoaderTreeNode* info) {
+    info->_next = _child;
+    _child = info;
+  }
+
+  void add_sibling(LoaderTreeNode* info) {
+    assert(info->_next == NULL, "must be");
+    info->_next = _next;
+    _next = info;
+  }
+
+  void add_classes(LoadedClassInfo* first_class, int num_classes, bool anonymous) {
+    LoadedClassInfo** p_list_to_add_to = anonymous ? &_anon_classes : &_classes;
+    // Search tail.
+    while ((*p_list_to_add_to) != NULL) {
+      p_list_to_add_to = &(*p_list_to_add_to)->_next;
+    }
+    *p_list_to_add_to = first_class;
+    if (anonymous) {
+      _num_anon_classes += num_classes;
+    } else {
+      _num_classes += num_classes;
+    }
+  }
+
+  const ClassLoaderData* cld() const {
+    return _cld;
+  }
+
+  const oop loader_oop() const {
+    return _loader_oop;
+  }
+
+  LoaderTreeNode* find(const oop loader_oop) {
+    LoaderTreeNode* result = NULL;
+    if (_loader_oop == loader_oop) {
+      result = this;
+    } else {
+      LoaderTreeNode* c = _child;
+      while (c != NULL && result == NULL) {
+        result = c->find(loader_oop);
+        c = c->_next;
+      }
+    }
+    return result;
+  }
+
+  void print_with_childs(outputStream* st, bool print_classes, bool print_add_info) const {
+    BranchTracker bwt;
+    print_with_childs(st, bwt, print_classes, print_add_info);
+  }
+
+};
+
+class LoadedClassCollectClosure : public KlassClosure {
+public:
+  LoadedClassInfo* _list;
+  const ClassLoaderData* _cld;
+  int _num_classes;
+  LoadedClassCollectClosure(const ClassLoaderData* cld)
+    : _list(NULL), _cld(cld), _num_classes(0) {}
+  void do_klass(Klass* k) {
+    LoadedClassInfo* lki = new LoadedClassInfo(k, _cld);
+    lki->_next = _list;
+    _list = lki;
+    _num_classes ++;
+  }
+};
+
+class LoaderInfoScanClosure : public CLDClosure {
+
+  const bool _print_classes;
+  const bool _verbose;
+  LoaderTreeNode* _root;
+
+  static void fill_in_classes(LoaderTreeNode* info, const ClassLoaderData* cld) {
+    assert(info != NULL && cld != NULL, "must be");
+    LoadedClassCollectClosure lccc(cld);
+    const_cast<ClassLoaderData*>(cld)->classes_do(&lccc);
+    if (lccc._num_classes > 0) {
+      info->add_classes(lccc._list, lccc._num_classes, cld->is_anonymous());
+    }
+  }
+
+  LoaderTreeNode* find_node_or_add_empty_node(oop loader_oop) {
+
+    assert(_root != NULL, "root node must exist");
+
+    if (loader_oop == NULL) {
+      return _root;
+    }
+
+    // Check if a node for this oop already exists.
+    LoaderTreeNode* info = _root->find(loader_oop);
+
+    if (info == NULL) {
+      // It does not. Create a node.
+      info = new LoaderTreeNode(loader_oop);
+
+      // Add it to tree.
+      LoaderTreeNode* parent_info = NULL;
+
+      // Recursively add parent nodes if needed.
+      const oop parent_oop = java_lang_ClassLoader::parent(loader_oop);
+      if (parent_oop == NULL) {
+        parent_info = _root;
+      } else {
+        parent_info = find_node_or_add_empty_node(parent_oop);
+      }
+      assert(parent_info != NULL, "must be");
+
+      parent_info->add_child(info);
+    }
+    return info;
+  }
+
+
+public:
+  LoaderInfoScanClosure(bool print_classes, bool verbose)
+    : _print_classes(print_classes), _verbose(verbose), _root(NULL) {
+    _root = new LoaderTreeNode(NULL);
+  }
+
+  void print_results(outputStream* st) const {
+    _root->print_with_childs(st, _print_classes, _verbose);
+  }
+
+  void do_cld (ClassLoaderData* cld) {
+
+    // We do not display unloading loaders, for now.
+    if (cld->is_unloading()) {
+      return;
+    }
+
+    const oop loader_oop = cld->class_loader();
+
+    LoaderTreeNode* info = find_node_or_add_empty_node(loader_oop);
+    assert(info != NULL, "must be");
+
+    // Update CLD in node, but only if this is the primary CLD for this loader.
+    if (cld->is_anonymous() == false) {
+      assert(info->cld() == NULL, "there should be only one primary CLD per loader");
+      info->set_cld(cld);
+    }
+
+    // Add classes.
+    fill_in_classes(info, cld);
+  }
+
+};
+
+
+class ClassLoaderHierarchyVMOperation : public VM_Operation {
+  outputStream* const _out;
+  const bool _show_classes;
+  const bool _verbose;
+public:
+  ClassLoaderHierarchyVMOperation(outputStream* out, bool show_classes, bool verbose) :
+    _out(out), _show_classes(show_classes), _verbose(verbose)
+  {}
+
+  VMOp_Type type() const {
+    return VMOp_ClassLoaderHierarchyOperation;
+  }
+
+  void doit() {
+    assert(SafepointSynchronize::is_at_safepoint(), "must be a safepoint");
+    ResourceMark rm;
+    LoaderInfoScanClosure cl (_show_classes, _verbose);
+    ClassLoaderDataGraph::cld_do(&cl);
+    cl.print_results(_out);
+  }
+};
+
+// This command needs to be executed at a safepoint.
+void ClassLoaderHierarchyDCmd::execute(DCmdSource source, TRAPS) {
+  ClassLoaderHierarchyVMOperation op(output(), _show_classes.value(), _verbose.value());
+  VMThread::execute(&op);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/hotspot/share/classfile/classLoaderHierarchyDCmd.hpp	Thu Jun 07 12:23:46 2018 +0200
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018 SAP SE. 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.
+ *
+ * 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.
+ *
+ */
+
+#ifndef HOTSPOT_SHARE_CLASSFILE_CLASSLOADERHIERARCHYDCMD_HPP_
+#define HOTSPOT_SHARE_CLASSFILE_CLASSLOADERHIERARCHYDCMD_HPP_
+
+#include "services/diagnosticCommand.hpp"
+
+class ClassLoaderHierarchyDCmd: public DCmdWithParser {
+  DCmdArgument<bool> _show_classes;
+  DCmdArgument<bool> _verbose;
+public:
+
+  ClassLoaderHierarchyDCmd(outputStream* output, bool heap);
+
+  static const char* name() {
+    return "VM.classloaders";
+  }
+
+  static const char* description() {
+    return "Prints classloader hierarchy.";
+  }
+  static const char* impact() {
+      return "Medium: Depends on number of class loaders and classes loaded.";
+  }
+  static const JavaPermission permission() {
+    JavaPermission p = {"java.lang.management.ManagementPermission",
+                        "monitor", NULL};
+    return p;
+  }
+  static int num_arguments();
+  virtual void execute(DCmdSource source, TRAPS);
+
+};
+
+#endif /* HOTSPOT_SHARE_CLASSFILE_CLASSLOADERHIERARCHYDCMD_HPP_ */
--- a/src/hotspot/share/runtime/vm_operations.hpp	Thu Jun 07 11:20:55 2018 +0200
+++ b/src/hotspot/share/runtime/vm_operations.hpp	Thu Jun 07 12:23:46 2018 +0200
@@ -103,6 +103,7 @@
   template(RotateGCLog)                           \
   template(WhiteBoxOperation)                     \
   template(ClassLoaderStatsOperation)             \
+  template(ClassLoaderHierarchyOperation)         \
   template(DumpHashtable)                         \
   template(DumpTouchedMethods)                    \
   template(MarkActiveNMethods)                    \
--- a/src/hotspot/share/services/diagnosticCommand.cpp	Thu Jun 07 11:20:55 2018 +0200
+++ b/src/hotspot/share/services/diagnosticCommand.cpp	Thu Jun 07 12:23:46 2018 +0200
@@ -24,6 +24,7 @@
 
 #include "precompiled.hpp"
 #include "jvm.h"
+#include "classfile/classLoaderHierarchyDCmd.hpp"
 #include "classfile/classLoaderStats.hpp"
 #include "classfile/compactHashtable.hpp"
 #include "compiler/compileBroker.hpp"
@@ -101,6 +102,7 @@
 #endif // INCLUDE_JVMTI
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ThreadDumpDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ClassLoaderStatsDCmd>(full_export, true, false));
+  DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ClassLoaderHierarchyDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CompileQueueDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeListDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeCacheDCmd>(full_export, true, false));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/hotspot/jtreg/serviceability/dcmd/vm/ClassLoaderHierarchyTest.java	Thu Jun 07 12:23:46 2018 +0200
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, SAP SE. 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.
+ *
+ * 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.
+ */
+
+/*
+ * @test
+ * @summary Test of diagnostic command VM.classloaders
+ * @library /test/lib
+ * @modules java.base/jdk.internal.misc
+ *          java.compiler
+ *          java.management
+ *          jdk.internal.jvmstat/sun.jvmstat.monitor
+ * @run testng ClassLoaderHierarchyTest
+ */
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import jdk.test.lib.process.OutputAnalyzer;
+import jdk.test.lib.dcmd.CommandExecutor;
+import jdk.test.lib.dcmd.JMXExecutor;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+public class ClassLoaderHierarchyTest {
+
+//+-- <bootstrap>
+//      |
+//      +-- "platform", jdk.internal.loader.ClassLoaders$PlatformClassLoader
+//      |     |
+//      |     +-- "app", jdk.internal.loader.ClassLoaders$AppClassLoader
+//      |
+//      +-- jdk.internal.reflect.DelegatingClassLoader
+//      |
+//      +-- "Kevin", ClassLoaderHierarchyTest$TestClassLoader
+//      |
+//      +-- ClassLoaderHierarchyTest$TestClassLoader
+//            |
+//            +-- "Bill", ClassLoaderHierarchyTest$TestClassLoader
+
+    public void run(CommandExecutor executor) throws ClassNotFoundException {
+
+        ClassLoader unnamed_cl = new TestClassLoader(null, null);
+        Class<?> c1 = Class.forName("TestClass2", true, unnamed_cl);
+        if (c1.getClassLoader() != unnamed_cl) {
+            Assert.fail("TestClass defined by wrong classloader: " + c1.getClassLoader());
+        }
+
+        ClassLoader named_cl = new TestClassLoader("Kevin", null);
+        Class<?> c2 = Class.forName("TestClass2", true, named_cl);
+        if (c2.getClassLoader() != named_cl) {
+            Assert.fail("TestClass defined by wrong classloader: " + c2.getClassLoader());
+        }
+
+        ClassLoader named_child_cl = new TestClassLoader("Bill", unnamed_cl);
+        Class<?> c3 = Class.forName("TestClass2", true, named_child_cl);
+        if (c3.getClassLoader() != named_child_cl) {
+            Assert.fail("TestClass defined by wrong classloader: " + c3.getClassLoader());
+        }
+
+        // First test: simple output, no classes displayed
+        OutputAnalyzer output = executor.execute("VM.classloaders");
+        output.shouldContain("<bootstrap>");
+        output.shouldMatch(".*TestClassLoader");
+        output.shouldMatch("Kevin.*TestClassLoader");
+        output.shouldMatch("Bill.*TestClassLoader");
+
+        // Second test: print with classes.
+        output = executor.execute("VM.classloaders show-classes");
+        output.shouldContain("<bootstrap>");
+        output.shouldContain("java.lang.Object");
+        output.shouldMatch(".*TestClassLoader");
+        output.shouldMatch("Kevin.*TestClassLoader");
+        output.shouldMatch("Bill.*TestClassLoader");
+        output.shouldContain("TestClass2");
+    }
+
+    static class TestClassLoader extends ClassLoader {
+
+        public TestClassLoader() {
+            super();
+        }
+
+        public TestClassLoader(String name, ClassLoader parent) {
+            super(name, parent);
+        }
+
+        public static final String CLASS_NAME = "TestClass2";
+
+        static ByteBuffer readClassFile(String name)
+        {
+            File f = new File(System.getProperty("test.classes", "."),
+                              name);
+            try (FileInputStream fin = new FileInputStream(f);
+                 FileChannel fc = fin.getChannel())
+            {
+                return fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
+            } catch (IOException e) {
+                Assert.fail("Can't open file: " + name, e);
+            }
+
+            /* Will not reach here as Assert.fail() throws exception */
+            return null;
+        }
+
+        protected Class<?> loadClass(String name, boolean resolve)
+            throws ClassNotFoundException
+        {
+            Class<?> c;
+            if (!CLASS_NAME.equals(name)) {
+                c = super.loadClass(name, resolve);
+            } else {
+                // should not delegate to the system class loader
+                c = findClass(name);
+                if (resolve) {
+                    resolveClass(c);
+                }
+            }
+            return c;
+        }
+
+        protected Class<?> findClass(String name)
+            throws ClassNotFoundException
+        {
+            if (!CLASS_NAME.equals(name)) {
+                throw new ClassNotFoundException("Unexpected class: " + name);
+            }
+            return defineClass(name, readClassFile(name + ".class"), null);
+        }
+
+    }
+
+    @Test
+    public void jmx() throws ClassNotFoundException {
+        run(new JMXExecutor());
+    }
+
+}
+
+class TestClass2 {
+    static {
+        Runnable r = () -> System.out.println("Hello");
+        r.run();
+    }
+}
+