起因

在一次我的项目问题剖析中,须要获取以后内存中的所有classloader,以及它们之间的父子关系。
借助于Java agent运行期动静attach到指标jvm的个性,以及通过Instrumentation对象来拿到所有加载的class,于是有了上面的工具。
参考:一波三折!记一次非堆内存透露(CXF+Jackson)的排查

理论应用中发现的问题

在编写agent的过程中,还发现了另一个问题,即agent.jar文件由指标jvm的SystemClassLoader加载,导致agent代码批改后,
再次attach到指标jvm上时,理论加载的依然是第一次attach的class内容,即便前一个agent.jar文件曾经被删除也没用。
为了解决这个问题,最初通过一个独立的、每次用完就抛弃的 agent classloader来实现每次attach时可执行不同的agent逻辑。

agent入口代码

public class AgentClassLoader extends URLClassLoader {public AgentClassLoader(URL[] urls, ClassLoader parent) {    super(urls, parent);}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {    synchronized (getClassLoadingLock(name)) {        //1.findLoadedClass        Class<?> c = findLoadedClass(name);        if (c == null) {            //如果找不到,会丢出异样            if (name.startsWith("toone.agent.runner.")) {                //2. runner从本地查找                c = findClass(name);            }            else {                //2. 其余走默认逻辑加载                c = super.loadClass(name, false);            }        }        //3.解析        if (resolve) {            resolveClass(c);        }        return c;    }}}public class AgentMain {@SuppressWarnings("unchecked")public static void agentmain(String agentArgument, Instrumentation instrumentation) throws Exception {    AgentClassLoader classLoader = null;    try {        //1.参数解析,提取logdir        Map<String, String> args = parseArgs(agentArgument);        String logdir = args.get("o");        logdir = URLDecoder.decode(logdir, "UTF-8");        //2.筹备上下文参数        Map<String, Object> context = new HashMap<String, Object>();        context.put("instrumentation", instrumentation);        context.put("logdir", new File(logdir));        //3.实例化agentrunner        URL agentPath = AgentMain.class.getProtectionDomain().getCodeSource().getLocation();        // 作用是应用独立的classloader来加载 真正的agent逻辑局部,并且在每次执行完该loader都是可回收的,        // 这样能够屡次attach到jvm上,执行不同的agent逻辑        classLoader = new AgentClassLoader(new URL[] { agentPath }, ClassLoader.getSystemClassLoader());        Class<?> agentRunnerClass = classLoader.loadClass("toone.agent.runner.AgentRunner");        Consumer<Map<String, Object>> agentRunner = (Consumer<Map<String, Object>>) agentRunnerClass.newInstance();        //4.执行        agentRunner.accept(context);    }    catch (Throwable e) {        //如果记录日志失败,那么输入到执行系统日志中        Logger.getLogger(AgentMain.class.getName()).log(Level.WARNING, "启动agentmain失败!", e);    }    finally {        if (classLoader != null) {            classLoader.close();        }    }}private static Map<String, String> parseArgs(String agentArgument) {    Map<String, String> args = new HashMap<String, String>();    if (agentArgument != null) {        String[] opts = agentArgument.split(",");        for (String opt : opts) {            String[] parts = opt.split("=");            String key = parts[0].trim();            String value = (parts.length <= 1) ? "" : parts[1].trim();            args.put(key, value);        }    }    return args;}}

agent runner入口

public class AgentRunner implements Consumer<Map<String, Object>> {@Overridepublic void accept(Map<String, Object> context) {    Instrumentation instrumentation = (Instrumentation) context.get("instrumentation");    File logdir = (File) context.get("logdir");    long now = System.currentTimeMillis();    String today = new SimpleDateFormat("yyyy-MM-dd").format(now);    String time = new SimpleDateFormat("HHmmss").format(now);    String fileNamePrefix = today + ".T" + time;    StringBuilder buf = new StringBuilder();    try {        //1.GC以前内存和class数量        logMemoryUsage(buf, ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage());        logClassesInfo(buf, ManagementFactory.getClassLoadingMXBean());        //2. GC        buf.append("\n开始GC...\n");        System.gc();        Thread.sleep(1000);        //3.GC当前的内存和class数量        logMemoryUsage(buf, ManagementFactory.getMemoryMXBean().getNonHeapMemoryUsage());        logClassesInfo(buf, ManagementFactory.getClassLoadingMXBean());        //6.记录class loader的信息        new ClassLoaderAnalyser().record(logdir, fileNamePrefix, instrumentation);    }    catch (Throwable e) {        logError(buf, "agent执行失败", e);    }    finally {        writeLogFile(buf, logdir, fileNamePrefix);    }}}

classloader分析器

public class ClassLoaderAnalyser {public void record(File logdir, String fileNamePrefix, Instrumentation instrumentation) {    Map<ClassLoader, ClassLoaderWraper> loaders = new HashMap<ClassLoader, ClassLoaderWraper>();    ClassLoaderWraper root = new ClassLoaderWraper(null);    try {        Class<?>[] loaded = instrumentation.getAllLoadedClasses();        for (Class<?> clazz : loaded) {            if (clazz == null) {                continue;            }            ClassLoader loader = clazz.getClassLoader();            //将以后loader增加到parent.children中            ClassLoaderWraper wraper = addToLoaderTree(root, loaders, loader);            wraper.count++;        }        StringBuilder buf = new StringBuilder();        //以 yml 格局记录树信息        traversalLoaderTree(buf, root);        writeLogFile(buf, logdir, fileNamePrefix);    }    catch (Throwable e) {        //如果记录日志失败,那么输入到执行系统日志中        Logger.getLogger(ClassLoaderAnalyser.class.getName()).log(Level.WARNING, "记录class loader信息失败!", e);    }}/**    * 将给定loader增加到parent.children中    *     * @param loaders 全副已知loader的汇合,在这外面寻找父节点    * @param loader 须要增加到loader tree上的节点    * @return loader的父节点    */private ClassLoaderWraper addToLoaderTree(ClassLoaderWraper root, Map<ClassLoader, ClassLoaderWraper> loaders,    ClassLoader loader) {    if (loader == null) {        return root;    }    ClassLoaderWraper self = loaders.get(loader);    if (self != null) {        //以后loader曾经在树中,就间接返回        return self;    }    //找到 parent    ClassLoaderWraper parent = addToLoaderTree(root, loaders, loader.getParent());    //把本人加进去    self = new ClassLoaderWraper(loader);    loaders.put(loader, self);    parent.children.add(self);    self.level = parent.level + 1;    return self;}/**    * 以 yml 格局记录树信息    *     * @param buf    * @param wraper    */private void traversalLoaderTree(StringBuilder buf, ClassLoaderWraper wraper) {    //每一层缩进两个空格    char[] prefix = new char[wraper.level * 2];    for (int i = 0; i < prefix.length; i++) {        prefix[i] = ' ';    }    buf.append(prefix).append(wraper.name).append(":\n");    buf.append(prefix).append("  COUNT: ").append(wraper.count).append("\n");    if (wraper.loader instanceof URLClassLoader) {        URLClassLoader urlLoader = (URLClassLoader) wraper.loader;        buf.append(prefix).append("  URLS:\n");        for (URL url : urlLoader.getURLs()) {            buf.append(prefix).append("  - ").append(url).append("\n");        }    }    if (!wraper.children.isEmpty()) {        Collections.sort(wraper.children);        for (ClassLoaderWraper child : wraper.children) {            traversalLoaderTree(buf, child);        }    }}private static class ClassLoaderWraper implements Comparable<ClassLoaderWraper> {    private ClassLoader loader;    private transient String name;    //该loader间接加载的class数量    private int count;    //不用每次计算level    private transient int level;    private List<ClassLoaderWraper> children = new ArrayList<ClassLoaderWraper>();    public ClassLoaderWraper(ClassLoader loader) {        this.loader = loader;        this.name = loader == null ? "ROOT" : loader.toString();        this.count++;    }    @Override    public int compareTo(ClassLoaderWraper an) {        int diff = level - an.level;        if (diff != 0) {            return level;        }        return name.compareTo(an.name);    }}}

agent installer 将agent attach到第3方jvm中

public class AgentInstaller {/**    * 将指定 agentJar 附加到 第三方 jvm过程上,日志输入到 logdir    *     * @param jvmpid    *        第三方jvm过程id    * @param agentJar    * @param logdir    *        输入日志目录    */public void install(String jvmpid, File agentJar, File logdir) {    StringBuilder buf = new StringBuilder();    logArgsInfo(buf, jvmpid, agentJar, logdir);    VirtualMachine vm = null;    boolean attachSuccess = false;    try {        vm = VirtualMachine.attach(jvmpid);        buf.append(vm.getSystemProperties()).append("\n");        attachSuccess = true;        //将输入目录作为参数传递给agent        String agentO = URLEncoder.encode(logdir.getAbsolutePath(), "UTF-8");        vm.loadAgent(agentJar.getAbsolutePath(), "o=" + agentO);    }    catch (Throwable e) {        if (!attachSuccess) {            logError(buf, "获取JVM过程失败!请查看 过程id指向的过程是否存在、或者是否JVM过程!pid=" + jvmpid + "\n", e);        }        else {            logError(buf, "将agent附加到JVM失败!请查看指定pid的JVM版本,目前只反对JRE 1.8.0_181!pid=" + jvmpid + "\n", e);        }    }    finally {        if (vm != null) {            try {                vm.detach();            }            catch (Throwable e) {                logError(buf, "vm.detach()执行失败!\n", e);            }        }        writeLogFile(buf, logdir);        System.out.println("执行实现。请查看日志:" + logdir);    }}}public class AgentInstallerFromCmd {public static void main(String[] args) {    try {        if (args == null || args.length < 2) {            args = loadArgsFromCmd();        }        String pid = args[0];        String logdir = args[1];        URL agentPath = AgentInstaller.class.getProtectionDomain().getCodeSource().getLocation();        new AgentInstaller().install(pid, new File(agentPath.toURI()), new File(logdir));    }    catch (Throwable e) {        System.out.println("agent装置失败!");        e.printStackTrace();    }}/**    * 从命令行录入 命令参数    *     * @return String[]同命令行参数内容, [0]=jvmpid, [1]=logdir    */private static String[] loadArgsFromCmd() {    List<VirtualMachineDescriptor> vms = VirtualMachine.list();    for (VirtualMachineDescriptor vmd : vms) {        System.out.println("jvm:" + vmd.id() + "\t" + vmd.displayName());    }    String[] args = new String[2];    Scanner scanner = new Scanner(System.in);    try {        System.out.println("请输出 指标Jvm过程id:");        args[0] = scanner.nextLine();        System.out.println("请输出 日志输入目录:");        args[1] = scanner.nextLine();        return args;    }    finally {        scanner.close();    }}}

manifest.mf文件内容

Manifest-Version: 1.0Bnd-LastModified: 1668494909100Bundle-ManifestVersion: 2Bundle-Name: toone-agentBundle-SymbolicName: toone-agentBundle-Version: 1.0DynamicImport-Package: *Agent-Class: toone.agent.loader.AgentMainMain-Class: toone.AgentInstallerFromCmd