乐趣区

关于java:分析jvm内存中classloader的关系java-agent使用实例

起因

在一次我的项目问题剖析中,须要获取以后内存中的所有 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);
}

@Override
protected 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>> {
@Override
public 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.0
Bnd-LastModified: 1668494909100
Bundle-ManifestVersion: 2
Bundle-Name: toone-agent
Bundle-SymbolicName: toone-agent
Bundle-Version: 1.0
DynamicImport-Package: *
Agent-Class: toone.agent.loader.AgentMain
Main-Class: toone.AgentInstallerFromCmd
退出移动版