我很喜欢祖师爷的这句话,我觉得只有了解了最核心的技术,才算得上是精通了这门技术。当精通某项技术之后,在这个体系下的一切,学习、运用、创造才能做到游刃有余、手到擒来;才能更好的发挥个人和想象和创造力,做出更有价值的事情。
当我们从一个Java学徒,逐步会运用Java语言编写项目的,只要1-2年时间,过几年后,也许大家和我会有同样的感觉,这门语言可以想象和发挥的空间似乎受到了限制,我们并不能很好的完成很多手上的工作。我猜测和我们的学习路径有关系,因为大部分学习Java的人,都是从HelloWorld开始学习,然后开始学习变量、函数、逻辑控制、循环、异常等,为了满足成就感,然后就开始项目了。很多速成教程也是为了让我们快速使用,而没有深入的体系化介绍Java的原理和技术栈,本文尝试通过围绕Java 虚拟机 (JVM)和 字节码 (ByteCode)相关的技术内容,以及其衍生出来的应用场景,进行一个整体的串联,目的是希望大家对Java核心技术有一个重新的认识。
我先提供一下本文的KeyWord,用于用于读者快速了解我接下来要讲的内容,各位也可以根据自己了解这些技术关键字的数量和深度,估算一下自己是否对这些Java的核心技术了解状况
JVM、ByteCode、 ASM 、AspectJ、 CGLIB 、Instrumentation、javaagent、JVMTI、Btrace、byteman
Pluggable Annotation Processing API
Attach API
Java Compiler API
这些技术都是围绕这JVM和ByteCode相关领域的,我将这些技术的核心内容定位于Java的高阶技术,主要的原因是因为我觉得这些技术非常的Hack,表现在几个方面:
- 这些技术在Java技术栈的偏底,一般Java程序员了解得不是很深入;
- 这些技术在实际运用非常的广泛,现在所有主流的框架中的核心技术都有用到;
- 这些技术可以想象的空间非常大,能够解决很多我们用常规方法解决不了的问题。
因为牵涉到的技术非常的多,我这里不会对每项技术深入太多,主要是让大家有一个体系化的认识,本文后面会附上我整理和筛选的相关资料,已被大家进一步学习和查阅。
JVM和ByteCode概述
JVM执行的不是Java,而是ByteCode
我先介绍下JVM的体系结构,JVM规范定义了一系列子系统以及它们的外部行为。JVM主要由以下子系统:
- 类加载器(Class Loader),用于读入.class字节码文件并将类加载到数据区。
- 执行引擎(Execution Engine),用于执行数据区的指令,操作系统提供真实内存给JVM,用于JVM数据区存取数据。
如果要深入学习JVM的原理,查看本文末尾的参考文档,本文不对JVM内部做很多的描述。从这个图里面,我们可以看出,JVM并不是执行的Java文件,而是class文件,也就是用于描述ByteCode的文件,这个点给我们提供了很多想象空间以,给我们留下了很多可以Hack的可能。我们可以修改ByteCode的值来插入一些代码,来做一些我们之前做不到的事情;也可以通过分析ByteCode来做一些分析和统计的工作,比如分析锁使用风险、分析代码逻辑结构等。
class文件结构简介
class文件有非常严谨的结构,Oracle公布的JVM标准规范中有详细描述
Code_attribute {
u2 attribute_name_index; //常量池中的uft8类型的索引,值固定为”Code“
u4 attribute_length; //属性值长度,为整个属性表长度-6
u2 max_stack; //操作数栈的最大深度值,jvm运行时根据该值佩服栈帧
u2 max_locals; //局部变量表最大存储空间,单位是slot
u4 code_length; // 字节码指令的个数
u1 code[code_length]; // 具体的字节码指令
u2 exception_table_length; //异常的个数
{ u2 start_pc;
u2 end_pc;
u2 handler_pc; //当字节码在[start_pc, end_pc)区间出现catch_type或子类,则转到handler_pc行继续处理。
u2 catch_type; //当catch_type=0,则任意异常都需转到handler_pc处理
} exception_table[exception_table_length]; //具体的异常内容
u2 attributes_count; //属性的个数
attribute_info attributes[attributes_count]; //具体的属性内容
}
修改 class文件的工具
要在实现一些Hack的能力,我们首先第一步要解决的是解决class文件的增、改、读的问题。我们有很多现成字节码工具可以用,这里介绍字节码工具中使用最广泛的一个,无处不在的ASM(也许你从没有注意到过它神一样的存在),ASM关注的是使用和性能的简单性,使它的设计和实现都尽可能的小和快,这使得它在动态系统中非常有吸引力。
AMS提供了两套API修改class文件,一套基于事件模型、一套基于树的数据结构模型,这两种API可以简单的理解为类似于的XML API( SAX )和XML文档对象模型(DOM)API文档:基于事件的API类似于SAX,基于对象的API是类似于DOM。基于对象的API建立在基于事件的基础之上,比如DOM可以在SAX上提供。
基于事件的API定义了一组可能的事件和他们必须发生的顺序,并提供一个类解析器生成一个事件解析每个元素,从这些事件序列生成编译类。
基于树的数据结构的API是面向对象的一种设计,类被表示为一个对象,原理是将class文件的结构隐射成了一套标准的树形结构,方便对calss文件进行编辑操作。
在ASM基础之上,进一步衍生出了一些工具,比如 AspectJ、CGLib 这两个鼎鼎大名的 AOP 工具。
除了ASM还有很多其他的字节码工具,这里例举几个比较常用的字节码工具,我这里整理一下前三甲字节码工具,供大家参考和扩展学习。
- OW2独立开源组织提供的ASM
- Jboss- Javassist 维护的 Javassist
- Apache 维护的BCEL
ByteCode技术原理和运用
calss文件的涉及到了3个阶段 编译 — 加载 –运行,为了体系化的介绍,接下来我从这三个阶段进行串联概述其原理和运用场景。
在编译的字节码技术
我们可以在非运行时,当然是可以对class文件进行修改或是分析操作的,所以原理上不需要过多介绍,虽说是在编译阶段对静态字节码的处理,但是其应用场景也非常多。
1、代码分析
利用字节码静态分析,对代码计算度量、查找bug和检查编码约定
- SemmleCode SemmleCode工程分析平台帮助快速识别和应对关键漏洞,开发安全、高质量的软件;
- Sonargraph Sonargraph 是一个强大的静态代码分析器,它允许您监视软件系统的技术质量,并在开发过程的所有阶段中执行有关软件体系结构、度量和其他方面的规则;
- TamiFlex 是一个工具套件,用于帮助对使用反射和自定义类装入器的Java程序进行静态分析。
- JCarder 是在并发多线程Java程序中寻找潜在死锁的开源工具。它通过动态地检测Java字节码来实现这一点(即:它不是用于静态代码分析的工具,而是在获得的锁的图中寻找循环。)
2、代码生成
通过字节码修改,结合Pluggable Annotation Processing API生成代码,减少代码量,比如ORM框架、EJB框架、IOC框架、JDO框架、UnitTest框架等,这里有很多的著名框架都有用到相关的技术,除此之外,还有一些辅助编程工具等等。
- Spring 核心技术中结合AspectJ实现静态代理的IOC框架;
- Hibernate 使用字节码技术自动生成DO类;
- AgitarOne 使用字节码技术自动生成测试类和测试代码;
- Lombok 通用的代码简化工具,结合标注和Pluggable Annotation Processing API,有效减少代码的编写,比如get、set、构造函数、hashCode、log等,都可以实现标签化;
- fun4j 它是一个将函数式编程的主要概念集成到Java平台的框架。它的核心是一个lambda-to-JVM字节码编译器。
3、语言扩展
JVM上运行的是class文件,所以说,我们可以将各种图灵完备的语言,编译成class文件,移植到JVM上面来。甚至自己在为了解决一些特定场景情况下,构造自己的领域语言(DSL),将其编译成class文件来运行。 下面给出了JVM上一些主流的其他语言。
Groovy、Scala、JRuby、 Kotlin 、Jython、NetRexx
类加载和运行时字节码技术介绍
本来想将启动时和运行时用到的字节码技术分开介绍,但是由于启动时和运行时这两块技术耦合得比较紧,而且很多运行时技术都是基于启动时技术发展起来的,所以合在了一起介绍。
Instrumentation
在Java SE5 中,提供了一种为JVM提供代理能力,如此一来我们可以支持JVM级别的 AOP 操作,就可以在类加载的时候,对class类文件进行替换和修改了。在 Java SE 5 及其后续版本当中,通过使用java.lang.instrument.Instrumentation这个接口,我们可以实现对JVM的拦截操作,可以在一个普通 Java 程序(带有 main 函数的 Java 类),通过 -javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。
Java SE 6中,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。另外,对native 的 Instrumentation也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。
在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 Attach APT 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。
下图介绍了javagent实现代理的方式以及内部的一些关键方法。
Attach API
javaagent可以在JVM启动后再加载,就是通过Attach API实现的。当然,Attach API可不仅仅是为了实现动态加载agent,Attach API其实是跨JVM进程通讯的工具,能够将某种指令从一个JVM进程发送给另一个JVM进程。加载javaagent只是Attach API发送的各种指令中的一种, 诸如jstack打印线程栈、jps列出Java进程、jmap做内存dump等功能,都属于Attach API可以发送的指令。
JVM Tool Interface(JVMTI)
JVM Tool Interface(JVMTI)是JVM提供的native编程接口,开发者可以通过JVMTI向JVM监控状态、执行指令,其目的是开放出一套JVM接口用于 profile、debug、监控、线程分析、代码覆盖分析等工具。
JVMTI和Instumentation API的作用很相似,都是一套JVM操作和监控的接口,且都需要通过agent来启动:
- Instumentation API需要打包成jar,并通过Java agent加载(-javaagent)
- JVMTI需要打包成动态链接库(随操作系统,如.dll/.so文件),并通过JVMTI agent加载(-agentlib/-agentpath)
既然都是agent,那么加载时机也同样有两种:启动时(Agent_OnLoad)和运行时Attach(Agent_OnAttach)。
JVMTI能做的事情包括:
- 获取所有线程、查看线程状态、线程调用栈、查看线程组、中断线程、查看线程持有和等待的锁、获取线程的CPU时间、甚至将一个运行中的方法强制返回值……
- 获取Class、Method、Field的各种信息,类的详细信息、方法体的字节码和行号、向Bootstrap/System Class Loader添加jar、修改System Property……
- 堆内存的遍历和对象获取、获取局部变量的值、监测成员变量的值……
- 各种事件的callback函数,事件包括:类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、gc开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出……
- 设置与取消断点、监听断点进入事件、单步执行事件……
前面说的Instumentation API也是基于JVMTI来实现的,具体以addTransformer来说,通过Instrumentation注册的ClassFileTransformer,实际上是注册了JVMTI针对类文件加载事件(ClassFileLoadHook)的callback函数。
类加载和运行期间字节码技术的运用场景
热部署领域
热部署一般有两种实现方式,一种是通过使用ClassLoader加载新类,但是因为JVM不允许相同的类多次加载,所以在加载之前,可能需要先卸载老的类,这样一来,在运行过程中的状态可能会丢失,也可能造成短时间不不可用,这就违背了热部署的一些初衷。另外一种是通过javaagent的方式修改内存中class的字节码,或是拦截默认加载器的行为这种方式使用场景很多。可以参考 《深入探索 Java 热部署》
- JRebel:目前最常用的热部署工具,是一款收费的商业软件,因此在稳定性和兼容性上做的都比较好。
- Spring-Loaded:Spring旗下的子项目,也是一款开源的热部署工具。
- Hotcode2:阿里内部开发和使用的热部署工具,功能和上面基本一样,同时针对各种框架做了很多适配。
- IDE提供的HotSwap
- 使用eclipse或IntelliJ IDEA通过debug模式启动时,默认会开启一项HotSwap功能。用户可以在IDE里修改代码时,直接替换到目标程序的类里。不过这个功能只允许修改方法体,而不允许对方法进行增删改。 该功能的实现与debug有关。
- debug其实也是通过JVMTI agent来实现的,JVITI agent会在debug连接时加载到debugee的JVM中。debuger(IDE)通过JDI(Java debug interface)与debugee(目标Java程序)通过进程通讯来设置断点、获取调试信息。除了这些debug的功能之外,JDI还有一项redefineClass的方法,可以直接修改一个类的字节码。没错,它其实就是暴露了JVMTI的bytecode instrument功能,而IDE作为debugger,也顺带实现了这种HotSwap功能。
线上诊断领域
Btrace 这是一个线上诊断神器,基本原理是Java Agent+ASM+Java instrument+ Java Complier Api来实现了运行是代码功能的动态调整注入。
Greys 实现原理类似,可以参考阿里云的文章 《Greys Java在线问题诊断工具》
Byteman Byteman的原理是在运行时修改应用程序类的字节码
代码覆盖率
JaCoCo JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。
应用性能监控(APM)工具
现在市场上大部分的监控产品都是基于代理启动instrumentation实现的,原理上和JaCoco类似为你准备了前5个可用的工具: APM工具集合
- Stagemonitor
- Pinpoint
- MoSKito
- Glowroot
- Kamon
动态代理技术实现 AOP
- CGLIB 在Spring AOP中,通常会用CGLIB来生成AopProxy对象。在Hibernate中PO(Persistant Object 持久化对象)字节码的生成工作也要靠它来完成;
- DynamicAspects 通过instrumentation和javaanent实现的动态代理
参考资料
JVM介绍
JVM内幕
JAVA语言和JVM规范
JVM指令集 #jvms-6.5.invokeinterface
ASM User
ASM4手册
CGLib User
开源字节码工具
BCEL官方文档
javaagent
JVMTI
JVMTM Tool Interface
JVM源码分析之javaagent原理完全解读
JSR 199 Java Compiler API
Annotation processor API
IBM Java SE 6 新特性 编译器API
JSR 269: Pluggable Annotation Processing API
Btrace
BTrace 简要介绍
IBM Java SE 6 新特性 Instrumentation 新功能
I nstrument package info
JCarder
AspectJ
The AspectJTM Programming Guide
Java Performance Monitoring: 5 Open Source Tools You Should Know
JVM Attach机制实现
Attach API
Attach API 标准
关于作者
头条号: Java深度思考 的作者,现就职于支付宝金融核心技术部,任高级技术专家,花名丛英,技术爱好广泛喜欢,热爱Java技术,也爱研究现在流行的区块链和机器学习相关的内容;对于三方支付业务、金融技术架构、技术管理方面有比较丰富的经验。