Java高阶技术(JVM&ByteCode)及其运用

Java
335
0
0
2023-12-20
标签   JVM

Java高阶技术(JVM&ByteCode)及其运用

我很喜欢祖师爷的这句话,我觉得只有了解了最核心的技术,才算得上是精通了这门技术。当精通某项技术之后,在这个体系下的一切,学习、运用、创造才能做到游刃有余、手到擒来;才能更好的发挥个人和想象和创造力,做出更有价值的事情。

当我们从一个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,表现在几个方面:

  1. 这些技术在Java技术栈的偏底,一般Java程序员了解得不是很深入;
  2. 这些技术在实际运用非常的广泛,现在所有主流的框架中的核心技术都有用到;
  3. 这些技术可以想象的空间非常大,能够解决很多我们用常规方法解决不了的问题。

因为牵涉到的技术非常的多,我这里不会对每项技术深入太多,主要是让大家有一个体系化的认识,本文后面会附上我整理和筛选的相关资料,已被大家进一步学习和查阅。

JVM和ByteCode概述

JVM执行的不是Java,而是ByteCode

我先介绍下JVM的体系结构,JVM规范定义了一系列子系统以及它们的外部行为。JVM主要由以下子系统:

  1. 类加载器(Class Loader),用于读入.class字节码文件并将类加载到数据区。
  2. 执行引擎(Execution Engine),用于执行数据区的指令,操作系统提供真实内存给JVM,用于JVM数据区存取数据。

Java高阶技术(JVM&ByteCode)及其运用

如果要深入学习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文件进行编辑操作。

Java高阶技术(JVM&ByteCode)及其运用

在ASM基础之上,进一步衍生出了一些工具,比如 AspectJ、CGLib 这两个鼎鼎大名的 AOP 工具。

除了ASM还有很多其他的字节码工具,这里例举几个比较常用的字节码工具,我这里整理一下前三甲字节码工具,供大家参考和扩展学习。

  1. OW2独立开源组织提供的ASM
  2. Jboss- Javassist 维护的 Javassist
  3. 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框架等,这里有很多的著名框架都有用到相关的技术,除此之外,还有一些辅助编程工具等等。

Java高阶技术(JVM&ByteCode)及其运用

  • 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

Java高阶技术(JVM&ByteCode)及其运用

类加载和运行时字节码技术介绍

本来想将启动时和运行时用到的字节码技术分开介绍,但是由于启动时和运行时这两块技术耦合得比较紧,而且很多运行时技术都是基于启动时技术发展起来的,所以合在了一起介绍。

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实现代理的方式以及内部的一些关键方法。

Java高阶技术(JVM&ByteCode)及其运用

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 热部署》

Java高阶技术(JVM&ByteCode)及其运用

  • 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功能。

线上诊断领域

Java高阶技术(JVM&ByteCode)及其运用

Btrace 这是一个线上诊断神器,基本原理是Java Agent+ASM+Java instrument+ Java Complier Api来实现了运行是代码功能的动态调整注入。

Greys 实现原理类似,可以参考阿里云的文章 《Greys Java在线问题诊断工具》

Byteman Byteman的原理是在运行时修改应用程序类的字节码

代码覆盖率

Java高阶技术(JVM&ByteCode)及其运用

JaCoCo JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。

Java高阶技术(JVM&ByteCode)及其运用

应用性能监控(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技术,也爱研究现在流行的区块链和机器学习相关的内容;对于三方支付业务、金融技术架构、技术管理方面有比较丰富的经验。