arthas簡(jiǎn)介
arthas 是Alibaba開源的Java診斷工具,基于jvm Agent方式,使用Instrumentation方式修改字節(jié)碼方式以及使用java.lang.management包提供的管理接口的方式進(jìn)行java應(yīng)用診斷。詳細(xì)的介紹可以參考官方文檔。
官方文檔地址:https://alibaba.github.io/arthas/
GitHub地址:https://github.com/alibaba/arthas/
本文主要分析arthas源碼,主要分成下面幾個(gè)部分:
- arthas組成模塊
- arthas服務(wù)端代碼分析
- arthas客戶端代碼分析
arthas組成模塊
arthas有多個(gè)模塊組成,如下圖所示:

- arthas-boot.jar和as.sh模塊功能類似,分別使用java和shell腳本,下載對(duì)應(yīng)的jar包,并生成服務(wù)端和客戶端的啟動(dòng)命令,然后啟動(dòng)客戶端和服務(wù)端。服務(wù)端最終生成的啟動(dòng)命令如下:
${JAVA_HOME}"/bin/java \
${opts} \
-jar "${arthas_lib_dir}/arthas-core.jar" \
-pid ${TARGET_PID} \ 要注入的進(jìn)程id
-target-ip ${TARGET_IP} \ 服務(wù)器ip地址
-telnet-port ${TELNET_PORT} \ 服務(wù)器telnet服務(wù)端口號(hào)
-http-port ${HTTP_PORT} \ websocket服務(wù)端口號(hào)
-core "${arthas_lib_dir}/arthas-core.jar" \ arthas-core目錄
-agent "${arthas_lib_dir}/arthas-agent.jar" arthas-agent目錄
- arthas-core.jar是服務(wù)端程序的啟動(dòng)入口類,會(huì)調(diào)用
virtualMachine#attach到目標(biāo)進(jìn)程,并加載arthas-agent.jar作為agent jar包。 - arthas-agent.jar既可以使用premain方式(在目標(biāo)進(jìn)程啟動(dòng)之前,通過-agent參數(shù)靜態(tài)指定),也可以通過agentmain方式(在進(jìn)程啟動(dòng)之后attach上去)。arthas-agent會(huì)使用自定義的classloader(
ArthasClassLoader)加載arthas-core.jar里面的com.taobao.arthas.core.config.Configure類以及com.taobao.arthas.core.server.ArthasBootstrap。 同時(shí)程序運(yùn)行的時(shí)候會(huì)使用arthas-spy.jar。 - arthas-spy.jar里面只包含Spy類,目的是為了將Spy類使用
BootstrapClassLoader來加載,從而使目標(biāo)進(jìn)程的java應(yīng)用可以訪問Spy類。通過ASM修改字節(jié)碼,可以將Spy類的方法ON_BEFORE_METHOD,ON_RETURN_METHOD等編織到目標(biāo)類里面。Spy類你可以簡(jiǎn)單理解為類似spring aop的Advice,有前置方法,后置方法等。 - arthas-client.jar是客戶端程序,用來連接arthas-core.jar啟動(dòng)的服務(wù)端代碼,使用telnet方式。一般由arthas-boot.jar和as.sh來負(fù)責(zé)啟動(dòng)。
arthas服務(wù)端代碼分析
前置準(zhǔn)備
看服務(wù)端啟動(dòng)命令可以知道 從 arthas-core.jar開始啟動(dòng),arthas-core的pom.xml文件里面指定了mainClass為com.taobao.arthas.core.Arthas,使得程序啟動(dòng)的時(shí)候從該類的main方法開始運(yùn)行。Arthas源碼如下:
public class Arthas {
private Arthas(String[] args) throws Exception {
attachAgent(parse(args));
}
private Configure parse(String[] args) {
// 省略非關(guān)鍵代碼,解析啟動(dòng)參數(shù)作為配置,并填充到configure對(duì)象里面
return configure;
}
private void attachAgent(Configure configure) throws Exception {
// 省略非關(guān)鍵代碼,attach到目標(biāo)進(jìn)程
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
virtualMachine.loadAgent(configure.getArthasAgent(),
configure.getArthasCore() + ";" + configure.toString());
}
public static void main(String[] args) {
new Arthas(args);
}
}
- Arthas首先解析入?yún)ⅲ?code>com.taobao.arthas.core.config.Configure類,包含了相關(guān)配置信息
- 使用jdk-tools里面的
VirtualMachine.loadAgent,其中第一個(gè)參數(shù)為agent路徑, 第二個(gè)參數(shù)向jar包中的agentmain()方法傳遞參數(shù)(此處為agent-core.jar包路徑和config序列化之后的字符串),加載arthas-agent.jar包,并運(yùn)行 - arthas-agent.jar包,指定了Agent-Class為
com.taobao.arthas.agent.AgentBootstrap,同時(shí)可以使用Premain的方式和目標(biāo)進(jìn)程同時(shí)啟動(dòng)
<manifestEntries>
<Premain-Class>com.taobao.arthas.agent.AgentBootstrap</Premain-Class>
<Agent-Class>com.taobao.arthas.agent.AgentBootstrap</Agent-Class>
</manifestEntries>
其中Premain-Class的premain和Agent-Class的agentmain都調(diào)用main方法。
main方法主要做4件事情:
- 找到arthas-spy.jar路徑,并調(diào)用
Instrumentation#appendToBootstrapClassLoaderSearch方法,使用bootstrapClassLoader來加載arthas-spy.jar里的Spy類。 - arthas-agent路徑傳遞給自定義的classloader(
ArthasClassloader),用來隔離arthas本身的類和目標(biāo)進(jìn)程的類。 - 使用
ArthasClassloader#loadClass方法,加載com.taobao.arthas.core.advisor.AdviceWeaver類,并將里面的methodOnBegin、methodOnReturnEnd、methodOnThrowingEnd等方法取出賦值給Spy類對(duì)應(yīng)的方法。同時(shí)Spy類里面的方法又會(huì)通過ASM字節(jié)碼增強(qiáng)的方式,編織到目標(biāo)代碼的方法里面。使得Spy 間諜類可以關(guān)聯(lián)由AppClassLoader加載的目標(biāo)進(jìn)程的業(yè)務(wù)類和ArthasClassloader加載的arthas類,因此Spy類可以看做兩者之間的橋梁。根據(jù)classloader雙親委派特性,子classloader可以訪問父classloader加載的類。源碼如下:
private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {
// 將Spy添加到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
// 構(gòu)造自定義的類加載器ArthasClassloader,盡量減少Arthas對(duì)現(xiàn)有工程的侵蝕
return loadOrDefineClassLoader(agentJarFile);
}
private static void initSpy(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException {
// 該classLoader為ArthasClassloader
Class<?> adviceWeaverClass = classLoader.loadClass(ADVICEWEAVER);
Method onBefore = adviceWeaverClass.getMethod(ON_BEFORE, int.class, ClassLoader.class, String.class,
String.class, String.class, Object.class, Object[].class);
Method onReturn = adviceWeaverClass.getMethod(ON_RETURN, Object.class);
Method onThrows = adviceWeaverClass.getMethod(ON_THROWS, Throwable.class);
Method beforeInvoke = adviceWeaverClass.getMethod(BEFORE_INVOKE, int.class, String.class, String.class, String.class);
Method afterInvoke = adviceWeaverClass.getMethod(AFTER_INVOKE, int.class, String.class, String.class, String.class);
Method throwInvoke = adviceWeaverClass.getMethod(THROW_INVOKE, int.class, String.class, String.class, String.class);
Method reset = AgentBootstrap.class.getMethod(RESET);
Spy.initForAgentLauncher(classLoader, onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke, reset);
}
classloader關(guān)系如下:
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@7bf2dede
+-com.taobao.arthas.agent.ArthasClassloader@51a10fc8
+-sun.misc.Launcher$AppClassLoader@18b4aac2
- 異步調(diào)用bind方法,該方法最終啟動(dòng)server監(jiān)聽線程,監(jiān)聽客戶端的連接,包括telnet和websocket兩種通信方式。源碼如下:
Thread bindingThread = new Thread() {
@Override
public void run() {
try {
bind(inst, agentLoader, agentArgs);
} catch (Throwable throwable) {
throwable.printStackTrace(ps);
}
}
};
private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
/**
* <pre>
* Configure configure = Configure.toConfigure(args);
* int javaPid = configure.getJavaPid();
* ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(javaPid, inst);
* </pre>
*/
Class<?> classOfConfigure = agentLoader.loadClass(ARTHAS_CONFIGURE);
Object configure = classOfConfigure.getMethod(TO_CONFIGURE, String.class).invoke(null, args);
int javaPid = (Integer) classOfConfigure.getMethod(GET_JAVA_PID).invoke(configure);
Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, int.class, Instrumentation.class).invoke(null, javaPid, inst);
boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
if (!isBind) {
try {
ps.println("Arthas start to bind...");
bootstrapClass.getMethod(BIND, classOfConfigure).invoke(bootstrap, configure);
ps.println("Arthas server bind success.");
return;
} catch (Exception e) {
ps.println("Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.");
throw e;
}
}
ps.println("Arthas server already bind.");
}
主要做兩件事情:
- 使用
ArthasClassloader加載com.taobao.arthas.core.config.Configure類(位于arthas-core.jar),并將傳遞過來的序列化之后的config,反序列化成對(duì)應(yīng)的Configure對(duì)象。 - 使用
ArthasClassloader加載com.taobao.arthas.core.server.ArthasBootstrap類(位于arthas-core.jar),并調(diào)用bind方法。
啟動(dòng)服務(wù)器,并監(jiān)聽客戶端請(qǐng)求
下面重點(diǎn)看下com.taobao.arthas.core.server.ArthasBootstrap#bind方法
/**
* Bootstrap arthas server
*
* @param configure 配置信息
* @throws IOException 服務(wù)器啟動(dòng)失敗
*/
public void bind(Configure configure) throws Throwable {
long start = System.currentTimeMillis();
if (!isBindRef.compareAndSet(false, true)) {
throw new IllegalStateException("already bind");
}
try {
ShellServerOptions options = new ShellServerOptions()
.setInstrumentation(instrumentation)
.setPid(pid)
.setSessionTimeout(configure.getSessionTimeout() * 1000);
shellServer = new ShellServerImpl(options, this);
BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
resolvers.add(builtinCommands);
// TODO: discover user provided command resolver
if (configure.getTelnetPort() > 0) {
// telnet方式的server
shellServer.registerTermServer(new TelnetTermServer(configure.getIp(), configure.getTelnetPort(),
options.getConnectionTimeout()));
} else {
logger.info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
}
if (configure.getHttpPort() > 0) {
// websocket方式的server
shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
options.getConnectionTimeout()));
} else {
logger.info("http port is {}, skip bind http server.", configure.getHttpPort());
}
for (CommandResolver resolver : resolvers) {
shellServer.registerCommandResolver(resolver);
}
shellServer.listen(new BindHandler(isBindRef));
logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(),
configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout());
// 異步回報(bào)啟動(dòng)次數(shù)
UserStatUtil.arthasStart();
logger.info("as-server started in {} ms", System.currentTimeMillis() - start );
} catch (Throwable e) {
logger.error(null, "Error during bind to port " + configure.getTelnetPort(), e);
if (shellServer != null) {
shellServer.close();
}
throw e;
}
}
可以看到有兩種類型的server,TelnetTermServer和HttpTermServer。同時(shí)會(huì)在BuiltinCommandPack里添加所有的命令Command,添加命令的源碼如下:
public class BuiltinCommandPack implements CommandResolver {
private static List<Command> commands = new ArrayList<Command>();
static {
initCommands();
}
@Override
public List<Command> commands() {
return commands;
}
private static void initCommands() {
commands.add(Command.create(HelpCommand.class));
commands.add(Command.create(KeymapCommand.class));
commands.add(Command.create(SearchClassCommand.class));
commands.add(Command.create(SearchMethodCommand.class));
commands.add(Command.create(ClassLoaderCommand.class));
commands.add(Command.create(JadCommand.class));
commands.add(Command.create(GetStaticCommand.class));
commands.add(Command.create(MonitorCommand.class));
commands.add(Command.create(StackCommand.class));
commands.add(Command.create(ThreadCommand.class));
commands.add(Command.create(TraceCommand.class));
commands.add(Command.create(WatchCommand.class));
commands.add(Command.create(TimeTunnelCommand.class));
commands.add(Command.create(JvmCommand.class));
// commands.add(Command.create(GroovyScriptCommand.class));
commands.add(Command.create(OgnlCommand.class));
commands.add(Command.create(DashboardCommand.class));
commands.add(Command.create(DumpClassCommand.class));
commands.add(Command.create(JulyCommand.class));
commands.add(Command.create(ThanksCommand.class));
commands.add(Command.create(OptionsCommand.class));
commands.add(Command.create(ClsCommand.class));
commands.add(Command.create(ResetCommand.class));
commands.add(Command.create(VersionCommand.class));
commands.add(Command.create(ShutdownCommand.class));
commands.add(Command.create(SessionCommand.class));
commands.add(Command.create(SystemPropertyCommand.class));
commands.add(Command.create(SystemEnvCommand.class));
commands.add(Command.create(RedefineCommand.class));
commands.add(Command.create(HistoryCommand.class));
}
}
調(diào)用shellServer.registerTermServer,shellServer.registerTermServer,shellServer.registerCommandResolve 注冊(cè)到ShellServer里,ShellServer是整個(gè)服務(wù)端的門面類,調(diào)用listen方法啟動(dòng)ShellServer。
ShellServer會(huì)使用一系列的類,細(xì)節(jié)比較復(fù)雜,可以見下面的類圖。

ShellServer#listen會(huì)調(diào)用所有注冊(cè)的TermServer的listen方法,比如TelnetTermServer。然后TelnetTermServer的listen方法會(huì)注冊(cè)一個(gè)回調(diào)類,該回調(diào)類在有新的客戶端連接時(shí)會(huì)調(diào)用TermServerTermHandler的handle方法處理。
bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port);
try {
bootstrap.start(new Consumer<TtyConnection>() {
@Override
public void accept(final TtyConnection conn) {
termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
}
}).get(connectionTimeout, TimeUnit.MILLISECONDS);
listenHandler.handle(Future.<TermServer>succeededFuture());
該方法會(huì)接著調(diào)用ShellServerImpl的handleTerm方法進(jìn)行處理,ShellServerImpl的handleTerm方法會(huì)調(diào)用ShellImpl的readline方法。該方法會(huì)注冊(cè)ShellLineHandler作為回調(diào)類,服務(wù)端接收到客戶端發(fā)送的請(qǐng)求行之后,會(huì)回調(diào)ShellLineHandler的handle方法處理請(qǐng)求。readline方法源碼如下:
public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) {
if (conn.getStdinHandler() != echoHandler) {
throw new IllegalStateException();
}
if (inReadline) {
throw new IllegalStateException();
}
inReadline = true;
// 注冊(cè)回調(diào)類RequestHandler,該類包裝了ShellLineHandler,處理邏輯還是在ShellLineHandler類里面
readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session));
}
處理客戶端請(qǐng)求
ShellLineHandler的handle方法會(huì)根據(jù)不同的請(qǐng)求命令執(zhí)行不同的邏輯:
- 如果是exit,logout,quit, jobs,fg,bg,kill等直接執(zhí)行。
-
如果是其他的命令,則創(chuàng)建Job,并運(yùn)行。創(chuàng)建Job的類圖如下:
服務(wù)端-創(chuàng)建job類圖.png
步驟比較多,就不一一細(xì)講,總之:
- 創(chuàng)建
Job時(shí),會(huì)根據(jù)具體客戶端傳遞的命令,找到對(duì)應(yīng)的Command,并包裝成Process,Process再被包裝成Job。 - 運(yùn)行
Job時(shí),反向先調(diào)用Process,再找到對(duì)應(yīng)的Command,最終調(diào)用Command的process處理請(qǐng)求。
Command處理流程
Command主要分為兩類:
- 不需要使用字節(jié)碼增強(qiáng)的命令
其中JVM相關(guān)的使用java.lang.management提供的管理接口,來查看具體的運(yùn)行時(shí)數(shù)據(jù)。比較簡(jiǎn)單,就不介紹了。 -
需要使用字節(jié)碼增強(qiáng)的命令
字節(jié)碼增強(qiáng)的命令,可以參考下圖:
arthas-command相關(guān)類圖.png
字節(jié)碼增加的命令統(tǒng)一繼承EnhancerCommand類,process方法里面調(diào)用enhance方法進(jìn)行增強(qiáng)。調(diào)用Enhancer類enhance方法,該方法內(nèi)部調(diào)用inst.addTransformer方法添加自定義的ClassFileTransformer,這邊是Enhancer類。
Enhancer類使用AdviceWeaver(繼承ClassVisitor),用來修改類的字節(jié)碼。重寫了visitMethod方法,在該方法里面修改類指定的方法。visitMethod方法里面使用了AdviceAdapter(繼承了MethodVisitor類),在onMethodEnter方法, onMethodExit方法中,把Spy類對(duì)應(yīng)的方法(ON_BEFORE_METHOD, ON_RETURN_METHOD, ON_THROWS_METHOD等)編織到目標(biāo)類的方法對(duì)應(yīng)的位置。
在前面Spy初始化的時(shí)候可以看到,這幾個(gè)方法其實(shí)指向的是AdviceWeaver類的methodOnBegin, methodOnReturnEnd等。在這些方法里面都會(huì)根據(jù)adviceId查找對(duì)應(yīng)的AdviceListener,并調(diào)用AdviceListener的對(duì)應(yīng)的方法,比如before,afterReturning, afterThrowing。
通過這種方式,可以實(shí)現(xiàn)不同的Command使用不同的AdviceListener,從而實(shí)現(xiàn)不同的處理邏輯。下面找?guī)讉€(gè)常用的AdviceListener介紹下:
-
StackAdviceListener
在方法執(zhí)行前,記錄堆棧和方法的耗時(shí)。 -
WatchAdviceListener
滿足條件時(shí)打印打印參數(shù)或者結(jié)果,條件表達(dá)式使用Ognl語法。 -
TraceAdviceListener
在每個(gè)方法前后都記錄,并維護(hù)一個(gè)調(diào)用樹結(jié)構(gòu)。
arthas客戶端代碼分析
客戶端代碼在arthas-client模塊里面,入口類是com.taobao.arthas.client.TelnetConsole。主要使用apache commons-net jar進(jìn)行telnet連接,關(guān)鍵的代碼有下面幾步:
- 構(gòu)造
TelnetClient對(duì)象,并初始化 - 構(gòu)造
ConsoleReader對(duì)象,并初始化 - 調(diào)用
IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput())處理各個(gè)流,一共有四個(gè)流:
telnet.getInputStream()telnet.getOutputStream()System.inconsoleReader.getOutput()
請(qǐng)求時(shí):從本地System.in讀取,發(fā)送到 telnet.getOutputStream(),即發(fā)送給遠(yuǎn)程服務(wù)端。
響應(yīng)時(shí):從telnet.getInputStream()讀取遠(yuǎn)程服務(wù)端發(fā)送過來的響應(yīng),并傳遞給 consoleReader.getOutput(),即在本地控制臺(tái)輸出。

