这篇文章是以前学习Arthas和jvm-sandbox的一些心得和总结,但愿能帮助到你们。本文字较多,能够根据目录进行对应的阅读。html
2018年已过,可是在过去的一年里面开源了不少优秀的项目,这里我要介绍两个比较类似的阿里开源项目一个是Arthas,另外一个是jvm-sandbox。这两个项目都是在今年开源的,为何要介绍这两个项目呢?这里先卖个关子,先问下你们不知道是否遇到过下面的场景呢?java
以上这些场景,再真正的业务开发中你们或多或少都碰见过,而通常你们的处理方式和我在场景的描述得大致一致。而这里要给你们介绍一下Arthas和jvm-sandbox,若是你学会了这两个项目,上面全部的问题再你手上不再是难事。git
固然再介绍Arthas以前仍是要给你们说一下Greys,不管是Arthas仍是jvm-sandbox都是从Greys演变而来,这个是2014年阿里开源的一款Java在线问题诊断工具。而Arthas能够看作是他的升级版本,是一款更加优秀的,功能更加丰富的Java诊断工具。 在他的github的READEME中的介绍这款工具能够帮助你作下面这些事:github
下面我将会介绍一下Arthas的一些经常使用的命令和用法,看看是如何解决咱们实际中的问题的,至于安装教程能够参考Arthas的github。sql
相信你们都遇到过NoSuchMethodError这个错误,通常老司机看见这个错误第一反应就是jar包版本号冲突,这种问题通常来讲使用maven的一些插件就能轻松解决。apache
以前遇到个奇怪的问题,咱们有两个服务的client-jar包,有个类的包名和类名均是一致,在编写代码的时候没有注意到这个问题,在编译阶段因为包名和类名都是一致,全部编译阶段并无报错,在线下的运行阶段没有问题,可是测试环境的机器中的运行阶段缺报出了问题。这个和以前的jar包版本号冲突有点不一样,由于在排查的时候咱们想使用A服务的client-jar包的这个类,可是这个jar包的版本号在Maven中的确是惟一的。数组
这个时候Arthas就能够大显神通了。oracle
找到对应的类,而后输出下面的命令(用例使用的是官方提供的用例):dom
$ sc -d demo.MathGame class-info demo.MathGame code-source /private/tmp/arthas-demo.jar name demo.MathGame isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name MathGame modifier public annotation interfaces super-class +-java.lang.Object class-loader +-sun.misc.Launcher$AppClassLoader@3d4eac69 +-sun.misc.Launcher$ExtClassLoader@66350f69 classLoaderHash 3d4eac69 Affect(row-cnt:1) cost in 875 ms. 复制代码
能够看见打印出了code-source,当时发现了code-source并非从对应的Jar包取出来的,因而发现了两个服务对于同一个类使用了一样的包名和类名,致使了这个奇怪的问题,后续经过修改包名和类名进行解决。jvm
sc原理
sc的信息主要从对应的Class中获取。 好比isInterface,isAnnotation等等都是经过下面的方式获取:
对于咱们上面的某个类从哪一个jar包加载的是经过CodeSource来进行获取的:
Arthas还提供了一个命令jad用来反编译,对于解决类冲突错误颇有用,好比咱们想知道这个类里面的代码究竟是什么,直接一个jad命令就能搞定:
$ jad java.lang.String ClassLoader: Location: /* * Decompiled with CFR 0_132. */ package java.lang; import java.io.ObjectStreamField; ... public final class String implements Serializable, Comparable<String>, CharSequence { private final char[] value; private int hash; private static final long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator(); public String(byte[] arrby, int n, int n2) { String.checkBounds(arrby, n, n2); this.value = StringCoding.decode(arrby, n, n2); } ... 复制代码
通常经过这个命令咱们就能发现和你所期待的类是否缺乏了某些方法,或者某些方法有些改变,从而肯定jar包冲突。
jad原理
jad使用的是cfr提供的jar包来进行反编译。这里过程比较复杂这里就不进行叙述。
有不少同窗可能会以为动态修改日志有什么用呢?好像本身也没怎么用过呢? 通常来讲下面这几个场景能够须要:
ognl是一门表达式语言,在Arthas中你能够利用这个表达式语言作不少事,好比执行某个方法,获取某个信息。再这里咱们能够经过下面的命令来动态的修改日志级别:
$ ognl '@com.lz.test@LOGGER.logger.privateConfig' @PrivateConfig[ loggerConfig=@LoggerConfig[root], loggerConfigLevel=@Level[INFO], intLevel=@Integer[400], ] $ ognl '@com.lz.test@LOGGER.logger.setLevel(@org.apache.logging.log4j.Level@ERROR)' null $ ognl '@com.lz.test@LOGGER.logger.privateConfig' @PrivateConfig[ loggerConfig=@LoggerConfig[root], loggerConfigLevel=@Level[ERROR], intLevel=@Integer[200], ] 复制代码
上面的命令能够修改对应类中的info日志为error日志打印级别,若是想全局修改root的级别的话对于ognl表达式来讲执行比较困难,总的来讲须要将ognl翻译为下面这段代码:
org.apache.logging.log4j.core.LoggerContext loggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false); Map<String, LoggerConfig> map = loggerContext.getConfiguration().getLoggers(); for (org.apache.logging.log4j.core.config.LoggerConfig loggerConfig : map.values()) { String key = loggerConfig.getName(); if (StringUtils.isBlank(key)) { loggerConfig.setLevel(Level.ERROR); } } loggerContext.updateLoggers(); 复制代码
总的来讲比较复杂,这里不给予实现,若是有兴趣的能够用代码的形式去实现如下,美团的动态调整日志组件也是经过这种方法实现的。
原理
具体原理是首先获取AppClassLoader(默认)或者指定的ClassLoader,而后再调用Ognl的包,自动执行解析这个表达式,而这个执行的类都会从前面的ClassLoader中获取中去获取。
不少时候咱们方法执行的状况和咱们预期不符合,可是咱们又不知道到底哪里不符合,Arthas的watch命令就能帮助咱们解决这个问题。
watch命令顾名思义观察,他能够观察指定方法调用状况,定义了4个观察事件点, -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后。默认是-f
好比咱们想知道某个方法执行的时候,参数和返回值究竟是什么。注意这里的参数是方法执行完成的时候的参数,和入参不一样有可能会发生变化。
$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2 Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 44 ms. ts=2018-12-03 19:16:51; [cost=1.280502ms] result=@ArrayList[ @Object[][ @Integer[535629513], ], @ArrayList[ @Integer[3], @Integer[19], @Integer[191], @Integer[49199], ], ] 复制代码
你能获得参数和返回值的状况,以及方法时间消耗的等信息。
原理
利用jdk1.6的instrument + ASM 记录方法的入参出参,以及方法消耗时间。
当某个方法耗时较长,这个时候你须要排查究竟是某一处发生了长时间的耗时,通常这种问题比较难排查,都是经过全链路追踪trace图去进行排查,可是在本地的应用中没有trace图,这个时候须要Arthas的trace命令来进行排查问题。
trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的全部性能开销和追踪调用链路。
可是trace只能追踪一层的调用链路,若是一层的链路信息不够用,能够把该链路上有问题的方法再次进行trace。 trace使用例子以下。
$ trace demo.MathGame run Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 42 ms. `---ts=2018-12-04 00:44:17;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69 `---[10.611029ms] demo.MathGame:run() +---[0.05638ms] java.util.Random:nextInt() +---[10.036885ms] demo.MathGame:primeFactors() `---[0.170316ms] demo.MathGame:print() 复制代码
能够看见上述耗时最多的方法是primeFactors,因此咱们能够对其进行trace进行再一步的排查。
原理
利用jdk1.6的instrument + ASM。在访问方法以前和以后会进行记录。
有时候排查一个问题须要上游再次调用这个方法,好比使用postMan等工具,固然Arthas提供了一个命令让替代咱们来回手动请求。
tt官方介绍: 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不一样的时间下调用进行观测。能够看见tt能够用于录制请求,固然也支持咱们重放。 若是要录制某个方法,能够用下面命令:
$ tt -t demo.MathGame primeFactors Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 66 ms. INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors 1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors 1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors 1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors 1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors 复制代码
上面录制了5个调用环境现场,也能够看作是录制了5个请求返回信息。好比咱们想选择index为1004个的请求来重放,能够输入下面的命令。
$ tt -i 1004 -p RE-INDEX 1004 GMT-REPLAY 2018-12-04 11:26:00 OBJECT 0x4b67cf4d CLASS demo.MathGame METHOD primeFactors PARAMETERS[0] @Integer[946738738] IS-RETURN true IS-EXCEPTION false RETURN-OBJ @ArrayList[ @Integer[2], @Integer[11], @Integer[17], @Integer[2531387], ] Time fragment[1004] successfully replayed. Affect(row-cnt:1) cost in 14 ms. 复制代码
注意重放请求须要关注两点:
ThreadLocal 信息丢失:因为使用的是Arthas线程调用,会让threadLocal信息丢失,好比一些TraceId信息可能会丢失
引用的对象:保存的入参是保存的引用,而不是拷贝,因此若是参数中的内容被修改,那么入参其实也是被修改的。
有时候有些方法很是耗时或者很是重要,须要知道究竟是谁发起的调用,好比System.gc(),有时候若是你发现fullgc频繁是由于System.gc()引发的,你须要查看究竟是什么应用调用的,那么你就可使用下面的命令。
咱们能够输入下面的命令:
$ options unsafe true NAME BEFORE-VALUE AFTER-VALUE ----------------------------------- unsafe false true $ stack java.lang.System gc Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 50 ms. ts=2019-01-20 21:14:05;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@14dad5dc @java.lang.System.gc() at com.lz.test.Test.main(Test.java:322) 复制代码
首先输入options unsafe true容许咱们对jdk加强,而后对System.gc进行进行监视,而后记录当前的堆栈来获取是什么位置进行的调用。
有些时候咱们找了全部的命令,发现和咱们的需求并不符合的时候,那么这个时候咱们能够从新定义这个类,咱们能够用使用下面的命令。
redefine命令提供了咱们能够从新定义jvm中的class,可是使用这个命令以后class不可恢复。咱们首先须要把重写的class编译出来,而后上传到咱们指定的目录,进行下面的操做:
redefine -p /tmp/Test.class 复制代码
能够重定义咱们的Test.class。从而修改逻辑,完成咱们自定义的需求。
上面介绍了7种Arthas比较常见的场景和命令。固然这个命令还远远不止这么点,每一个命令的用法也没有局限于我介绍的。尤为是开源之后更多的开发者参与了进来,如今也将其优化成能够有界面的,在线排查问题的方式,来解决去线上安装的各类不便。
更多的命令能够参考Arthas的用户文档:alibaba.github.io/arthas/inde…
上面已经给你们介绍了强大的Arthas,有不少人也想作一个能够动态替换Class的工具,可是这种东西过于底层,比较小众,入门的门槛相对来讲比较高。可是jvm-sandbox,给咱们提供了用通俗易懂的编码方式来动态替换Class。
对于AOP来讲你们确定对其不陌生,在Spring中咱们能够很方便的实现一个AOP,可是这样有两个缺点:一个是只能针对Spring中的Bean进行加强,还有个是加强以后若是要修改加强内容那么就只能重写而后发布项目,不能动态的加强。
JVM Sandbox 利用 HotSwap 技术在不重启 JVM的状况下实现:
也就是咱们能够经过这种技术来完成咱们在arthas的命令。 通常来讲sandbox的适用场景以下:
固然还有更多的场景,他能作什么彻底取决于你的想象,只要你想得出来他就能作到。
sandbox提供了Module的概念,每一个Module都是一个AOP的实例。 好比咱们想完成一个打印全部jdbc statement sql日志的Module,须要建一个下面的Module:
public class JdbcLoggerModule implements Module, LoadCompleted { private final Logger smLogger = LoggerFactory.getLogger("DEBUG-JDBC-LOGGER"); @Resource private ModuleEventWatcher moduleEventWatcher; @Override public void loadCompleted() { monitorJavaSqlStatement(); } // 监控java.sql.Statement的全部实现类 private void monitorJavaSqlStatement() { new EventWatchBuilder(moduleEventWatcher) .onClass(Statement.class).includeSubClasses() .onBehavior("execute*") /**/.withParameterTypes(String.class) /**/.withParameterTypes(String.class, int.class) /**/.withParameterTypes(String.class, int[].class) /**/.withParameterTypes(String.class, String[].class) .onWatch(new AdviceListener() { private final String MARK_STATEMENT_EXECUTE = "MARK_STATEMENT_EXECUTE"; private final String PREFIX = "STMT"; @Override public void before(Advice advice) { advice.attach(System.currentTimeMillis(), MARK_STATEMENT_EXECUTE); } @Override public void afterReturning(Advice advice) { if (advice.hasMark(MARK_STATEMENT_EXECUTE)) { final long costMs = System.currentTimeMillis() - (Long) advice.attachment(); final String sql = advice.getParameterArray()[0].toString(); logSql(PREFIX, sql, costMs, true, null); } } .... }); } } 复制代码
monitorJavaSqlStatement是咱们的核心方法。流程以下:
Arthas是一款很优秀的Java线上问题诊断工具,Sandbox的做者没有选择和Arthas去作一个功能很全的工具平台,而选择了去作一款底层中台,让更多的人能够很轻松的去实现字节码加强相关的工具。若是说Arthas是一把锋利的剑能斩杀万千敌人,那么jvm-sandbox就是打造一把好剑的模子,等待着你们去打造一把属于本身的绝世好剑。
sadbox介绍得比较少,有兴趣的同窗能够去github上自行了解:github.com/alibaba/jvm…
不论上咱们的Arthas仍是咱们的jvm-sandbox无外乎使用的就是下面几种技术:
对于ASM字节码修改技术能够参考我以前写的几篇文章:
对于ASM修改字节码的技术这里就不作多余阐述。
Instrumentation是JDK1.6用来构建Java代码的类。Instrumentation是在方法中添加字节码来达到收集数据或者改变流程的目的。固然他也提供了一些额外功能,好比获取当前JVM中全部加载的Class等。
Java提供了两种方法获取Instrumentation,下面介绍一下这两种:
4.2.1.1 premain
在启动的时候,会调用preMain方法:
public static void premain(String agentArgs, Instrumentation inst) { } 复制代码
须要在启动时添加额外命令
java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ] 复制代码
也须要在maven中配置PreMainClass。
在教你用Java字节码作日志脱敏工具中很详细的介绍了premain
4.2.1.2 agentmain
premain是Java SE5开始就提供的代理方式,给了开发者诸多惊喜,不过也有些须不变,因为其必须在命令行指定代理jar,而且代理类必须在main方法前启动。所以,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等,在有些场合下,这是比较困难的。好比正常的生产环境下,通常不会开启代理功能,全部java SE6以后提供了agentmain,用于咱们动态的进行修改,而不须要在设置代理。在 JavaSE6文档当中,开发者也许没法在 java.lang.instrument包相关的文档部分看到明确的介绍,更加没法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。
Attach API 不是Java的标准API,而是Sun公司提供的一套扩展 API,用来向目标JVM”附着”(Attach)代理工具程序的。有了它,开发者能够方便的监控一个JVM,运行一个外加的代理程序。
在VirtualMachine中提供了attach的接口
本文实现的HotSwap的代码均在https://github.com/lzggsimida123/hotswapsample中,下面简单介绍一下:
redefineClasses容许咱们从新替换JVM中的类,咱们如今利用它实现一个简单的需求,咱们有下面一个类:
public class Test1 implements T1 { public void sayHello(){ System.out.println("Test1"); } } 复制代码
在sayHello中打印Test1,而后咱们在main方法中循环调用sayHello:
public static void main(String[] args) throws Exception { Test1 tt = new Test1(); int max = 20; int index = 0; while (++index<max){ Thread.sleep(100L); } } 复制代码
若是咱们不作任何处理,那么确定打印出20次Test1。若是咱们想完成一个需求,这20次打印是交替打印出Test1,Test2,Test3。那么咱们能够借助redefineClass。
//获取Test1,Test2,Test3的字节码 List<byte[]> bytess = getBytesList(); int index = 0; for (Class<?> clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("Test1")) { while (true) { //根据index获取本次对应的字节码 ClassDefinition classDefinition = new ClassDefinition(clazz, getIndexBytes(index, bytess)); // redefindeClass Test1 inst.redefineClasses(classDefinition); Thread.sleep(100L); index++; } } } 复制代码
能够看见咱们获取了三个calss的字节码,在咱们根目录下面有,而后调用redefineClasses替换咱们对应的字节码,能够看见咱们的结果,将Test1,Test2,Test3打印出来。
redefineClasses直接将字节码作了交换,致使原始字节码丢失,局限较大。使用retransformClasses配合咱们的Transformer进行转换字节码。一样的咱们有下面这个类:
public class TestTransformer { public void testTrans() { System.out.println("testTrans1"); } } 复制代码
在testTrans中打印testTrans1,咱们有下面一个main方法:
public static void main(String[] args) throws Exception { TestTransformer testTransformer = new TestTransformer(); int max = 20; int index = 0; while (++index<max){ testTransformer.testTrans(); Thread.sleep(100L); } 复制代码
若是咱们不作任何操做,那么确定打印的是testTrans1,接下来咱们使用retransformClasses:
while (true) { try { for(Class<?> clazz : inst.getAllLoadedClasses()){ if (clazz.getName().equals("TestTransformer")) { inst.retransformClasses(clazz); } } Thread.sleep(100L); }catch (Exception e){ e.printStackTrace(); } } 复制代码
这里只是将咱们对应的类尝试去retransform,可是须要Transformer:
//必须设置true,才能进行屡次retrans inst.addTransformer(new SampleTransformer(), true); 复制代码
上面添加了一个Transformer,若是设置为false,这下次retransform一个类的时候他不会执行,而是直接返回他已经执行完以后的代码。若是设置为true,那么只要有retransform的调用就会执行。
public class SampleTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (!"TestTransformer".equals(className)){ //返回Null表明不进行处理 return null; } //进行随机输出testTrans + random.nextInt(3) ClassReader reader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor classVisitor = new SampleClassVistor(Opcodes.ASM5,classWriter); reader.accept(classVisitor,ClassReader.SKIP_DEBUG); return classWriter.toByteArray(); } } } 复制代码
这里的SampleTransFormer使用ASM去对代码进行替换,进行随机输出testTrans + random.nextInt(3)。能够看有下面的结果:
上面的代码已经上传至github:github.com/lzggsimida1…
A:执行顺序以下:
在同一级当中,按照添加顺序进行处理。
A:redefineClass的class不可找回到之前的,不会触发咱们的Transformer,retransClass会根据当前的calss而后依次执行Transformer作class替换。
A:在jdk文档中的解释是,不会影响当前调用,会在本次调用结束之后才会加载咱们替换的class。
A: 从新转换能够会更改方法体、常量池和属性。从新转换不能添加、删除或重命名字段或方法、更改方法的签名或更改继承。将来版本会取消(java8没有取消) 5. 哪些类字节码不能转换?
A:私有类,好比Integer.TYPE,和数组class。
6.JIT的代码怎么办?
A:清除原来JIT代码,而后从新走解释执行的过程。
7.arthas和jvm-sandbox性能影响?
A:因为添加了部分逻辑,确定会有影响,而且替换代码的时候须要到SafePoint的时候才能替换,进行STW,若是替换代码过于频繁,那么会频繁执行STW,这个时候会影响性能。
今年阿里开源的arthas和jvm-sandbox推进了Java线上诊断工具的发展。你们之后遇到一些难以解决的线上问题,那么arthas确定是你的首选目标工具之一。固然若是你想要作本身的一些日志收集,Mock平台,故障模拟等公共的组件,jvm-sandbox可以很好的帮助你。同时了解他们的底层原理也能对你在调优或者排查问题的时候起很大的帮助做用。字数有点多,但愿你们能学习到有用的知识。
参考文档: