性能微优化-字节码
第5章的性能优化策略适合各种开发语言,应用这些策略有较大的系统性能提升。有些场景下,核心系统或者工具类性能有极致的要求,这时候可以应用Java 自身的性能最佳实践,这些实践需要以Java编译和运行作为基础知识,掌握字节码和JIT等知识。
学完本章,将掌握如下知识
- Java编译,把Java源码编译成字节码,并在编译过程中做的性能优化。
- 字节码的基本知识,包括如何查看字节码,以及字节码的常用指令。本章的部分性能优化策略需要结合字节码解释。同时Java系統很多高级功能均是通过动态字节码生成实现
- JIT基本知识,包括了解JIT的运行过程,以及观察JIT。通过对核心系统或者工具的运行的JIT日志进行分析,确保实现预期的Java优化。
- Java语言中的一些高性能实践,可以应用核心系统或者工具库提高性能,比如String的最佳使用办法,位运算等
字节码
读懂字节码有助于了解JVM执行代码过程,找到可能得性能优化点,或者使用字节码实现性能优化。阅读完本节,你将掌握如下知识
- JVM规范中核心概念栈帧以及变量表,操作数栈。
- 如何查看字节码,有助于我们在后续章节掌握Java的性能微优化知识
- 如何直接编写字节码,有助于完成或者掌握Java性能优化中的反射,这是一个很多Java工具需要使用的性能优化点。同时直接在运行中生成字节码能完成Java中许多高级功能,如AOP,RPC等。
栈帧
Java方法是在线程中执行的,而在每一个Java线程被创建的同时有一个Java虚拟机栈的结构被创建。虚拟机栈存储着Frame(虚拟机栈帧)。虽然说虚拟机栈中存储着多个Frame但同一时间只会有一个Frame是激活状态(current frame),此时线程的栈也是与此Frame相关联的,当进行新的方法调用(current method)时就会创建新的Frame,并将其压入Java虚拟机栈中,方法返回后(正常或异常返回),该Frame从虚拟机栈中被移除。如下是线程1的虚拟机栈

关于Frame的销毁,有两种途径
- 正常返回 返回合适的值给调用者,并压入调用者的操作数栈。
- 异常 抛出异常,如果当前方法不能处理该异常,然后舍弃当前的栈与本地变量,销毁Frame,没有返回值,并在调用者中重新抛出异常
每个Frame主要包含本地变量表与操作数栈,使用方式说明如下
本地变量表: 类似一个通过序号访问的数组
本地变量表可以存储boolean, byte, char, short, int,float, reference, 或 returnAddress类型的数据,占用1个slot(槽位),而long、double则需要两个本地变量slot表示.比如,对于如下方法
public int add(int x,int y){
return x+y;
}局部变量数组包含3个元素,索引0是this的引用,索引1第二个是x,索引2是y值,当调用方法add(5,7)的时候,局部变量数组如下
| 索引 | 内容 | 说明 |
|---|---|---|
| 0 | this | 实例方法的第一个元素总是this,即指向实例本身 |
| 1 | 5 | x=5 |
| 2 | 7 | y=7 |
操作数栈 :字节码操作的数据,是一个LIFO Stack,根据不同的操作指令,对操作数栈有不同的操作。比如iadd 指令,表示对俩个int相加,需要操作数栈上有俩个int值,iadd从操作数栈中取出这俩个值,计算结果然后在push到操作数栈。对于如上add方法,在执行前的操作数栈是
| 索引 | 内容 | 说明 |
|---|---|---|
| 0 | 5 | x=5 |
| 1 | 7 | y=7 |
当执行iadd指令后,操作数栈变为
| 索引 | 内容 | 说明 |
|---|---|---|
| 0 | 12 | x+y的结果 |
当继续执行返回的时候,使用的指令是IRETURN,此指令表示返回一个int类型,且其值在操作数栈顶位置。刚才iadd指令执行完毕,栈顶是需要返回的值。
在了解了本地变量表和操作数栈后,add方法字节码如下
public add(II)I
ILOAD 1
ILOAD 2
IADD
IRETURN
MAXSTACK = 2
MAXLOCALS = 3
}解释如下:
| 字节码 | 说明 |
|---|---|
| add(II)I | 字节码描述了add方法签名,其中(II)表示入参为俩个int类型,括号后的I表示返回的类型也为int |
| ILOAD 1 | 从本地变量表加载索引1的 int 值到操作数栈,即参数x的值,其他类似指令还有aload(加载一个对象引用,后续会有此对象方法调用)。本地变量表在方法调用前,已经初始化为(this,5,7) |
| ILOAD 2 | 从本地变量表加载索引2的 int 值到操作数栈,即参数y的值 |
| IADD | 执行整数相加指令,IADD表示整数相加。其他相加指令还有dadd,fadd等指令,分别表示double和float相加。执行结果将压入操作数栈顶 |
| IRETURN | 返回一个int类型,此数据来源于操作数栈顶。 执行完毕,此栈帧将被销毁 |
| MAXSTACK | MAXSTACK = 2,通过前面分析,操作数栈只需2个slot即可,这是编译时期计算结果 |
| MAXLOCALS | MAXLOCALS = 3,本地标量表只需要3个slot即可,这是编译时期计算的结果 |
由于JVM指令非常多,有限于篇幅,不会介绍所有的字节码和详细运行过程。本书会在随后章节的性能优化中,会详细描述遇到的字节码。
字节码和汇编语言的执行机制有一些类似,汇编语言中,通过指令操作内存地址和寄存器,字节码操作本地变量表和操作数栈。
访问官网了了解Class文件结构,以及关于JVM的字节码: https://docs.oracle.com/en/java/javase/26/docs/specs/jvms/index.html
查看字节码
查看类的字节码最快的方式是使用IDE的插件,比如idea的插件市场中的ASM Bytecode Viewer。它能显示源码对应的字节码,也能显示如何用ASM工具手工编写字节码 。对于前一节的add方法,用插件显示效果如下

默认显示效果与上图有所不同,这是因为默认会显示字节码的Debug信息,可以通过右上角的Settings 取消Debug显示
此图还显示了默认构造函数,这个构造函数是在编译Java代码时候生成(参考6.1节),这个构造函数的字节码解释如下
| 字节码 | 描述 |
|---|---|
| ALOAD | ALOAD 0 ,加载本地变量表第一个元素到操作数栈,本地变标量第一个元素总是this对象 |
| INVOKESPECIAL | 调用方法java.lang.Object的<init>v 这里init指的构造函数,v指的是void返回。INVOKESPECIAL 要求操作数栈上有一个对象引用,即刚才的this |
| RETURN | 返回,不同于IRETURN 需要操作数栈顶存放一个int类型,RETURN不需要返回任何值 |
| MAXLOCALS | MAXLOCALS = 1,本地变量表容量为1个slot。即存放的this |
| MAXSTACK | MAXSTACK =1,操作数栈容量为1个slot。即ALOAD 0 (即this对象)操作的结果存放到操作数栈。 |
编写字节码
有少部分场景会要求在系统运行时动态生成字节码并被加载后调用,这些场景包括:
- AOP场景中,生成目标了类的动代理类,比如Spring框架中,对于一个申明@Service的类,可能会生成一个动态代理类,此动态代理类除了具备调用原有的方法外,结合@Transactional完成事务管理相关操作。
- 虚拟机加载类的时候,可以修改方法的实现,比如在方法调用处增加监控日志代码,全链路跟踪调用等
- RPC远程调用框架,通过接口动态的生成实现类,以实现远程调用,路由,流量控制等功能
- JVM支持对class的方法重定义实现,通过调用
java.lang.instrument.Instrumentation.redefineClasses重新定义方法实现,比如Arthas工具调用此动态为执行的方法埋点,或者动态修改执行方法 - 本章5.6中的反射优化中提到的生成JavaBean的属性访问类,避免反射调用开销,这也是许多高性能框架必须实现的一个特性。比如Fastjson2,作者的Beetl,以及Apache Fory等工具都使用此提高性能
编写字节码可以通过ow2.asm工具来实现,在6.2.2章节中,ASM插件除了显示Java源代码对应的字节码外,可能用此工具直接编写此源码的字节,点击右上的ASMified,完整列出了一个用ASM直接生成Test类的的代码。

代码包含了使用ASM 的API 生成字节码,代码第50行返回了生成的byte[] 字节码,与Test.class一致。主要通过调用methodVisitor的各个字节码生成API实现,内容与在上一节列出的add方法的字节码很类似,这里就不在详细描述,可以通过ow2.asm官网文档了解其API。
ow2.asm是一个较为底层的工具,需要对JVM字节码有完全的了解。有一些更容易入门的工具包括CGLib,ByteBuddy,或者类似Javac的工具Janino等等,他们都能运行时刻动态生成字节码。
