当前位置: 首页 > 后端技术 > Java

骚操作:如何在不重启JVM的情况下替换加载的类?

时间:2023-04-01 18:07:58 Java

给大家送上下面的java学习资料java对象行为java.lang.instrument.Instrumentation直接操作字节码程序有问题,一时看不出问题出在哪里,于是有了以下对话:“调试它。”“在线机,Debug口没有打开。”“看日志看请求值和返回值是什么?”“那段代码没有打印日志。”“更改代码,添加日志,然后重新发布。”“怀疑是线程池的问题,重启会破坏站点。”几十秒的沉默后:“据说最高级别的故障排查,只有通过Reviewcode才能发现问题。”数十秒以上的沉默后:“轮询了17次代码后,我终于有了结论。”“结论是?”“我还没有达到只能通过Review代码才能发现问题的最高境界。”Java对象行为一文开头的问题本质上是一个动态改变内存中已有对象行为的问题。因此,首先要搞清楚JVM与对象的行为有什么关系,是否有改变的可能。对象使用两种东西来描述事物:行为和属性。例如:publicclassPerson{privateintage;私有字符串名称;publicvoidspeak(Stringstr){System.out.println(str);}publicPerson(intage,Stringname){this.age=age;这。名字=名字;}}在上面的Person类中,age和name是属性,speak是行为。对象是类的实例,每个对象的属性都属于对象本身,但每个对象的行为都是公开的。比如我们现在基于Person类创建两个对象personA和personB:PersonpersonA=newPerson(43,"lixunhuan");personA.speak("我是李寻欢");PersonpersonB=newPerson(23,"afei");personB.speak("我是阿飞");personA和personB有自己的名字和年龄,但是他们有一个共同的行为:说话。想象一下,如果我们是Java语言的设计者,我们将如何存储对象的行为和属性?“很简单,属性跟随对象,每个对象都有一个副本。行为是公共的东西,抽取出来放在单独的地方。”“嗯?提取公共部分类似于代码重用。”““路越简单越好,很多事情都是一个目标。”也就是说,第一步就是要找到这个公共的地方来存放对象behavior.经过一番搜索,我们发现了这样的描述:方法区是在虚拟机启动时创建的,在所有Java虚拟机线程之间共享,它在逻辑上是堆区的一部分。它存储了每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码。Java对象的行为(方法、函数)都存放在方法区。“方法区的数据从哪里来?”“方法区的数据是加载类时从class文件中提取。”“class文件从哪里来?”“从Java或者其他符合JVM规范的源代码编译而来。”“源代码从哪里来?”“废话,当然是手写了!”“往后推,手写没问题,编译没问题,至于加载……有没有办法加载一个已经加载过的类?如果有,我们可以在字节码中修改目标方法所在的区域,然后重新加载类,这样在不改变对象的属性或影响的情况下,改变方法区中的对象行为(方法)对象的状态已经存在,那么这个问题就可以解决了。但是,这不是违反了JVM的类加载原则吗?毕竟,我们不想更改ClassLoader。“年轻人,你可以去看看java.lang.instrument.Instrumentation。”"java.lang.instrument.Instrumentation看了文档,发现有两个接口:redefineClasses和retransformClasses。一个是重定义类,一个是修改类。这两个类似,看redefineClasses的说明:这个方法用于在不引用现有类文件字节的情况下替换类的定义,就像从源代码重新编译以进行修复并继续调试时可能会做的那样。要转换现有类文件字节的地方(例如在字节码中instrumentation)retransformClasses应该是用retransformClasses来替换已有的class文件,redefineClasses是提供字节码文件来替换已有的class文件,retransformClasses是修改已有的字节码文件然后替换,当然runtime直接替换一个类是不安全的,比如新建一个类文件引用了一个不存在的类,或者删除了某个类的字段等等,都会引发异常。所以如文档中所述,对instrument有很多限制:重定义可能会改变方法体、常量池和属性。重新定义不得添加、删除或重命名字段或方法,不得更改方法的签名或更改继承。这些限制可能会在未来的版本中取消。在应用转换之前,不会检查、验证和安装类文件字节,如果生成的字节有误,此方法将抛出异常。基本上我们能做的就是简单的修改方法中的一些行为,这对于我们一开始的问题来说已经足够了,打印一条日志。当然,除了通过retransform打印日志,我们还可以做很多其他非常有用的事情,下面会介绍。那么如何获取我们需要的类文件呢?最简单的方法之一是重新编译修改后的Java文件得到类文件,然后调用redefineClasses替换它。但是对于没有(或无法获取,或不便修改)源代码的文件,我们应该怎么办呢?其实对于JVM来说,不管是Java还是Scala,任何符合JVM规范的语言的源代码都可以编译成class文件。JVM运行的对象是类文件,而不是源代码。所以,从这个意义上说,我们可以说“JVM与语言无关”。既然如此,不管有没有源码,我们只需要修改class文件即可。直接运行字节码Java是软件开发者可以理解的语言,类字节码是JVM可以理解的语言,类字节码最终会被JVM解释成机器可以理解的语言。所有的语言都是人类创造的。因此,理论上(事实上也是如此)人们可以理解上述任何一种语言,既然能够理解,自然就可以对其进行修改。只要我们愿意,我们可以跳过Java编译器,直接写字节码文件,但这不符合时代的发展。高级语言设计毕竟是为我们人类服务的,其开发效率也高于机器。语言要高得多。对于人类来说,字节码文件的可读性远不如Java代码。尽管如此,一些优秀的程序员已经创建了一个可以用来直接编辑字节码的框架,提供了一个接口让我们可以轻松地操作字节码文件、注入方法修改类、动态创建新类等等。其中最著名的框架应该就是ASM了。cglib、Spring等框架中字节码的运行都是基于ASM的。我们都知道Spring的AOP是基于动态代理实现的。Spring会在运行时动态创建一个代理类。代理类是指代理类,在代理方法执行前后进行一些神秘的操作。那么,Spring是如何在运行时创建代理类的呢?动态代理的美妙之处在于我们不必为每个需要代理的类手动编写代理类代码。Spring会在运行时根据需要动态创建一个类。这里创建的过程并不是通过字符串写Java文件,然后编译成class文件,然后加载。Spring会直接“创建”一个类文件,然后加载它。创建类文件的工具是ASM。至此,我们知道可以使用ASM框架直接操作class文件,在类中添加一段代码打印日志,然后重新改造。BTrace到现在为止,我们都停留在理论描述的层面。那么如何实施呢?我们先来看几个问题:在我们的项目中,查找字节码,修改字节码,然后重新转化的动作由谁来做?我们不是预言家,以后会不会遇到本文开头的问题就不得而知了。考虑到成本效益,我们不可能在每个项目中都开发一段专门用于修改字节码和重新加载字节码的代码。如果JVM不是本地的而是远程的怎么办?如果您甚至不能使用ASM怎么办?能不能再笼统一点,再“傻一点”一点。幸运的是,因为BTrace的存在,我们不用自己写一套这样的工具。什么是BTrace?BTrace已经开源,项目描述极其简短:Asafe,dynamictracingtoolfortheJavaplatform。BTrace是一种基于Java语言的安全、动态的跟踪工具。BTrace是基于ASM、JavaAttachAPI和Instrument开发的,为用户提供了很多注解。依靠这些注解,我们可以编写BTrace脚本(简单的Java代码)来实现我们想要的,而不会卡在ASM对字节码的操作中。看BTrace官方提供的一个简单例子:拦截所有java.io包中所有类中以read开头的方法,打印类名、方法名和参数名。当程序的IO负载比较高时,可以从输出信息中看出是哪些类引起的。是不是很方便?packagecom.sun.btrace.samples;importcom.sun.btrace.annotations.*;importcom.sun.btrace.AnyType;importstaticcom.sun.btrace.BTraceUtils.*;/***此示例演示正则表达式*探测匹配并获取输入参数*作为一个数组——这样任何重载变体*都可以在“一个地方”进行追踪。此示例*跟踪*java.io包中任何类的任何“readXX”方法。被探测的类、方法和arg*数组打印在操作中。*/@BTracepublicclassArgArray{@OnMethod(clazz="/java\\.io\\..*/",method="/read.*/")publicstaticvoidanyRead(@ProbeClassNameStringpcn,@ProbeMethodNameStringpmn,AnyType[]args){println(pcn);打印(下午);打印数组(参数);}}再看一个例子:每2秒打印一次,直到当前创建的Threads。packagecom.sun.btrace.samples;importcom.sun.btrace.annotations.*;importstaticcom.sun.btrace.BTraceUtils.*;importcom.sun.btrace.annotations.Export;/***此示例创建了一个每次调用Thread.start()时,jvmstat计数器和*递增它。可以从进程外部访问*这个线程数。@Export注释的*字段映射到jvmstat计数器。计数器*名称是“btrace”。+<类名>+"."+*/@BTracepublicclassThreadCounter{//使用@Export@Exportprivatestaticlongcount创建一个jvmstat计数器;@OnMethod(clazz="java.lang.Thread",method="start")publicstaticvoidonnewThread(@SelfThreadt){//更新计数器很容易。只需分配给//静态字段!计数++;}@OnTimer(2000)publicstaticvoidontimer(){//我们可以像“计数”一样访问计数器//就像直接从jvmstat计数器一样。打印(数数);//或者等价地...println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count"));}}是不是受到上面用法的启发?不由得想出很多点子,比如检查HashMap什么时候会触发rehash,这个时候容器里有多少元素等等。有了BTrace,文章开头的问题就可以完美解决。至于BTrace的具体功能以及如何编写脚本,在Git上的BTrace项目中有大量的讲解和例子。网上介绍BTrace使用的文章更是恒河沙,这里不再赘述。明白了原理,有了好用的工具支持,剩下的就是发挥我们的创造力,在合适的场景中合理使用就好了。既然BTrace可以解决我们上面提到的所有问题,那么BTrace的架构是怎样的呢?BTrace主要有以下几个模块:BTrace脚本:使用BTrace定义的注解,我们可以很方便的根据需要开发脚本。编译器:将BTrace脚本编译成BTrace类文件。客户端:将类文件发送给代理。Agent:基于JavaAttachAPI,Agent可以动态附加到一个正在运行的JVM上,然后启动一个BTraceServer,接收客户端发送的BTrace脚本;解析脚本,然后根据脚本中的规则找到需要修改的类;修改word代码保存后,调用JavaInstrument的retransform接口,完成对对象行为的修改,并使其生效。整个BTrace的结构大致如下:btrace工作流程BTrace最终使用Instrument来代替类。如上所述,出于安全考虑,Instrument的使用有很多限制,BTrace也不例外。BTrace对于JVM是“只读”的,所以BTrace脚本的局限性如下:不允许创建对象,不允许创建数组,不抛出异常,不允许异常,catch异常不允许调用其他对象或类的方法随意,只允许com.sun.btrace.BTraceUtils中提供的静态方法(一些数据处理和信息输出工具)不允许改变类的属性。不允许使用成员变量和方法,只允许使用staticpublicvoid方法。不允许使用内部类和嵌套类不允许使用同步方法和块。不允许循环。不允许其他类继承其他类(当然java.lang.Object除外)。接口是不允许的。不允许断言。BTrace要做的是,虽然修改了字节码,但除了输出需要的信息外,对整个程序的正常运行没有任何影响。ArthasBTrace脚本的使用有一定的学习成本。如果能封装一些常用的功能,直接对外提供简单的命令就好了。阿里巴巴的工程师早就想到了这一点。就在去年,阿里巴巴开源了自己的Java诊断工具——ArthasArthas,命令行操作简单,功能强大。其背后的技术原理与本文提到的大致相同。Arthas的文档非常全面。如果您想了解更多信息,可以单击此处。本文旨在解释Java动态跟踪技术的来龙去脉。在掌握了技术背后的原理后,读者也可以随心所欲地开发属于自己的“冰封王座”。现在,让我们尝试从更高的角度“俯视”这些问题。Java的Instrument为运行时的动态跟踪留下了希望,AttachAPI为运行时的动态跟踪提供了“入口和出口”。ASM极大地方便了“人”对Java字节码的操作。基于Instrument和AttachAPI的前辈们创建了JProfiler、Jvisualvm、BTrace等工具。基于ASM,开发了cglib和动态代理,然后是广泛使用的SpringAOP。Java是一种静态语言,数据结构在运行时是不允许改变的。然而,在Java5引入Instrument和Java6引入AttachAPI之后,事情开始发生变化。虽然有很多局限性,但是在前人的努力下,他们创造了各种辉煌的技术,大大提高了软件开发人员定位问题的效率。计算机应该是人类历史上最伟大的发明之一,从电磁感应到磁电,到高低电压模拟0和1位,到二进制表示几种基本类型,再到表示无限对象的基本类型,最后,无穷无尽的物体组合相互作用,模拟现实生活乃至整个宇宙。两千五百年前,《道德经》说:道生一,一生二,二生三,三生万物。两千五百年后,计算机的发展过程大概也是如此。