字节码

虚拟机实现的方式主要有以下两种:

Class 文件

mindmap  Class文件    魔数        Class文件的特征    小版本号    大版本号    常量池        各种常量        整数        字符串        Class    访问标记        static        public    当前类    类的属性    类的方法        访问标记        名称        描述符        子主题            code属性                行号属性                局部变量表                栈映射帧    类的字段        访问标记        名称        描述符        属性    实现的接口    父类

构成

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

Class 文件中的所有字节存储都是使用大端序

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:

反编译:

javap -v classname

魔数与 Class 文件版本

前4个字节为魔数,十六进制表示为0xCAFEBABE,标识该文件为class文件

第5、6字节表示次版本号(小更新) 第7和第8个字节是主版本号(从45开始,一个大版本加1)

常量池

常量池的入口放置了一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个数是从1开始

访问标志

常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被public、abstract、final等修饰符修饰

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
AcC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
AcC_ABSTRACTOx0400是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假
AcC_SYNTHETICOx1000标识这个类并非由用户代码产生的
Acc_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
AcC_MODULE0x8000标识这是一个模块

类索引、父类索引与接口索引集合

字段表集合

字段表结构:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

字段访问标志,存放在access_flags里面:

标志名称标志值含义
ACC_PUBLICOx0001字段是否public
ACC_PRIVATEOx0002字段是否 private
ACC_PROTECTEDox0004字段是否 protected
ACC_STATICOx0008字段是否static
ACC FINAL0x0010字段是否final
ACC_VOLATILEOx0040字段是否 volatile
ACC_TRANSIENTox0080字段是否transient
ACC_SYNTHETICox1000字段是否由编译器自动产生
ACC_ENUMOx4000字段是否 enum

name_index和descriptor_index分别代表着字段的简单名称以及字段和方法的描述符

描述符:

标志字符含义
B基本类型byte
C基本类型char
D基本类型double
F基本类型float
I基本类型int
J基本类型1ong
S基本类型short
Z基本类型boolean
V特殊类型void
L对象类型,如Ljava/lang/Object;

对于数组类型,每一维度将使用一个前置的[字符来描述,,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”

用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内

方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”

方法表集合

方法表的结构同字段表

方法访问标志:

标志名称标志值含义
ACC_PUBLICOx0001方法是否为public
ACC_PRIVATEOx0002方法是否为private
ACC_PROTECTEDOx0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
AcC_SYNCHRONIZEDOx0020方法是否为synchronized
ACC_BRIDGEOx0040方法是不是由编译器产生的桥接方法
AcC_VARARGSOx0080方法是否接受不定参数
ACC_NATIVEOx0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
AcC_STRICTOx0800方法是否为strictfp
ACC_SYNTHETICOx1000方法是否由编译器自动产生

方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面

属性表集合

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表由final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated 的方法和字段
Exceptions方法表方法抛出的异常列表
EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法类文件
InncrClassesCode属性内部类列表
LineNumberTableCodc属性Java 源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表JDK 5中新增的属性,用于支持范型情况下的方法签名。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的范型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录范型中的相关信息
SourceFile类文件记录源文件名称
SourceDebugExtension类文件JDK 5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号JSR 45提案为这些非Java类文件语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加人的调试信息
Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
LocalVariablcTypeTableJDK 5中新增的属性,它使用特征签名代替描述符,是为了引人泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段表JDK 5中新增的属性,为动态注解提供支持。该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimcInvisiblcAnnotations类、方法表、字段表JDK 5中新增的属性,与RuntimeVisibleAnnota-tions属性作用刚好相应,用于指明哪些注解是运行时不可见的
RuntimeVisibleParamcterAnnotations方法表JDK5中新增的属性,作用与RuntimeVisible-Annotations属性类似,只不过作用对象为方法参数
RuntimelnvisibleParameterAnnotations方法表JDK 5中新增的属性,作用与 RuntimelnvisiblcAnnotations属性类似,只不过作用对象为方法参数
AnnotationDefault方法表JDK 5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods类文件JDK 7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations类、方法表、字段表,Code属性JDK 8中新增的属性,为实现JSR 308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimelnvisibleTypeAnnotations类、方法表、字段表,Code属性JDK 8中新增的属性,为实现JSR 308中新增的类型注解提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的
MethodParameters方法表JDK 8中新增的属性,用于支持(编译时加上-parameters参数)将方法名称编译进 Class文件中,并可运行时获取。此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到
ModuleJDK 9中新增的属性,用于记录一个Module的名称以及相关信息(requires.exports.opens, uses .provides)
ModulePackagesJDK9中新增的属性,用于记录一个模块中所有被exports或者opens 的包
ModuleMainClassJDK9中新增的属性,用于指定一个模块的主类
NestHostJDK 11中新增的属性,用于支持嵌套类(Java中类的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类
NestMembersJDK 11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自已己有哪些内部类

属性表结构:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

1.Code属性

类型名称数量说明
u2attribute_name_index1attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为"Code",它代表了该属性的属性名称
u4attribute_lengthl属性值的长度
u2max_stackl代表了操作数栈(Operand Stack)深度的最大值
u2max_locals1代表了局部变量表所需的存储空间(32位以下(包含)的变量占用一个槽)
u4code_length1执行的字节码长度(《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令)
u1codecode_length存放执行的字节码
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
u2attributes_count1
attribute_infoattributesattributes_count
public int inc() {return m + 1;}

一个方法编译后:

public int inc();    descriptor: ()I    flags: (0x0001) ACC_PUBLIC    Code:      stack=2, locals=1, args_size=1 //这里的方法虽然没有参数,但是参数数量为1,1就是this         0: aload_0         1: getfield      #2                  // Field m:I         4: iconst_1         5: iadd         6: ireturn      LineNumberTable:        line 4: 0

异常表:

类型名称数量说明
u2start_pc1异常捕获起始行
u2end_pc1异常捕获结束行(不包含本行)
u2handler_pc1发生异常后跳转的位置
u2catch_type1异常的类型

2.Exceptions属性

列举出方法中可能抛出的受查异常(Checked Excepitons)

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

3.LineNumberTable属性

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

4.LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length

local_variable_info项目代表了一个栈帧与源码中的局部变量的关联:

类型名称数量说明
u2start_pc1生命周期开始的字节码偏移量
u2length1作用范围覆盖的长度
u2name_index1局部变量的名称
u2descriptor_index1局部变量的描述符
u2index1局部变量在栈帧的局部变量表中变量槽的位置

LocalVariableTypeTable。这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)

5.SourceFile及SourceDebugExtension属性

类型名称数量说明
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名

SourceDebugExtension属性用于存储额外的代码调试信息:

类型 | 名称 | 数量|说明-- | --------------------------------- | --u2 | attribute_name_index | 1u4 | attribute_length | 1u1 | debug_extension[attribute_length] | 1|额外的debug信息

6.ConstantValue属性

通知虚拟机自动为静态变量赋值。

目前Oracle公司实现的Javac编译器的选择是,如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化

类型名称数量说明
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1所以这里的常量最多只能为64bit

7.InnerClasses属性

用于记录内部类与宿主类之间的关联

类型名称数量说明
u2attribute_name_index1
u4attribute_length1
u2number_of_classes1表需要记录多少个内部类信息
inner_class_infoinner_classesnumber_of_classes记录的内部类信息
类型名称数量说明
u2inner_class_info_index1内部类的符号引用
u2outer_class_info_index1宿主类的符号引用
u2inner_name_index1代表这个内部类的名称,如果是匿名内部类,这项值为0
u2inner_class_accessflags1

8.Deprecated及Synthetic属性

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用“@deprecated”注解进行设置

Synthetic属性代表此字段或者方法并不是由Java源码直接产生的

属性结构:

类型名称数量
u2attribute_name_index1
u4attribute_length1

9.StackMapTable属性

在编译阶段将一系列的验证类型(Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能

类型名称数量说明
u2attribute_name_index1
u4attribute_length1
u2number_of_entries1
stack_map_framestack_map_frame_entriesnumber_of_entries

10.Signature属性

一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中,用来记录泛型信息

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2signature_index1

11.BootstrapMethods属性

位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_bootstrap_methods1
bootstrap_methodbootstrap_methodsnum_bootstrap_methods

12.MethodParameters属性

一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息

13.模块化相关属性

14.运行时注解相关属性

字节码指令

不考虑异常处理的字节码执行:

do {自动计算PC寄存器的值加1;根据PC寄存器指示的位置,从字节码流中取出操作码;if (字节码存在操作数) 从字节码流中取出操作数;执行操作码所定义的操作;} while (字节码流长度 > 0);

大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference

oracle 官方 pdf

  1. 基于寄存器的指令集
  2. 基于栈的指令集Hotspot中的Local Variable Table = JVM中的寄存器

加载和存储指令

用于将数据在栈帧中的局部变量表和操作数栈之间来回传输

iload_<n>代表了iload_0、iload_1、iload_2和iload_3这几条指令 这些指令都是iload的特殊形式,这些特殊的指令省略掉了操作数,但是语义同iload一样

运算指令

用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶

类型转换指令

Java虚拟机直接支持(即转换时无须显式的转换指令)的宽化类型转换

窄化转换:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f

对象/数组创建与访问指令

操作数栈管理指令

直接操作操作数栈的指令

控制转移指令

有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值

方法调用和返回指令

方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用

异常处理指令

同步控制指令

  1. ACC_SYNCHRONIZED 标志同步方法
  2. MONITORENTER MONITOREXIT 标记临界区

ASM

ASM是一个通用的Java字节码操作和分析框架

字节码增强

字节码混淆

javassist

ClassPool pool = ClassPool.getDefault();// 创建类CtClass userClass = pool.makeClass("wang.ismy.test.User");// 添加属性userClass.addField(CtField.make("private String name;",userClass));userClass.addField(CtField.make("private Integer age;",userClass));// 添加方法userClass.addMethod(CtMethod.make("public String getName(){return name;}",userClass));userClass.addConstructor(new CtConstructor(new CtClass[]{pool.get("java.lang.String"),pool.get("java.lang.Integer")},userClass));userClass.writeFile("./User.class");
ClassPool pool = ClassPool.getDefault();pool.appendClassPath(new ClassClassPath(Main.class));CtClass userClass = pool.get("wang.ismy.assist.User");userClass.getDeclaredMethod("getName").setBody("{return name + \"123\";}");Class<?> aClass = userClass.toClass();Object obj = aClass.newInstance();System.out.println(aClass.getMethod("getName").invoke(obj));

实例-动态代理字节码生成

public class DynamicProxyTest {    public static void main(String[] args) {        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");        Run origin = new Run();        Runnable hello = (Runnable)Proxy.newProxyInstance(Run.class.getClassLoader(), new Class[]{Runnable.class}, new InvocationHandler() {            @Override            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {                System.out.println("hello");                return method.invoke(origin);            }        });        hello.run();    }}class Run implements Runnable{    @Override    public void run() {        System.out.println("world");    }}

这里newProxyInstance会生成这么样的一个代理类:

public final class $Proxy0 extends Proxy implements Runnable {    ...    public final void run() throws  {        try {            super.h.invoke(this, m3, (Object[])null);        } catch (RuntimeException | Error var2) {            throw var2;        } catch (Throwable var3) {            throw new UndeclaredThrowableException(var3);        }    }    ...}

生成的代理类run方法会调用我们写的InvocationHandler 我们的InvocationHandler又会执行一些附带逻辑并最后执行真实对象的方法

实例-Backport:回到未来

把高版本JDK中编写的代码放到低版本JDK环境中去部署使用。为了解决这个问题,一种名为“Java逆向移植”的工具(Java Backporting Tools)应运而生,Retrotranslator和Retrolambda是这类工具中的杰出代表

这些工具可以很好地移植下面两种情况:

  1. 对Java类库API的代码增强
  2. 在前端编译器层面做的改进。这种改进被称作语法糖

对于第一种情况 一些诸如只有高版本才有的类库移植工具可以很方便移植

但对于第二种情况,移植工具就需要通过修改字节码的方式来实现