乐趣区

关于java:骚操作不重启-JVM如何替换掉已经加载的类

送大家以下 java 学习材料

  • Java 对象行为
  • java.lang.instrument.Instrumentation
  • 间接操作字节码
  • BTrace
  • Arthas
  • 三生万物

在边远的希艾斯星球爪哇国塞沃城中,两名年老的程序员正在为一件事件苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:

“Debug 一下吧。”

“线上机器,没开 Debug 端口。”

“看日志,看看申请值和返回值别离是什么?”

“那段代码没打印日志。”

“改代码,加日志,从新公布一次。”

“狐疑是线程池的问题,重启会毁坏现场。”

长达几十秒的缄默之后:“据说,排查问题的最高境界,就是只通过 Review 代码来发现问题。”

比几十秒长几十倍的缄默之后:“我轮询了那段代码一十七遍之后,终于得出一个论断。”

“论断是?”

“我还没达到只通过 Review 代码就能发现问题的至高境界。”

Java 对象行为

文章结尾的问题实质上是动静扭转内存中已存在对象的行为问题。

所以,得先弄清楚 JVM 中和对象行为无关的中央在哪里,有没有更改的可能性。

对象应用两种货色来形容事物:行为和属性。

举个例子:

publicclass Person{

  privateint age;
  private String name;

  public void speak(String str) {System.out.println(str);
 }

  public Person(int age, String name) {
    this.age = age;
    this.name = name;
  }
}

下面 Person 类中 age 和 name 是属性,speak 是行为。对象是类的实例,每个对象的属性都属于对象自身,然而每个对象的行为却是公共的。举个例子,比方咱们当初基于 Person 类创立了两个对象,personA 和 personB:

Person personA = new Person(43, "lixunhuan");
personA.speak("我是李寻欢");
Person personB = new Person(23, "afei");
personB.speak("我是阿飞");

personA 和 personB 有各自的姓名和年龄,然而有独特的行为:speak。设想一下,如果咱们是 Java 语言的设计者,咱们会怎么存储对象的行为和属性呢?

“很简略,属性跟着对象走,每个对象都存一份。行为是公共的货色,抽离进去,独自放到一个中央。”

“咦?抽离出公共的局部,跟代码复用如同啊。”

“大道至简,很多货色原本都是必由之路。”

也就是说,第一步咱们首先得找到存储对象行为的这个公共的中央。一番搜寻之后,咱们发现这样一段形容:

Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.

Java 的对象行为(办法、函数)是存储在办法区的。

“办法区中的数据从哪来?”

“办法区中的数据是类加载时从 class 文件中提取进去的。”

“class 文件从哪来?”

“从 Java 或者其余合乎 JVM 标准的源代码中编译而来。”

“源代码从哪来?”

“废话,当然是手写!”

“倒着推,手写没问题,编译没问题,至于加载……有没有方法加载一个曾经加载过的类呢?如果有的话,咱们就能批改字节码中指标办法所在的区域,而后从新加载这个类,这样办法区中的对象行为(办法)就被扭转了,而且不扭转对象的属性,也不影响曾经存在对象的状态,那么就能够搞定这个问题了。

可是,这岂不是违反了 JVM 的类加载原理?毕竟咱们不想扭转 ClassLoader。”

“少年,能够去看看java.lang.instrument.Instrumentation。”

java.lang.instrument.Instrumentation

看完文档之后,咱们发现这么两个接口:redefineClasses 和 retransformClasses。一个是从新定义 class,一个是批改 class。这两个大同小异,看 redefineClasses 的阐明:

This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.

都是替换曾经存在的 class 文件,redefineClasses 是本人提供字节码文件替换掉已存在的 class 文件,retransformClasses 是在已存在的字节码文件上批改后再替换之。

当然,运行时间接替换类很不平安。比方新的 class 文件援用了一个不存在的类,或者把某个类的一个 field 给删除了等等,这些状况都会引发异样。所以如文档中所言,instrument 存在诸多的限度:

The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.

咱们能做的基本上也就是简略批改办法内的一些行为,这对于咱们结尾的问题,打印一段日志来说,曾经足够了。当然,咱们除了通过 retransform 来打印日志,还能做很多其余十分有用的事件,这个下文会进行介绍。

那怎么失去咱们须要的 class 文件呢?一个最简略的办法,是把批改后的 Java 文件从新编译一遍失去 class 文件,而后调用 redefineClasses 替换。然而对于没有(或者拿不到,或者不不便批改)源码的文件咱们应该怎么办呢?其实对于 JVM 来说,不论是 Java 也好,Scala 也好,任何一种合乎 JVM 标准的语言的源代码,都能够编译成 class 文件。

JVM 的操作对象是 class 文件,而不是源码。所以,从这种意义上来讲,咱们能够说“JVM 跟语言无关”。既然如此,不论有没有源码,其实咱们只须要批改 class 文件就行了。

间接操作字节码

Java 是软件开发人员能读懂的语言,class 字节码是 JVM 能读懂的语言,class 字节码最终会被 JVM 解释成机器能读懂的语言。无论哪种语言,都是人发明的。

所以,实践上(实际上也的确如此)人能读懂上述任何一种语言,既然能读懂,天然能批改。只有咱们违心,咱们齐全能够跳过 Java 编译器,间接写字节码文件,只不过这并不合乎时代的倒退罢了,毕竟高级语言设计之始就是为咱们人类所服务,其开发效率也比机器语言高很多。

对于人类来说,字节码文件的可读性远远没有 Java 代码高。尽管如此,还是有一些卓越的程序员们发明出了能够用来间接编辑字节码的框架,提供接口能够让咱们不便地操作字节码文件,进行注入批改类的办法,动静发明一个新的类等等操作。其中最驰名的框架应该就是 ASM 了,cglib、Spring 等框架中对于字节码的操作就建设在 ASM 之上。

咱们都晓得,Spring 的 AOP 是基于动静代理实现的,Spring 会在运行时动态创建代理类,代理类中援用被代理类,在被代理的办法执行前后进行一些神秘的操作。

那么,Spring 是怎么在运行时创立代理类的呢?动静代理的美好之处,就在于咱们不用手动为每个须要被代理的类写代理类代码,Spring 在运行时会依据须要动静地发明出一个类。这里发明的过程并非通过字符串写 Java 文件,而后编译成 class 文件,而后加载。Spring 会间接“发明”一个 class 文件,而后加载,发明 class 文件的工具,就是 ASM 了。

到这里,咱们晓得了用 ASM 框架间接操作 class 文件,在类中加一段打印日志的代码,而后 retransform 就能够了。

BTrace

截止到目前,咱们都是停留在实践形容的层面。那么如何进行实现呢?先来看几个问题:

  1. 在咱们的工程中,谁来做这个寻找字节码,批改字节码,而后 retransform 的动作呢?咱们并非先知,不可能晓得将来有没有可能遇到文章结尾的这种问题。思考到性价比,咱们也不可能在每个工程中都开发一段专门做这些批改字节码、从新加载字节码的代码。
  2. 如果 JVM 不在本地,在近程呢?
  3. 如果连 ASM 都不会用呢?能不能更通用一些,更“傻瓜”一些。

侥幸的是,因为有 BTrace 的存在,咱们不用本人写一套这样的工具了。什么是 BTrace 呢?BTrace 曾经开源,我的项目形容极其简短:

A safe, dynamic tracing tool for the Java platform.

BTrace 是基于 Java 语言的一个平安的、可提供动静追踪服务的工具。BTrace 基于 ASM、Java Attach API、Instrument 开发,为用户提供了很多注解。依附这些注解,咱们能够编写 BTrace 脚本(简略的 Java 代码)达到咱们想要的成果,而不用深陷于 ASM 对字节码的操作中不可自拔。

看 BTrace 官网提供的一个简略例子:拦挡所有 java.io 包中所有类中以 read 结尾的办法,打印类名、办法名和参数名。当程序 IO 负载比拟高的时候,就能够从输入的信息中看到是哪些类所引起,是不是很不便?

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
import com.sun.btrace.AnyType;
importstatic com.sun.btrace.BTraceUtils.*;

/**
 * This sample demonstrates regular expression
 * probe matching and getting input arguments
 * as an array - so that any overload variant
 * can be traced in "one place". This example
 * traces any "readXX" method on any class in
 * java.io package. Probed class, method and arg
 * array is printed in the action.
 */
@BTracepublicclass ArgArray {
    @OnMethod(
        clazz="/java\\.io\\..*/",
        method="/read.*/"
    )
    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args) {println(pcn);
        println(pmn);
        printArray(args);
    }
}

再来看另一个例子:每隔 2 秒打印截止到以后创立过的线程数。

package com.sun.btrace.samples;

import com.sun.btrace.annotations.*;
importstatic com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.Export;

/**
 * This sample creates a jvmstat counter and
 * increments it everytime Thread.start() is
 * called. This thread count may be accessed
 * from outside the process. The @Export annotated
 * fields are mapped to jvmstat counters. The counter
 * name is "btrace." + <className> + "." + <fieldName>
 */
@BTracepublicclass ThreadCounter {

    // create a jvmstat counter using @Export
    @Exportprivatestaticlong count;

    @OnMethod(
        clazz="java.lang.Thread",
        method="start"
    )
    public static void onnewThread(@Self Thread t) {
        // updating counter is easy. Just assign to
        // the static field!
        count++;
    }

    @OnTimer(2000)
    public static void ontimer() {
        // we can access counter as "count" as well
        // as from jvmstat counter directly.
        println(count);
        // or equivalently ...
        println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count"));
    }
}

看了下面的用法是不是有所启发?忍不住冒出来许多想法。比方查看 HashMap 什么时候会触发 rehash,以及此时容器中有多少元素等等。

有了 BTrace,文章结尾的问题能够失去完满的解决。至于 BTrace 具体有哪些性能,脚本怎么写,这些 Git 上 BTrace 工程中有大量的阐明和举例,网上介绍 BTrace 用法的文章更是恒河沙数,这里就不再赘述了。

咱们明确了原理,又有好用的工具反对,剩下的就是施展咱们的创造力了,只需在适合的场景下正当地进行应用即可。

既然 BTrace 能解决下面咱们提到的所有问题,那么 BTrace 的架构是怎么的呢?

BTrace 次要有上面几个模块:

  1. BTrace 脚本:利用 BTrace 定义的注解,咱们能够很不便地依据须要进行脚本的开发。
  2. Compiler:将 BTrace 脚本编译成 BTrace class 文件。
  3. Client:将 class 文件发送到 Agent。
  4. Agent:基于 Java 的 Attach API,Agent 能够动静附着到一个运行的 JVM 上,而后开启一个 BTrace Server,接管 client 发过来的 BTrace 脚本;解析脚本,而后依据脚本中的规定找到要批改的类;批改字节码后,调用 Java Instrument 的 retransform 接口,实现对对象行为的批改并使之失效。

整个 BTrace 的架构大抵如下:

btrace 工作流程

BTrace 最终借 Instrument 实现 class 的替换。如上文所说,出于平安思考,Instrument 在应用上存在诸多的限度,BTrace 也不例外。BTrace 对 JVM 来说是“只读的”,因而 BTrace 脚本的限度如下:

  1. 不容许创建对象
  2. 不容许创立数组
  3. 不容许抛异样
  4. 不容许 catch 异样
  5. 不容许随便调用其余对象或者类的办法,只容许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输入工具)
  6. 不容许扭转类的属性
  7. 不容许有成员变量和办法,只容许存在 static public void 办法
  8. 不容许有外部类、嵌套类
  9. 不容许有同步办法和同步块
  10. 不容许有循环
  11. 不容许随便继承其余类(当然,java.lang.Object 除外)
  12. 不容许实现接口
  13. 不容许应用 assert
  14. 不容许应用 Class 对象

如此多的限度,其实能够了解。BTrace 要做的是,尽管批改了字节码,然而除了输入须要的信息外,对整个程序的失常运行并没有影响。

Arthas

BTrace 脚本在应用上有肯定的学习老本,如果能把一些罕用的性能封装起来,对外间接提供简略的命令即可操作的话,那就再好不过了。阿里的工程师们早已想到这一点,就在去年,阿里巴巴开源了本人的 Java 诊断工具——Arthas

Arthas 提供简略的命令行操作,功能强大。究其背地的技术原理,和本文中提到的大抵无二。Arthas 的文档很全面,想具体理解的话能够戳这里。

本文旨在阐明 Java 动静追踪技术的前因后果,把握技术背地的原理之后,只有违心,各位读者也能够开发出本人的“冰封王座”进去。

三生万物

当初,让咱们试着站在更高的中央“鸟瞰”这些问题。

Java 的 Instrument 给运行时的动静追踪留下了心愿,Attach API 则给运行时动静追踪提供了“出入口”,ASM 则大大不便了“人类”操作 Java 字节码的操作。

基于 Instrument 和 Attach API 前辈们发明出了诸如 JProfiler、Jvisualvm、BTrace 这样的工具。以 ASM 为根底倒退出了 cglib、动静代理,继而是利用宽泛的 Spring AOP。

Java 是动态语言,运行时不容许扭转数据结构。然而,Java 5 引入 Instrument,Java 6 引入 Attach API 之后,事件开始变得不一样了。尽管存在诸多限度,然而,在前辈们的致力下,仅仅是利用预留的近似于“只读”的这一点点狭小的空间,依然发明出了各种大放异彩的技术,极大地提高了软件开发人员定位问题的效率。

计算机应该是人类有史以来最平凡的创造之一,从电磁感应磁生电,到高下电压模仿 0 和 1 的比特,再到二进制示意出几种根本类型,再到根本类型示意出无穷的对象,最初无穷的对象组合交互模仿现实生活乃至整个宇宙。

两千五百年前,《道德经》有言:“道生一,毕生二,二生三,三生万物。”

两千五百年后,计算机的倒退过程也大抵如此吧。

退出移动版