Java字节码加强探秘

手机软件开发 2024-10-2 08:16:29 38 0 来自 中国
本文转载自 美团技能团队:Java字节码加强探秘
一、字节码

1.1 什么是字节码

Java之以是可以“一次编译,到处运行”,一是因为JVM针对各种操纵体系、平台都举行了定制,二是因为无论在什么平台,都可以编译天生固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的告急性。之以是被称之为字节码,是因为字节码文件由十六进制值构成,而JVM以两个十六进制值为一组,即以字节为单位举行读取。在Java中一样寻常是用javac下令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示。
对于开辟职员,相识字节码可以更精确、直观地明白Java语言中更深层次的东西,比如通过字节码,可以很直观地看到Volatile关键字如安在字节码上见效。别的,字节码加强技能在Spring AOP、各种ORM框架、热摆设中的应用家常便饭,深入明白其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要终极可以天生符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性大概实现各种语法糖。明白字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的操持思绪,学习起来也“安若泰山”。
本文重点着眼于字节码加强技能,从字节码开始逐层向上,由JVM字节码操纵聚集到Java中操纵字节码的框架,再到我们熟悉的各类框架原理及应用,也都会逐一举行先容。
1.2 字节码布局

.java文件通过javac编译后将得到一个.class文件,比如编写一个简朴的ByteCodeDemo类,如下图2的左侧部分:
编译后天生ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位举行分割后展示如图2右侧部分所示。上文提及过,JVM对于字节码是有规范要求的,那么看似紊乱的十六进制符合什么布局呢?JVM规范要求每一个字节码文件都要由十部分按照固定的序次构成,团体布局如图3所示。接下来我们将逐一先容这十个部分:
3.png (1) 魔数(Magic Number)
全部的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否大概是一个.class文件,如果是,才会继续举行之后的操纵。
风趣的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。
(2) 版本号
版本号为魔数之后的4个字节,前两个字节表现次版本号(Minor Version),后两个字节表现主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,以是编译该文件的Java版本号为1.8.0。
(3) 常量池(Constant Pool)
紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全范围定名、字段的名称和形貌符、方法的名称和形貌符。常量池团体上分为两部分:常量池计数器以及常量池数据区,如下图4所示。


  • 常量池计数器(constant_pool_count):由于常量的数目不固定,以是须要先放置两个字节来表现常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排撤消下标“0”,也就是说,这个类文件中共有35个常量。


  • 常量池数据区:数据区是由(constant_pool_count-1)个cp_info布局构成,一个cp_info布局对应一个常量。在字节码中共有14种范例的cp_info(如下图6所示),每种范例的布局都是固定的。
详细以CONSTANT_utf8_info为例,它的布局如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的范例是utf8_info,以是值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串详细的值。从图2中的字节码摘取一个cp_info布局,如下图7右侧所示。将它翻译过来后,其寄义为:该常量范例为utf8字符串,长度为一字节,数据为“a”。
7.png 其他范例的cp_info布局在本文不再赘述,团体布局大同小异,都是先通过Tag来标识范例,然后后续n个字节来形貌长度和(或)数据。先知其以是然,以后可以通过javap -verbose ByteCodeDemo下令,查察JVM反编译后的完备常量池,如下图8所示。可以看到反编译效果将每一个cp_info布局的范例和值都很明白地出现了出来。
8.png (4) 访问标记
常量池竣事之后的两个字节,形貌该Class是类照旧接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标记(Access_Flag)。须要留意的是,JVM并没有穷举全部的访问标记,而是使用按位或操纵来举行形貌的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。
(5) 当前类名
访问标记后的两个字节,形貌的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
(6) 父类名称
当前类名后的两个字节,形貌父类的全限定名,同上,保存的也是常量池中的索引值。
(7) 接口信息
父类名称后为两字节的接口计数器,形貌了该类或父类实现的接口数目。紧接着的n个字节是全部接口名称的字符串常量的索引值。
(8) 字段表
字段表用于形貌类和接口中声明的变量,包罗类级别的变量以及实例变量,但是不包罗方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,形貌字段个数;第二部分是每个字段的详细信息fields_info。字段表布局如下图所示:
以图2中字节码的字段表为例,如下图11所示。此中字段的访问标记查图9,0002对应为Private。通过索引下标在图8中常量池分别得到字段名为“a”,形貌符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。
11.png (9)方法表
字段表竣过后为方法表,方法表也是由两部分构成,第一部分为两个字节形貌方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包罗方法的访问标记、方法名、方法的形貌符以及方法的属性,如下图所示:
方法的权限修饰符依然可以通过图9的值查询得到,方法名和方法的形貌符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息举行解读,如图13所示。可以看到属性中包罗以下三个部分:

  • “Code区”:源代码对应的JVM指令操纵码,在举行字节码加强时重点操纵的就是“Code区”这一部分。
  • “LineNumberTable”:行号表,将Code区的操纵码和源代码中的行号对应,Debug时会起到作用(源代码走一行,须要走多少个JVM指令操纵码)。
  • “LocalVariableTable”:当地变量表,包罗This和局部变量,之以是可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式举行传入。当然,这是针对非Static方法而言。
13.png (10)附加属性表
字节码的最后一部分,该项存放了在该文件中类或接口所界说属性的根本信息。
1.3 字节码操纵聚集

在上图13中,Code区的赤色编号0~17,就是.java中的方法源代码编译后让JVM真正实行的操纵码。为了资助人们明白,反编译后看到的是十六进制操纵码所对应的助记符,十六进制值操纵码与助记符的对应关系,以及每一个操纵码的用处可以查察Oracle官方文档举行相识,在须要用到时举行查阅即可。比如上图中第一个助记符为iconst_2,对应到图2中的字节码为0x05,用处是将int值2压入操纵数栈中。以此类推,对0~17的助记符明白后,就是完备的add()方法的实现。
1.4 操纵数栈和字节码

JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集每每和硬件挂钩),但缺点在于,要完成同样的操纵,基于栈的实现须要更多指令才华完成(因为栈只是一个FILO布局,须要频仍压栈出栈)。别的,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速率要慢许多,这也是为了跨平台性而做出的捐躯。
我们在上文所说的操纵码大概操纵聚集,其实控制的就是这个JVM的操纵数栈。为了更直观地感受操纵码是怎样控制操纵数栈的,以及明白常量池、变量表的作用,将add()方法的对操纵数栈的操纵制作为GIF,如下图14所示,图中仅截取了常量池中被引用的部分,以指令iconst_2开始到ireturn竣事,与图13中Code区0~17的指令逐一对应:
1.5 查察字节码工具

如果每次查察反编译后的字节码都使用javap下令的话,好非常繁琐。这里保举一个Idea插件:jclasslib。使用效果如图15所示,代码编译后在菜单栏"View"中选择"Show Bytecode With jclasslib",可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
15.png 二、字节码加强

在上文中,偏重先容了字节码的布局,这为我们相识字节码加强技能的实现打下了底子。字节码加强技能就是一类对现有字节码举行修改大概动态生玉成新字节码文件的技能。接下来,我们将从最直接利用字节码的实现方式开始深入举行分析。
16.png 2.1 ASM

对于须要手动利用字节码的需求,可以使用ASM,它可以直接天生.class字节码文件,也可以在类被加载入JVM之前动态修改类活动(如下图17所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热摆设、修改其他jar包中的类等。当然,涉及到云云底层的步调,实现起来也比力贫困。接下来,本文将先容ASM的两种API,并用ASM来实现一个比力粗糙的AOP。但在此之前,为了让各人更快地明白ASM的处理流程,猛烈发起读者先对访问者模式举行相识。简朴来说,访问者模式告急用于修改或操纵一些数据布局比力稳固的数据,而通过第一章,我们知道字节码文件的布局是由JVM固定的,以是很恰当利用访问者模式对字节码文件举行修改。
17.png 2.1.1 ASM API

2.1.1.1 核心API

ASM Core API可以类比剖析XML文件中的SAX方式,不须要把这个类的整个布局读取进来,就可以用流式的方法来处理字节码文件。利益黑白常节约内存,但是编程难度较大。然而出于性能考虑,一样寻常情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以天生新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中差异的地区有差异的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
2.1.1.2 树形API

ASM Tree API可以类比剖析XML文件中的DOM方式,把整个类的布局读取到内存中,缺点是斲丧内存多,但是编程比力简朴。TreeApi差异于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个地区,类比DOM节点,就可以很好地明白这种编程方式。
2.1.2 直接利用ASM实现AOP

利用ASM的CoreAPI来加强类。这里不纠结于AOP的专业名词如切片、关照,只实如今方法调用前、后增长逻辑,普通易懂且方便明白。首先界说须要被加强的Base类:此中只包罗一个process()方法,方法内输出一行“process”。加强后,我们盼望的是,方法实行前输出“start”,之后输出"end"。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 16:45, Processed in 0.186920 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表