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