阿里线上问题诊断工具Arthas和jvm-sandbox

大纲目录

这篇文章是以前学习Arthas和jvm-sandbox的一些心得和总结,但愿能帮助到你们。本文字较多,能够根据目录进行对应的阅读。html

  • 背景:如今的问题所在?
  • Arthas: Arthas能帮助你干什么?各类命令原理是什么?
  • jvm-sandbox: jvm-sandbox能帮助你干什么?
  • 实现原理?本身如何实现一个?
  • 常见的一些问题?

1.背景

2018年已过,可是在过去的一年里面开源了不少优秀的项目,这里我要介绍两个比较类似的阿里开源项目一个是Arthas,另外一个是jvm-sandbox。这两个项目都是在今年开源的,为何要介绍这两个项目呢?这里先卖个关子,先问下你们不知道是否遇到过下面的场景呢?java

  • 当你线上项目出了问题,可是一打开日志发现,有些地方忘记打了日志,因而你立刻补上日志,而后从新上线。这个在一些上线流程不规范的公司还比较轻松,在一些流程比较严格,好比美团上线的时候就有封禁期,通常就只能9点以后才能上线。有可能这样一拖就耽误了解决问题的黄金时刻。
  • 当你的项目某个接口执行速度较慢,为了排查问题,因而你四处加上每一个方法运行时间。
  • 当你发现某个类有冲突,好像在线上运行的结果和你预期的不符合,手动把线上编译出的class文件下载下来而后反编译,看看究竟class内容是什么。
  • 当代码已经写好准备联调,可是下游业务环境并无准备好,因而你把之前的代码依次进行注释,采用mock的形式又写了一遍方便联调。

以上这些场景,再真正的业务开发中你们或多或少都碰见过,而通常你们的处理方式和我在场景的描述得大致一致。而这里要给你们介绍一下Arthas和jvm-sandbox,若是你学会了这两个项目,上面全部的问题再你手上不再是难事。git

2. Arthas

固然再介绍Arthas以前仍是要给你们说一下Greys,不管是Arthas仍是jvm-sandbox都是从Greys演变而来,这个是2014年阿里开源的一款Java在线问题诊断工具。而Arthas能够看作是他的升级版本,是一款更加优秀的,功能更加丰富的Java诊断工具。 在他的github的READEME中的介绍这款工具能够帮助你作下面这些事:github

  • 这个类从哪一个 jar 包加载的?为何会报各类类相关的 Exception?
  • 我改的代码为何没有执行到?难道是我没 commit?分支搞错了?
  • 遇到问题没法在线上 debug,难道只能经过加日志再从新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上一样没法 debug,线下没法重现!
  • 是否有一个全局视角来查看系统的运行情况?
  • 有什么办法能够监控到JVM的实时运行状态?

下面我将会介绍一下Arthas的一些经常使用的命令和用法,看看是如何解决咱们实际中的问题的,至于安装教程能够参考Arthas的github。sql

2.1 奇怪的类加载错误

相信你们都遇到过NoSuchMethodError这个错误,通常老司机看见这个错误第一反应就是jar包版本号冲突,这种问题通常来讲使用maven的一些插件就能轻松解决。apache

以前遇到个奇怪的问题,咱们有两个服务的client-jar包,有个类的包名和类名均是一致,在编写代码的时候没有注意到这个问题,在编译阶段因为包名和类名都是一致,全部编译阶段并无报错,在线下的运行阶段没有问题,可是测试环境的机器中的运行阶段缺报出了问题。这个和以前的jar包版本号冲突有点不一样,由于在排查的时候咱们想使用A服务的client-jar包的这个类,可是这个jar包的版本号在Maven中的确是惟一的。数组

这个时候Arthas就能够大显神通了。oracle

2.1.1 sc命令

找到对应的类,而后输出下面的命令(用例使用的是官方提供的用例):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来进行获取的:

 

 

 

2.1.2 jad

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包来进行反编译。这里过程比较复杂这里就不进行叙述。

2.2 动态修改日志级别

有不少同窗可能会以为动态修改日志有什么用呢?好像本身也没怎么用过呢? 通常来讲下面这几个场景能够须要:

  • 通常你们日志级别默认是info,有时候须要查看debug的日志可能须要从新上线。
  • 当线上某个应用流量比较大的时候,如何业务出现问题,可能会短期以内产生大量日志,因为日志会写盘,会消耗大量的内存和磁盘IO进一步加剧咱们的问题严重性,进而引发雪崩。 咱们可使用动态修改日志解决咱们上面两个问题,在美团内部开发了一个工具经过LogContext,记录下全部的logConfig而后动态修改更新。可是若是没有这个工具咱们如何动态修改日志呢?

2.2.1 ognl

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中获取中去获取。

2.3 如何知道某个方法是否调用

不少时候咱们方法执行的状况和咱们预期不符合,可是咱们又不知道到底哪里不符合,Arthas的watch命令就能帮助咱们解决这个问题。

2.3.1 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 记录方法的入参出参,以及方法消耗时间。

2.4 如何知道某个方法耗时较多

当某个方法耗时较长,这个时候你须要排查究竟是某一处发生了长时间的耗时,通常这种问题比较难排查,都是经过全链路追踪trace图去进行排查,可是在本地的应用中没有trace图,这个时候须要Arthas的trace命令来进行排查问题。

2.4.1 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。在访问方法以前和以后会进行记录。

2.5 如何使用命令重发请求?

有时候排查一个问题须要上游再次调用这个方法,好比使用postMan等工具,固然Arthas提供了一个命令让替代咱们来回手动请求。

2.5.1 tt

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信息可能会丢失

  • 引用的对象:保存的入参是保存的引用,而不是拷贝,因此若是参数中的内容被修改,那么入参其实也是被修改的。

2.6 一些耗时的方法,常常被触发,如何知道谁调用的?

有时候有些方法很是耗时或者很是重要,须要知道究竟是谁发起的调用,好比System.gc(),有时候若是你发现fullgc频繁是由于System.gc()引发的,你须要查看究竟是什么应用调用的,那么你就可使用下面的命令。

2.6.1

咱们能够输入下面的命令:

$ 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进行进行监视,而后记录当前的堆栈来获取是什么位置进行的调用。

2.7 如何重定义某个类?

有些时候咱们找了全部的命令,发现和咱们的需求并不符合的时候,那么这个时候咱们能够从新定义这个类,咱们能够用使用下面的命令。

2.7.1 redefine

redefine命令提供了咱们能够从新定义jvm中的class,可是使用这个命令以后class不可恢复。咱们首先须要把重写的class编译出来,而后上传到咱们指定的目录,进行下面的操做:

redefine -p /tmp/Test.class
复制代码

能够重定义咱们的Test.class。从而修改逻辑,完成咱们自定义的需求。

2.8 Arthas小结

上面介绍了7种Arthas比较常见的场景和命令。固然这个命令还远远不止这么点,每一个命令的用法也没有局限于我介绍的。尤为是开源之后更多的开发者参与了进来,如今也将其优化成能够有界面的,在线排查问题的方式,来解决去线上安装的各类不便。

更多的命令能够参考Arthas的用户文档:alibaba.github.io/arthas/inde…

3.jvm-sandbox

上面已经给你们介绍了强大的Arthas,有不少人也想作一个能够动态替换Class的工具,可是这种东西过于底层,比较小众,入门的门槛相对来讲比较高。可是jvm-sandbox,给咱们提供了用通俗易懂的编码方式来动态替换Class。

3.1 AOP

对于AOP来讲你们确定对其不陌生,在Spring中咱们能够很方便的实现一个AOP,可是这样有两个缺点:一个是只能针对Spring中的Bean进行加强,还有个是加强以后若是要修改加强内容那么就只能重写而后发布项目,不能动态的加强。

3.2 sanbox能带来什么

JVM Sandbox 利用 HotSwap 技术在不重启 JVM的状况下实现:

  • 在运行期完成对 JVM 中任意类里的任意方法的 AOP 加强
  • 能够动态热插拔扩展模块
  • 经过扩展模块改变任意方法执行的流程

也就是咱们能够经过这种技术来完成咱们在arthas的命令。 通常来讲sandbox的适用场景以下:

  • 线上故障定位
  • 线上系统流控
  • 线上故障模拟
  • 方法请求录制和结果回放
  • 动态日志打印
  • ...

固然还有更多的场景,他能作什么彻底取决于你的想象,只要你想得出来他就能作到。

3.3 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是咱们的核心方法。流程以下:

  1. 首先经过new EventWatchBuilder(moduleEventWatcher)构造一个事件观察者的构造器,经过Builder咱们能够方便的构造出咱们的观察者。
  2. onclass是对咱们须要观察的类进行筛选,includeSubClasses包含全部的子类。
  3. withParameterTypes进一步筛选参数。
  4. onWatch进行观察,采起模板模式,和咱们Spring的AOP很相似,首先在before里面记录下当前的时间,而后在afterReturning中将before的时间取出来获得当前消耗的时间,而后获取当前的sql语句,最后进行打印。

3.4 sandbox小结

Arthas是一款很优秀的Java线上问题诊断工具,Sandbox的做者没有选择和Arthas去作一个功能很全的工具平台,而选择了去作一款底层中台,让更多的人能够很轻松的去实现字节码加强相关的工具。若是说Arthas是一把锋利的剑能斩杀万千敌人,那么jvm-sandbox就是打造一把好剑的模子,等待着你们去打造一把属于本身的绝世好剑。

sadbox介绍得比较少,有兴趣的同窗能够去github上自行了解:github.com/alibaba/jvm…

4.本身实现字节码动态替换

不论上咱们的Arthas仍是咱们的jvm-sandbox无外乎使用的就是下面几种技术:

  • ASM
  • Instrumentation(核心)
  • VirtualMachine

4.1 ASM

对于ASM字节码修改技术能够参考我以前写的几篇文章:

对于ASM修改字节码的技术这里就不作多余阐述。

4.2 Instrumentation

Instrumentation是JDK1.6用来构建Java代码的类。Instrumentation是在方法中添加字节码来达到收集数据或者改变流程的目的。固然他也提供了一些额外功能,好比获取当前JVM中全部加载的Class等。

4.2.1获取Instrumentation

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的接口

4.3 实现HotSwap

本文实现的HotSwap的代码均在https://github.com/lzggsimida123/hotswapsample中,下面简单介绍一下:

4.3.1 redefineClasses

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打印出来。

 

 

4.3.2 retransformClasses

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…

5.常见的一些问题

  1. Q: instrumentation中trans的顺序?我有多个Transformer执行顺序是怎么样的?

A:执行顺序以下:

  • 执行不可retransformClasses的Transformer
  • 执行不可retransformClasses的native-Transformer
  • 执行能够retransformClasses的Transformer
  • 执行能够retransformClasses的native-Transformer

在同一级当中,按照添加顺序进行处理。

  1. Q: redefineClass和retransClass区别?

A:redefineClass的class不可找回到之前的,不会触发咱们的Transformer,retransClass会根据当前的calss而后依次执行Transformer作class替换。

  1. Q:何时替换?会影响我运行的代码吗?

A:在jdk文档中的解释是,不会影响当前调用,会在本次调用结束之后才会加载咱们替换的class。

  1. 从新转换类能改哪些地方?

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可以很好的帮助你。同时了解他们的底层原理也能对你在调优或者排查问题的时候起很大的帮助做用。字数有点多,但愿你们能学习到有用的知识。

参考文档:

相关文章
相关标签/搜索