此前,阿里开源了 监控与诊断 工具 「 Arthas 」,一款可用于线上问题分析的利器,短时间以内收获了大量关注,在 Twitter 上连 Java 官方的 Twitter 也转发了,真的很赞。java
GitHub 上是这样自述的:apache
Arthas 是一款线上监控诊断产品,经过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的状况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提高线上问题排查效率。tomcat
我通常看到感兴趣的开源工具,会找几个最感兴趣的功能点切入,从源码了解设计与实现原理。对于一些本身了解的实现思路,再从源码中验证一下是不是采用相同的实现思路。若是实现和本身想的同样,可能你会想,啊哈,想到一块了。若是源码中是另外一种实现,你就会想 Cool, 还能够这样玩。 仿佛如同在和源码的做者对话同样 。session
此次趁着国庆假期看了一些「 Arthas 」的源码,大体总结下。jvm
从源码的包结构上,能够看到分为几个大的 模块:工具
我主要看了如下几个功能:ui
链接到指定的进程,是后续监控与诊断的 基础 。只有先 attach 到进程之上,才能获取 VM 对应的信息,查询 ClassLoader 加载的类等等。spa
用于相似诊断工具的读者可能都有印象,像 JProfile、 VisualVM 等工具,都会让你选择一个要链接到的进程。而后再在指定的 VM 上进行操做。好比查看对应的内存分区信息,内存垃圾收集信息,执行 BTrace脚本等等。命令行
我们先来想一想,这些可供链接的进程列表,是怎么列出来的呢?线程
通常可能会是相似 ps aux | grep java
这种,或者是使用 Java 提供的工具 jps -lv
均可以列出包含进程id的内容。我在很早以前的文章里写过一点 jps 的内容( 你可能不知道的几个java小工具 ),其背后实现,是会将本地启动的全部 Java 进程,以 pid 作为文件名存放在Java 的临时目录中。这个列表,遍历这些文件便可得出来。
Arthas 是怎么作的呢?
在启动脚本 as.sh 中,有关于进程列表的代码以下,实现也是经过 jps
而后把Jps本身排除掉:
# check pid if [ -z ${TARGET_PID} ] && [ ${BATCH_MODE} = false ]; then local IFS_backup=$IFS IFS=$'\n' CANDIDATES=($(${JAVA_HOME}/bin/jps -l | grep -v sun.tools.jps.Jps | awk '{print $0}')) if [ ${#CANDIDATES[@]} -eq 0 ]; then echo "Error: no available java process to attach." # recover IFS IFS=$IFS_backup return 1 fi echo "Found existing java process, please choose one and hit RETURN." index=0 suggest=1 # auto select tomcat/pandora-boot process for process in "${CANDIDATES[@]}"; do index=$(($index+1)) if [ $(echo ${process} | grep -c org.apache.catalina.startup.Bootstrap) -eq 1 ] \ || [ $(echo ${process} | grep -c com.taobao.pandora.boot.loader.SarLauncher) -eq 1 ] then suggest=${index} break fi done
选择好进程以后,就是链接到指定进程了。链接部分在 attach
这里
# attach arthas to target jvm # $1 : arthas_local_version attach_jvm() { local arthas_version=$1 local arthas_lib_dir=${ARTHAS_LIB_DIR}/${arthas_version}/arthas echo "Attaching to ${TARGET_PID} using version ${1}..." if [ ${TARGET_IP} = ${DEFAULT_TARGET_IP} ]; then ${JAVA_HOME}/bin/java \ ${ARTHAS_OPTS} ${BOOT_CLASSPATH} ${JVM_OPTS} \ -jar ${arthas_lib_dir}/arthas-core.jar \ -pid ${TARGET_PID} \ -target-ip ${TARGET_IP} \ -telnet-port ${TELNET_PORT} \ -http-port ${HTTP_PORT} \ -core "${arthas_lib_dir}/arthas-core.jar" \ -agent "${arthas_lib_dir}/arthas-agent.jar" fi }
对于 JVM 内部的 attach 实现,是经过 tools.jar 这个包中的 com.sun.tools.attach.VirtualMachine
以及 VirtualMachine.attach(pid)
这种方式来实现的。
底层则是经过 JVMTI
。以前的文章简单分析过 JVMTI
这种技术( 当咱们谈Debug时,咱们在谈什么(Debug实现原理) ),在运行前或者运行时,将自定义的 Agent加载并和 VM 进行 通讯 。
上面具体执行的内容在 arthas-core.jar 的主类中,咱们来看具体的内容:
private void attachAgent(Configure configure) throws Exception { VirtualMachineDescriptor virtualMachineDescriptor = null; for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { String pid = descriptor.id(); if (pid.equals(Integer.toString(configure.getJavaPid()))) { virtualMachineDescriptor = descriptor; } } VirtualMachine virtualMachine = null; try { if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式 virtualMachine = VirtualMachine.attach("" + configure.getJavaPid()); } else { virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); } Properties targetSystemProperties = virtualMachine.getSystemProperties(); String targetJavaVersion = targetSystemProperties.getProperty("java.specification.version"); String currentJavaVersion = System.getProperty("java.specification.version"); if (targetJavaVersion != null && currentJavaVersion != null) { if (!targetJavaVersion.equals(currentJavaVersion)) { AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.", currentJavaVersion, targetJavaVersion); AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.", targetSystemProperties.getProperty("java.home")); } } virtualMachine.loadAgent(configure.getArthasAgent(), configure.getArthasCore() + ";" + configure.toString()); } finally { if (null != virtualMachine) { virtualMachine.detach(); } } }
经过 VirtualMachine
, 能够attach到当前指定的pid上,或者是经过 VirtualMachineDescriptor
实现指定进程的attach,最核心的就是这一句:
virtualMachine.loadAgent(configure.getArthasAgent(),configure.getArthasCore() + ";" + configure.toString());
这样,就和指定进程的 VM创建了链接,此时就能够进行通讯啦。
咱们在问题诊断中,有些时候须要了解当前加载的 class 对应的内容,方便确认加载的类是否正确等,通常经过 javap 只能显示相似摘要的内容,并不直观。 在桌面端咱们能够经过 jd-gui 之类的工具,在命令行里通常可选的很少。Arthas 则集成了这一功能。
大体的步骤以下:
咱们来看 Arthas
的实现。
对于 VM 中指定名称的 class 的查找,咱们看下面这几行代码:
public void process(CommandProcess process) { RowAffect affect = new RowAffect(); Instrumentation inst = process.session().getInstrumentation(); Set<Class> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code); try { if (matchedClasses == null || matchedClasses.isEmpty()) { processNoMatch(process); } else if (matchedClasses.size() > 1) { processMatches(process, matchedClasses); } else { Set<Class> withInnerClasses = SearchUtils.searchClassOnly(inst, classPattern + "(?!.*\\$\\$Lambda\\$).*", true, code); processExactMatch(process, affect, inst, matchedClasses, withInnerClasses); }
关键的查找内容,作了封装,在 SearchUtils
里,这里有一个核心的参数: Instrumentation
,都是这个哥们给实现的。
/** * 根据类名匹配,搜已经被JVM加载的类 * * @param inst inst * @param classNameMatcher 类名匹配 * @return 匹配的类集合 */ public static Set> searchClass(Instrumentation inst, Matcher classNameMatcher, int limit) { for (Class clazz : inst.getAllLoadedClasses()) { if (classNameMatcher.matching(clazz.getName())) { matches.add(clazz); } } return matches; }
inst.getAllLoadedClasses()
,它才是背后的大玩家。
查找到了 Class 以后,怎么反编译的呢?
private String decompileWithCFR(String classPath, Class clazz, String methodName) { List<String> options = new ArrayList<String>(); options.add(classPath); // options.add(clazz.getName()); if (methodName != null) { options.add(methodName); } options.add(OUTPUTOPTION); options.add(DecompilePath); options.add(COMMENTS); options.add("false"); String args[] = new String[options.size()]; options.toArray(args); Main.main(args); String outputFilePath = DecompilePath + File.separator + Type.getInternalName(clazz) + ".java"; File outputFile = new File(outputFilePath); if (outputFile.exists()) { try { return FileUtils.readFileToString(outputFile, Charset.defaultCharset()); } catch (IOException e) { logger.error(null, "error read decompile result in: " + outputFilePath, e); } } return null; }
decompileWithCFR
,因此咱们大概了解到反编译是经过第三方工具「 CFR 」来实现的。上面的代码也是拼 Option
而后传给 CFR
的 Main
方法实现,再保存下来。感兴趣的朋友能够查询 benf cfr
了解具体用法。看过上面反编译 class 的内容以后,咱们知道封装了一个 SearchUtil
的类,后面许多地方都会用到,并且上面反编译也是在查询到类的以后再进行的。查询的过程,也是在Instrument的基础之上,再加上各类匹配规则过滤,因此更多的具体内容再也不赘述。
咱们发现上面几个功能的实现中,有两个关键的东西:
VirtualMachine
Instrumentation
Arthas 的总体逻辑也是在 Java 的 Instrumentation
基础上来实现,全部在加载的类会经过Agent的加载, 经过addTransformer
以后,进行加强,而后将对应的Advice
织入进去,对于类的查找,方法的查找,都是经过SearchUtil
来进行的,经过Instrument
的loadAllClass
方法将全部的JVM加载的class按名字进行匹配,一致的会进行返回。
Instrumentation 是个好同志! ?