起因
在一次项目问题分析中,需要获取当前内存中的所有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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。