本文已收录GitHub,更有互联网大厂面试真题,面试攻略,高效学习资料等前端
Java 程序员几乎都了解 Spring。它的 IoC(依赖反转)和 AOP(面向切面编程)功能很是强大、易用。而它背后的字节码生成技术(在运行时,根据须要修改和生成 Java 字节码的技术)就是就是一项重要的支撑技术。java
Java 字节码可以在 JVM(Java 虚拟机)上解释执行,或即时编译执行。其实,除了Java,JVM 上的 Groovy、Kotlin、Closure、Scala 等不少语言,也都须要生成字节码。另外,playscript 也能够生成字节码,从而在 JVM 上高效地运行!git
并且,字节码生成技术颇有用。你能够用它将高级语言编译成字节码,还能够向原来的代码中注入新代码,来实现对性能的监测等功能。程序员
目前,我就有一个实际项目的需求。咱们的一个产品,须要一个规则引擎,解析自定义的DSL,进行规则的计算。这个规则引擎处理的数据量比较大,因此它的性能越高越好。所以,若是把 DSL 编译成字节码就最理想了。github
既然字节码生成技术有很强的实用价值,那么本文就带你掌握它。面试
我会先带你了解 Java 的虚拟机和字节码的指令,而后借助 asm 这个工具,生成字节码,最后,再实现从 AST 编译成字节码。经过这样一个过程,你会加深对 Java 虚拟机的了解,掌握字节码生成技术,从而更加了解 Spring 的运行机制,甚至有能力编写这样的工具!算法
字节码是一种二进制格式的中间代码,它不是物理机器的目标代码,而是运行在 Java 虚拟机上,能够被解释执行和即时编译执行。编程
在讲后端技术时,我强调的都是,如何生成直接在计算机上运行的二进制代码,这比较符合C、C++、Go 等静态编译型语言。但若是想要解释执行,除了直接解释执行 AST 之外,我没有讲其余解释执行技术。后端
而目前更常见的解释执行的语言,是采用虚拟机,其中最典型的就是 JVM,它可以解释执行 Java 字节码。数组
而虚拟机的设计又有两种技术:一是基于栈的虚拟机;二是基于寄存器的虚拟机。
标准的 JVM 是基于栈的虚拟机(后面简称“栈机”)。
每个线程都有一个 JVM 栈,每次调用一个方法都会生成一个栈桢,来支持这个方法的运行。栈桢里面又包含了本地变量数组(包括方法的参数和本地变量)、操做数栈和这个方法所用到的常数。
栈机是基于操做数栈作计算的。以“2+3”的计算为例,只要把它转化成逆波兰表达式,“2 3 +”,而后按照顺序执行就能够了。也就是:先把 2 入栈,再把 3 入栈,再执行加法指令,这时,要从栈里弹出 2 个操做数作加法计算,再把结果压入栈。
你能够看出,栈机的加法指令,是不须要带操做数的,就是简单的“iadd”就行,这跟你以前学过的 IR 都不同。为何呢?由于操做数都在栈里,加法操做须要 2 个操做数,从栈里弹出 2 个元素就好了。
也就是说,指令的操做数是由栈肯定的,咱们不须要为每一个操做数显式地指定存储位置,因此指令能够比较短,这是栈机的一个优势。
接下来,咱们聊聊字节码的特色。
字节码是什么样子的呢?我编写了一个简单的类,其中的 foo() 方法实现了一个简单的加法计算,你能够看看它对应的字节码是怎样的:
publicclassMyClass{ publicintfoo(inta){ returna+3; } }
在命令行终端敲入下面两行命令,生成文本格式的字节码文件:
javacMyClass.java javap-vMyClass>MyClass.bc
打开 MyClass.bc 文件,你会看到下面的内容片断:
publicintfoo(int); Code: 0:iload_1 //把下标为1的本地变量入栈 1:iconst_3 //把常数3入栈 2:iadd //执行加法操做 3:ireturn //返回
其中,foo() 方法一共有四条指令,前三条指令是计算一个加法表达式 a+3。这彻底是按照逆波兰表达式的顺序来执行的:先把一个本地变量入栈,再把常数 3 入栈,再执行加法运算。
若是你细心的话,应该会发现:把参数 a 入栈的第一条指令,用的下标是 1,而不是 0。这是由于,每一个方法的第一个参数(下标为 0)是当前对象实例的引用(this)。
我提供了字节码中,一些经常使用的指令,增长你对字节码特色的直观认识,完整的指令集能够参见JVM 的规格书:
其中,每一个指令都是 8 位的,占一个字节,并且 iload_0,iconst_0 这种指令,甚至把操做数(变量的下标、常数的值)压缩进了操做码里,能够看出,字节码的设计很注重节省空间。
根据这些指令所对应的操做码的数值,MyClass.bc 文件中,你所看到的那四行代码,变成二进制格式,就是下面的样子:
你能够用"hexdump MyClass.class"显示字节码文件的内容,从中能够发现这个片断(就是橙色框里的内容):
如今,你已经初步了解了基于栈的虚拟机,与此对应的是基于寄存器的虚拟机。这类虚拟机的运行机制跟机器码的运行机制是差很少的,它的指令要显式地指出操做数的位置(寄存器或内存地址)。它的优点是:能够更充分地利用寄存器来保存中间值,从而能够进行更多的优化。
例如,当存在公共子表达式时,这个表达式的计算结果能够保存在某个寄存器中,另外一个用到该公共子表达式的指令,就能够直接访问这个寄存器,不用再计算了。在栈机里是作不到这样的优化的,因此基于寄存器的虚拟机,性能能够更高。而它的典型表明,是 Google公司为 Android 开发的 Dalvik 虚拟机和 Lua 语言的虚拟机。
这里你须要注意,栈机并非不用寄存器,实际上,操做数栈是能够基于寄存器实现的,寄存器放不下的再溢出到内存里。只不过栈机的每条指令,只能操做栈顶部的几个操做数,因此也就没有办法访问其它寄存器,实现更多的优化。
如今,你应该对虚拟机以及字节码有了必定的了解了。那么,如何借助工具生成字节码呢?你可能会问了:为何不纯手工生成字节码呢?固然能够,只不过借助工具会更快一些。
就像你生成 LLVM 的 IR 时,也曾得到了 LLVM 的 API 的帮助。因此,接下来我会带你认识 asm 这个工具,并借助它为咱们生成字节码。
其实,有不少工具会帮咱们生成字节码,好比 Apache BCEL、Javassist 等,选择 asm 是由于它的性能比较高,而且它还被 Spring 等著名软件所采用。
asm是一个开源的字节码生成工具。Grovvy 语言就是用它来生成字节码的,它还能解析Java 编译后生成的字节码,从而进行修改。
asm 解析字节码的过程,有点像 xml 的解析器解析 xml 的过程:先解析类,再解析类的成员,好比类的成员变量(Field)、类的方法(Mothod)。在方法里,又能够解析出一行行的指令。
你须要掌握两个核心的类的用法:
这两个类若是配合起来用,就能够一边读入,作必定修改后再写出,从而实现对原来代码的修改。
咱们先试验一下,用 ClassWriter 生成字节码,看看能不能生成一个跟前面示例代码中的MyClass 同样的类(咱们能够称呼这个类为 MyClass2),里面也有一个如出一辙的 foo函数。相关代码参考genMyClass2()方法,这里只拿出其中一段看一下:
//////建立foo方法 MethodVisitormv=cw.visitMethod(Opcodes.ACC_PUBLIC,"foo", "(I)I",//括号中的是参数类型,括号后面的是返回值类型 null,null); //添加参数a mv.visitParameter("a",Opcodes.ACC_PUBLIC); mv.visitVarInsn(Opcodes.ILOAD,1); //iload_1 mv.visitInsn(Opcodes.ICONST_3); //iconst_3 mv.visitInsn(Opcodes.IADD); //iadd mv.visitInsn(Opcodes.IRETURN); //ireturn //设置操做数栈最大的帧数,以及最大的本地变量数 mv.visitMaxs(2,2); //结束方法 mv.visitEnd(); //////建立foo方法 MethodVisitormv=cw.visitMethod(Opcodes.ACC_PUBLIC,"foo", "(I)I",//括号中的是参数类型,括号后面的是返回值类型 null,null); //添加参数a mv.visitParameter("a",Opcodes.ACC_PUBLIC); mv.visitVarInsn(Opcodes.ILOAD,1); //iload_1 mv.visitInsn(Opcodes.ICONST_3); //iconst_3 mv.visitInsn(Opcodes.IADD); //iadd mv.visitInsn(Opcodes.IRETURN); //ireturn //设置操做数栈最大的帧数,以及最大的本地变量数 mv.visitMaxs(2,2); //结束方法 mv.visitEnd();
从这个示例代码中,你会看到两个特色:
执行这个程序,就会生成 MyClass2.class 文件。
把 MyClass2.class 变成可读的文本格式以后,你能够看到它跟 MyClass 的字节码内容几乎是同样的,只有类名称不一样。固然了,你还能够写一个程序调用 MyClass2,验证一下它是否可以正常工做。
发现了吗?只要熟悉 Java 的字节码指令,在 asm 的帮助下,你能够很方便地生成字节码!
既然你已经能生成字节码了,那么不如趁热打铁,把编译器前端生成的 AST 编译成字节码,在 JVM 上运行?由于这样,你就能从前端到后端,完整地实现一门基于 JVM 的语言了!
基于 AST 生成 JVM 的字节码的逻辑仍是比较简单的,比生成针对物理机器的目标代码要简单得多,为何这么说呢?主要有如下几个缘由:
按照这个思路,你能够在 playscript-java 中增长一个ByteCodeGen的类,针对少许的语言特性作一下字节码的生成。最后,咱们再增长一点代码,可以加载并执行所生成的字节码。运行下面的命令,能够把bytecode.play示例代码编译并运行。
javaplay.PlayScript-bcbytecode.play
固然了,咱们只实现了 playscript 的少许特性,不过,若是在这个基础上继续完善,你就能够逐步实现一门完整的,基于 JVM 的语言了。
我在开篇提到,Java 程序员大部分都会使用 Spring。Spring 的 IoC(依赖反转)和AOP(面向切面编程)特性几乎是 Java 程序员在面试时必被问到的问题,了解 Spring 和字节码生成技术的关系,能让你在面试时更轻松。
Spring 的 AOP 是基于代理(proxy)的机制实现的。在调用某个对象的方法以前,要先通过代理,在代理这儿,能够进行安全检查、记日志、支持事务等额外的功能。
Spring 采用的代理技术有两个:一个是 Java 的动态代理(dynamic proxy)技术;一个是采用 cglib 自动生成代理,cglib 采用了 asm 来生成字节码。
Java 的动态代理技术,只支持某个类所实现的接口中的方法。若是一个类不是某个接口的实现,那么 Spring 就必须用到 cglib,从而用到字节码生成技术来生成代理对象的字节码。
本文主要带你了解了字节码生成技术。字节码生成技术是 Java 程序员很是熟悉的Spring 框架背后所依赖的核心技术之一。若是想要掌握这个技术,你须要对 Java 虚拟机的运行原理、字节码的格式,以及常见指令有所了解。我想强调的重点以下:
运行程序的虚拟机有两种设计:一个是基于栈的;一个是基于寄存器的。
基于栈的虚拟机不用显式地管理操做数的地址,所以指令会比较短,指令生成也比较容易。而基于寄存器的虚拟机,则能更好地利用寄存器资源,也能对代码进行更多的优化。
你要可以在大脑中图形化地想象出栈机运行的过程,从而对它的原理理解得更清晰。
asm 是一个字节码操纵框架,它能帮你修改和生成字节码,若是你有这方面的需求,能够采用这样的工具。
在这里,我也建议 Java 程序员,多多了解 JVM 的运行机制,和 Java 字节码,这样会更好地把握 Java 语言的底层机制,从而更利于本身职业生涯的发展。