Arthas, 字节码操作
一个线上的NPE
-
问题
- 没有更多updateTerToLastStatus相关的日志
- 业务不接受重启
- 重启会破坏现场, 短时间无法复现bug
- 现在只靠review code基本无法发现问题
-
使用Arthas
-
根据服务端口找到进程号
-
根据进程号启动arthas, 执行trace
NPE出现在
#40
-
然后使用watch打印异常和参数
可以看到具体的异常信息和参数
10015729
-
验证猜测, 使用getstatic+ognl
-
-
最后关于最开始的NPE信息
npe会被jvm当热点异常优化掉, 当npe输出过多的时候信息会变少, 故去找最早的异常, 可以看到更多的异常信息;
回忆jsp(java server page)
最早的动态页面是在cgi时代, 这个时候都是脚本, 直接改代码加日志加好.
后来在jsp这种动态页面时代, tomcat这种web容器会判断jsp文件是否被更改, 如果更改, 就将jsp翻译成一个新的servlet类, 加载到jvm中去, 之后的所有请求, 都由新的servlet处理; 问题是根据类加载机制(bootstrap->extension->app->自定义类加载器), 同一个类只能被加载一次, 那jsp是如何实现的呢?
tomcat 自定义了Jsp classLoader来实现servlet类的替换, 当jsp文件更改, 会删除就的jsp classloader, 换成新的;
HTTP服务是无状态的,所以JSP的场景基本上都是一次性消费,这种通过创建新的ClassLoader来“替换”class的做法行得通,但是对于其他应用,比如Spring框架,即便这样做了,对象多数是单例,对于内存中已经创建好的对象,我们无法通过这种创建新的ClassLoader实例的方法来修改对象行为。
Instrumentation
一个类分成两个部分, 属性和方法, 方法时存在方法区中, 类变量应该是存在堆上, 函数变量应该是存在栈上
总之: 我们可以修改字节码中目标方法所在的区域, 然后重新加载这个类, 这样方法区中的对象行为(方法)就被改变了,而且不改变对象的属性,也不影响已经存在对象的状态;
-
java.lang.instrument.Instrumentation定义了两个接口
redefineClasses
和retransformClasses
; 一个是重新定义class,一个是修改class。这两个大同小异,都是替换已经存在的class文件,redefineClasses是自己提供字节码文件替换掉已存在的class文件,retransformClasses是在已存在的字节码文件上修改后再替换之。-
redefineClasses
对于有java源码的类, 我们直接重新编译一下得到字节码文件, 然后redefineClasses就好
-
retransformClasses
对于没有java源码的类, 咋整?直接使用直接编辑字节码的框架, 如
Byte Buddy
,ASM
,他们提供接口可以让我们方便地操作字节码文件,进行注入修改类的方法,动态创造一个新的类等等操作。其中最著名的框架应该就是ASM了,cglib、Spring等框架中对于字节码的操作就建立在ASM之上。我们都知道,Spring的AOP是基于动态代理实现的,Spring会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。那么,Spring是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring在运行时会根据需要动态地创造出一个类,这里创造的过程并非通过字符串写Java文件,然后编译成class文件,然后加载。Spring会直接“创造”一个class文件,然后加载,创造class文件的工具,就是ASM了。
到这里,我们知道了用ASM框架直接操作class文件,在类中加一段打印日志的代码,然后调用retransformClasses就可以了。
-
增强版直接操作字节码的工具: BTrace
BTrace基于ASM、Java Attach Api、Instruments开发,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)达到我们想要的效果,而不必深陷于ASM对字节码的操作中不可自拔。 它帮我们寻找字节码,修改字节码,然后reTransform
BTrace主要有下面几个模块:
- BTrace脚本:利用BTrace定义的注解,我们可以很方便地根据需要进行脚本的开发。
- Compiler:将BTrace脚本编译成BTrace class文件。
- Client:将class文件发送到Agent。
- Agent:基于Java的Attach Api,Agent可以动态附着到一个运行的JVM上(Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类),然后开启一个BTrace Server,接收client发过来的BTrace脚本;解析脚本,然后根据脚本中的规则找到要修改的类;修改字节码后,调用Java Instrument的reTransform接口,完成对对象行为的修改并使之生效。
BTrace对JVM来说是“只读的”, 不允许对 JVM 状态做有副作用的改动,因此BTrace的使用有许多限制, 如: 不允许有内部类、嵌套类, 不允许有同步方法和同步块(避免死锁风险。如果 BTrace 脚本中使用锁,可能和业务线程争锁造成死锁), 不允许创建对象(避免 GC 压力或内存泄露,防止你在 BTrace 中写内存泄露代码), 不能抛异常(避免改变原业务逻辑流程。BTrace 不应该影响被观测方法的行为)等等!!!!