diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java index 262793812e1..cf51da52631 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java +++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java @@ -1,5 +1,6 @@ package com.taobao.arthas.core.command; +import com.taobao.arthas.core.command.klass100.ReflectAnalysisCommand; import java.util.ArrayList; import java.util.List; @@ -82,6 +83,7 @@ private void initCommands(List disabledCommands) { commandClassList.add(RetransformCommand.class); commandClassList.add(DashboardCommand.class); commandClassList.add(DumpClassCommand.class); + commandClassList.add(ReflectAnalysisCommand.class); commandClassList.add(HeapDumpCommand.class); commandClassList.add(JulyCommand.class); commandClassList.add(ThanksCommand.class); diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/ReflectAnalysisCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/ReflectAnalysisCommand.java new file mode 100644 index 00000000000..58bd333673c --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/ReflectAnalysisCommand.java @@ -0,0 +1,249 @@ +package com.taobao.arthas.core.command.klass100; + + +import com.alibaba.arthas.deps.org.slf4j.Logger; +import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.alibaba.deps.org.objectweb.asm.ClassReader; +import com.alibaba.deps.org.objectweb.asm.ClassVisitor; +import com.alibaba.deps.org.objectweb.asm.MethodVisitor; +import com.alibaba.deps.org.objectweb.asm.Opcodes; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.command.model.EchoModel; +import com.taobao.arthas.core.command.model.ReflectAnalysisModel; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.shell.command.ExitStatus; +import com.taobao.arthas.core.util.CommandUtils; +import com.taobao.arthas.core.util.InstrumentationUtils; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.collection.MapUtil; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Summary; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStreamWriter; +import java.lang.instrument.Instrumentation; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import static com.alibaba.deps.org.objectweb.asm.ClassReader.SKIP_DEBUG; +import static com.alibaba.deps.org.objectweb.asm.ClassReader.SKIP_FRAMES; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +/** + * @author: Ares + * @time: 2023-12-19 17:33:14 + * @description: 反射分析 + * @version: JDK 1.8 + */ +@Name("reflect-analysis") +@Summary("Analyze the reflection situation within the application") +@Description(Constants.EXAMPLE + + " reflect-analysis\n" + + " reflect-analysis reflect-analysis-result.csv\n" + + Constants.WIKI + Constants.WIKI_HOME + "dump") +public class ReflectAnalysisCommand extends AnnotatedCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReflectAnalysisCommand.class); + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + @Override + public void process(CommandProcess process) { + List args = process.args(); + + if (args.isEmpty()) { + ExitStatus exitStatus = processImpl(process, null); + CommandUtils.end(process, exitStatus); + return; + } + + String resultFilePath = buildResultFilePath(args); + CompletableFuture.runAsync(() -> processImpl(process, resultFilePath)); + String message = String.format("The reflect analysis result is being generated asynchronously, check the %s file later", resultFilePath); + process.appendResult(new EchoModel(message)); + CommandUtils.end(process, ExitStatus.success()); + } + + private String buildResultFilePath(List args) { + String resultFilePath = args.get(0); + Path path = Paths.get(resultFilePath); + if (!path.isAbsolute()) { + resultFilePath = Paths.get(TEMP_DIR, resultFilePath).toString(); + } + return resultFilePath; + } + + private ExitStatus processImpl(CommandProcess process, String resultFilePath) { + ExitStatus status; + try { + Instrumentation inst = process.session().getInstrumentation(); + // 找到所有反射生成的sun.reflect.GeneratedMethodAccessor类 + Set> matchedClasses = SearchUtils.searchClass(inst, "sun.reflect.GeneratedMethodAccessor*", false); + if (null != matchedClasses && !matchedClasses.isEmpty()) { + ClassDumpTransformer transformer = new ClassDumpTransformer(matchedClasses); + InstrumentationUtils.retransformClasses(inst, transformer, matchedClasses); + Map, File> dumpResult = transformer.getDumpResult(); + + Map> result = MapUtil.newHashMap(dumpResult.size()); + + for (Map.Entry, File> entry : dumpResult.entrySet()) { + Class clazz = entry.getKey(); + File classFile = entry.getValue(); + + String generatedMethodAccessorName = clazz.getSimpleName(); + try (FileInputStream inputStream = new FileInputStream(classFile)) { + ClassReader classReader = new ClassReader(inputStream); + ReflectAnalyzerClassVisitor classVisitor = new ReflectAnalyzerClassVisitor(); + classReader.accept(classVisitor, SKIP_DEBUG | SKIP_FRAMES); + + String invokeClassName = classVisitor.getInvokeClassName(); + String refName = classVisitor.getRefName(); + + if (invokeClassName != null && refName != null) { + String key = invokeClassName + "#" + refName; + result.computeIfAbsent(key, k -> new ArrayList<>()).add(generatedMethodAccessorName); + } + } catch (Throwable throwable) { + LOGGER.warn("analyze class file: {} fail:", classFile.getName(), throwable); + } + } + + processResult(process, result, resultFilePath); + } + status = ExitStatus.success(); + } catch (Throwable throwable) { + LOGGER.error("processing fail:", throwable); + process.end(-1, "processing fail"); + status = ExitStatus.failure(-1, "reflect analysis fail"); + } + return status; + } + + private void processResult(CommandProcess process, Map> result, String resultFilePath) { + List resultList = new ArrayList<>(result.size()); + result.forEach((key, valueList) -> { + ReflectAnalysisModel reflectAnalysisModel = new ReflectAnalysisModel(); + reflectAnalysisModel.setRefName(key); + reflectAnalysisModel.setGeneratedMethodAccessorNameCount(valueList.size()); + StringBuilder generatedMethodAccessorNames = new StringBuilder(); + for (String generatedMethodAccessorName : valueList) { + generatedMethodAccessorNames.append(generatedMethodAccessorName).append(","); + } + if (generatedMethodAccessorNames.length() > 0) { + reflectAnalysisModel.setGeneratedMethodAccessorNames(generatedMethodAccessorNames.substring(0, generatedMethodAccessorNames.length() - 1) + ); + } else { + reflectAnalysisModel.setGeneratedMethodAccessorNames(""); + } + resultList.add(reflectAnalysisModel); + }); + resultList.sort((leftMode, rightModel) -> { + int leftCount = leftMode.getGeneratedMethodAccessorNameCount(); + int rightCount = rightModel.getGeneratedMethodAccessorNameCount(); + if (leftCount > rightCount) { + return -1; + } else if (leftCount < rightCount) { + return 1; + } else { + return leftMode.getRefName().compareTo(rightModel.getRefName()); + } + }); + if (null == resultFilePath) { + for (ReflectAnalysisModel reflectAnalysisModel : resultList) { + process.appendResult(reflectAnalysisModel); + } + } else { + exportDataToCsvFile(resultList, resultFilePath); + } + } + + private void exportDataToCsvFile(List reflectAnalysisModelList, String resultFilePath) { + LOGGER.info("start write reflect analysis result: {} to file: {}", reflectAnalysisModelList.size(), resultFilePath); + File file = new File(resultFilePath); + try { + // write csv file + try (BufferedWriter bufferedWriter = new BufferedWriter( + new OutputStreamWriter(Files.newOutputStream(file.toPath(), CREATE, TRUNCATE_EXISTING), UTF_8))) { + bufferedWriter.write("count"); + bufferedWriter.write(","); + bufferedWriter.write("refName"); + bufferedWriter.write(","); + bufferedWriter.write("names"); + bufferedWriter.newLine(); + for (ReflectAnalysisModel reflectAnalysisModel : reflectAnalysisModelList) { + bufferedWriter.write(String.valueOf(reflectAnalysisModel.getGeneratedMethodAccessorNameCount())); + bufferedWriter.write(","); + bufferedWriter.write(reflectAnalysisModel.getRefName()); + bufferedWriter.write(","); + bufferedWriter.write(reflectAnalysisModel.getGeneratedMethodAccessorNames()); + bufferedWriter.newLine(); + } + LOGGER.info("write csv file end"); + } + } catch (Throwable throwable) { + LOGGER.error("export data to csv file fail:", throwable); + } + } + + /** + * ASM字节码分析器,用于直接从字节码中提取反射调用信息 + */ + private static class ReflectAnalyzerClassVisitor extends ClassVisitor { + + private String invokeClassName; + private String refName; + + public ReflectAnalyzerClassVisitor() { + super(Opcodes.ASM9); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ("invoke".equals(name)) { + return new InvokeMethodVisitor(); + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + private class InvokeMethodVisitor extends MethodVisitor { + + public InvokeMethodVisitor() { + super(Opcodes.ASM9); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + // 查找invoke方法中的方法调用,这通常是反射的目标方法 + if (opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKESPECIAL + || opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKEINTERFACE) { + + // 过滤掉Java标准库和sun包的调用 + if (!owner.startsWith("java/") && !owner.startsWith("sun/") && !owner.startsWith("jdk/")) { + invokeClassName = owner.replace('/', '.'); + refName = name; + } + } + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + + public String getInvokeClassName() { + return invokeClassName; + } + + public String getRefName() { + return refName; + } + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/ReflectAnalysisModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/ReflectAnalysisModel.java new file mode 100644 index 00000000000..46c25e88d60 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/model/ReflectAnalysisModel.java @@ -0,0 +1,46 @@ +package com.taobao.arthas.core.command.model; + +/** + * @author: Ares + * @time: 2023-12-20 11:33:32 + * @description: ReflectAnalysis model + * @version: JDK 1.8 + */ +public class ReflectAnalysisModel extends ResultModel { + + private String refName; + + private String generatedMethodAccessorNames; + + private Integer generatedMethodAccessorNameCount; + + @Override + public String getType() { + return "reflect-analysis"; + } + + public String getRefName() { + return refName; + } + + public void setRefName(String refName) { + this.refName = refName; + } + + public String getGeneratedMethodAccessorNames() { + return generatedMethodAccessorNames; + } + + public void setGeneratedMethodAccessorNames(String generatedMethodAccessorNames) { + this.generatedMethodAccessorNames = generatedMethodAccessorNames; + } + + public Integer getGeneratedMethodAccessorNameCount() { + return generatedMethodAccessorNameCount; + } + + public void setGeneratedMethodAccessorNameCount(Integer generatedMethodAccessorNameCount) { + this.generatedMethodAccessorNameCount = generatedMethodAccessorNameCount; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ReflectAnalysisView.java b/core/src/main/java/com/taobao/arthas/core/command/view/ReflectAnalysisView.java new file mode 100644 index 00000000000..fab79d91a9d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ReflectAnalysisView.java @@ -0,0 +1,22 @@ +package com.taobao.arthas.core.command.view; + +import com.taobao.arthas.core.command.model.ReflectAnalysisModel; +import com.taobao.arthas.core.shell.command.CommandProcess; + +/** + * @author: Ares + * @time: 2023-12-21 11:45:05 + * @description: ReflectAnalysis view + * @version: JDK 1.8 + */ +public class ReflectAnalysisView extends ResultView { + + @Override + public void draw(CommandProcess process, ReflectAnalysisModel result) { + String line = String.format("count=%s, refName=%s, names=%s", + result.getGeneratedMethodAccessorNameCount(), result.getRefName(), + result.getGeneratedMethodAccessorNames()); + process.write(line).write("\n"); + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java index 947b60a1ee6..c9cb2f83af1 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java @@ -52,6 +52,7 @@ private void initResultViews() { //klass100 registerView(ClassLoaderView.class); registerView(DumpClassView.class); + registerView(ReflectAnalysisView.class); registerView(GetStaticView.class); registerView(JadView.class); registerView(MemoryCompilerView.class); diff --git a/core/src/main/java/com/taobao/arthas/core/util/collection/MapUtil.java b/core/src/main/java/com/taobao/arthas/core/util/collection/MapUtil.java new file mode 100644 index 00000000000..934cbe837a2 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/collection/MapUtil.java @@ -0,0 +1,42 @@ +package com.taobao.arthas.core.util.collection; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author: Ares + * @time: 2025-09-25 12:34:07 + * @description: Map util + * @version: JDK 1.8 + */ +public class MapUtil { + + private static final int MAX_POWER_OF_TWO = 1 << (Integer.SIZE - 2); + + /** + * @see java.util.HashMap#DEFAULT_INITIAL_CAPACITY + */ + private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + + public static int capacity(int expectedSize) { + if (expectedSize < 0) { + return DEFAULT_INITIAL_CAPACITY; + } + + if (expectedSize < 3) { + return expectedSize + 1; + } + if (expectedSize < MAX_POWER_OF_TWO) { + // This is the calculation used in JDK8 to resize when a putAll + // happens; it seems to be the most conservative calculation we + // can make. 0.75 is the default load factor. + return (int) ((float) expectedSize / 0.75F + 1.0F); + } + return Integer.MAX_VALUE; + } + + public static Map newHashMap(int expectedSize) { + return new HashMap<>(capacity(expectedSize)); + } + +}