接上文:《??性能优化那些事儿(一)??》《??性能优化那些事儿(二)??》讨论完性能优化的方面和策略,这次我们的文章比较技术,分享如何开发自己的性能分析工具(基于虚拟机)。“新”知识考虑到我们大多数人还是以业务开发为主,Java中一些“鲜为人知”的API可能并不为很多人所知。这里简单介绍一下。如果您想更深入地研究,只需自己谷歌一下。.JVMTI(JVMToolInterface)是Java虚拟机提供的原生编程接口,即底层相关调试接口调用。我们熟悉的Java调试其实就是基于它的。Instrumentation,虽然Java提供了JVMTI,但是需要用C/C++开发相应的agent,对Java开发者不是很友好。因此,在JavaSE5的新特性中加入了Instrumentation机制。通过Instrumentation,开发者可以构建一个基于Java的Agent来监控或操作JVM,例如替换或修改某些类的定义。有了以上两个知识,我们其实就可以开发一个简单的Agent了。Instrumentation可以理解为JVM层面的AOP(AspectOrientedProgramming)。通过在应用启动时挂载Agent,我们可以进行查看和修改。ASMASM是一个通用的Java字节码操作和分析框架。它可用于修改现有类文件或动态生成类文件。结合Instrumentation,我们可以在挂载Agent的时候修改字节码。加上我们需要的性能监控方法。学习ASM比较困难,需要了解字节码。但由于其优异的性能,是各种工具修改字节码的首选,比如大家熟悉的Cglib。Javassist仍然是一个字节码修改工具,但是对初学者更加友好。你不需要对字节码级别了解太多。您可以编写Java语法片段来修改现有的类字节。缺点是过于模板化,难以优化,并且功能有限。我们在做性能分析工具的时候,希望把插入字节码对已有代码的影响降到最低,注入速度尽可能快,所以一般会选择ASM作为首选。那么,引入Instrumentation和ASM之后,是不是可以满足做性能分析工具的前提条件呢?可以看到我们通过Instrumentation在JVM层面进行AOP,然后通过ASM修改JAVA的字节码,然后就可以开始完成性能分析最重要的埋点环节了。看起来没什么问题,但是没人要我们增强修改代码保存在内存中,分析一次就对环境造成不可逆的破坏。Instrumentation可以通过addTransformer添加一个字节码转换器,或者将字节码恢复到原来的状态(只需要removeTransformer再retransformClasses就可以恢复),但是javaAgent毕竟是一个单独的jar包,它也有一些依赖,所以加载它进来难免会导致新的Class加载,甚至引发Class冲突。那么一个新的问题出现了,javaAgent如何不影响已有的类呢?ClassLoader类加载器,我们可以使用一个新的类加载器来专门加载javaAgent中的类库,这样就可以解决agent的类引起的冲突问题。在旧版本的JDK中,我们很难卸载ClassLoader,而classClassLoader的卸载非常麻烦,限制也很多。幸运的是,我们现在大多数人都在使用jdk1.8。只要我们遵循类卸载的规则,清理ClassLoader还是很容易的。额外的类加载器实现了业务代码和Agent代码类的隔离,让他们可以安全的引用封装和卸载Agent类,但是这同时引入了一个新的问题。班级是孤立的。当我增强业务代码时,如何将信息传递给代理代码?增强代码必须加载到AppClassLoader中。如何与AgentClassLoader通信?BootStrapClassLoader启动类加载器,它由JVM在启动时创建。要理解这部分知识,就必须理解ClassLoader的双亲委托机制。我们可以创建一个非常简单的Spy类和一个SpyHandler接口。Spy类定义了一些静态方法供代码增强时调用,SpyHandler类定义了一些用于通信和参数传递的接口。我们将这两个类打包成一个jar包,通过Instrumentation的appendToBootstrapClassLoaderSearch接口在加载agent的时候引入BootStrapClassLoader类,这样我们就可以访问各个ClassLoader中的Spy类和SpyHandler接口。通过上面的介绍,我们现在可以制作自己的APM工具了。通过Instrumentation+ASM,我们可以实现对Class文件的修改和增强,甚至可以对String等JDK自带的类进行修改。我们可以通过自定义的ClassLoaderAgent类和业务类来隔离它们,通过进入BootStrap的Spy,实现ClassLoader之间的通信。一切准备就绪,我们现在可以开始实施我们自己的APM工具了!打住,其实上面的功能不需要我们自己一一实现,不需要我们重复造轮子,来自阿里巴巴开源项目的JVM-SANDBOX已经登场了。本项目屏蔽了ASM难用的缺点,简化了Instrumentation的打桩过程,实现了ClassLoader的隔离。它在BootStrapClassLoader中也有Spy类。我们在这个框架的基础上更容易开发。原图链接:https://github.com/alibaba/jvm-sandbox/wiki/img/jvm-sandbox-classloader.png我们有JVM-SANDBOX这个武器,貌似可以省不少钱时间到了,我们终于可以开始分析了。那么如何进行性能分析呢?Zipkin,开源链接跟踪。Jaeger,开源的链接跟踪支持Zipkin协议,个人认为更加人性化。我们可以引入Zipkin或者Jaeger作为采集器和UI展示,根据自己的喜好选择好用的开源工具。通过沙箱提供的功能,我们可以很方便的编写埋点代码,将我们的链接跟踪工具集成到Agent中,最终实现非侵入式的自定义链接跟踪。通过集成ZipkinClient或JaegerClient,我们可以采集埋点。我们似乎以积木的形式组装一些功能来解决一个相当复杂的实现。这就是开源的魅力。其实我们在实际过程中还是遇到了一些难点,比如如何跟踪异步调用,如何跟踪跨线程调用,如何处理线程池,如何处理ForkJoin?最复杂的是如何处理那些跨线程调度,以及我们如何在多个线程之间传递链接的上下文。JDK的InheritableThreadLocal类可以完成从父线程到子线程的值传递。但是,在使用线程池等执行组件池复用线程的情况下,线程由线程池创建,线程被池化,重复使用;此时,父子线程关系的ThreadLocal值传递是没有意义的,应用程序需要将任务提交到线程池时的ThreadLocal值传递给任务执行时。可能难以理解,但一般来说,无论是ThreadLocal还是InheritableThreadLocal,都无法处理ForkJoin带来的线程池或线程重用的副作用,即无法有效准确地传递链接的上下文。如果你不相信我,你可以试一试。那么如何解决这个问题呢?对,就是修改JDK源码,让线程池在调度的时候具备安全准确传递上下文信息的能力,比如增强Runnable和Callable接口,让它们可以承载线程上下文。如果我们要增强JDK代码,就需要非常熟悉线程调度、线程池、Forkjoin的源码,还需要小心处理值的传递以确保安全,这听起来很危险也很困难.别担心,这不是我们第一次遇到这种问题。我们又转向了阿里的开源产品TTL。该库解决了上述问题。但是,找到开源产品不一定能解决所有问题。transmittable-thread-local虽然可以解决线程复用时的传值问题,但是其实现对JDK代码进行了“过度”修改,使得Instrumentation无法进行动态增强,需要对JDK源码进行增强时启动时不加载到ClassLoader中,不能对加载的JDK源码进行动态增强,也就是说这种增强只能发生在开始的时候,不能发生在中间的时候,也不能卸载。这是因为Instrumentation的redefineClasses方法有限制:重定义不能添加、删除、重命名字段或方法;方法签名和继承关系是不能改变的(不然那些商业化的热重载技术怎么赚钱。。。)。TTL的增强违背了这个原则,我们需要对其进行修改,将其集成到Agent中。这种修改很无聊,也很难解释。大家可以直接看修改后的JVM-SANDBOX。为了后续使用方便,我们直接用BootStrapClassLoader把TTL库加载进去。开源最终开源的性能分析工具可以在这里找到:https://github.com/tmtbe/PVisualization,配合修改后的JVM-SANDBOX,可以实现360度无死角的性能链路跟踪分析,而且开发埋点也很方便,不需要考虑任何线程池的问题。原图链接:https://github.com/tmtbe/PVisualization/raw/master/source/img.png
