此前,阿里開源了 監控與診斷 工具 「 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 是個好同志! ?