字节码

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

Class 文件

批注 2019-12-20 144534

构成

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_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_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
AcC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
AcC_ABSTRACT Ox0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假
AcC_SYNTHETIC Ox1000 标识这个类并非由用户代码产生的
Acc_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
AcC_MODULE 0x8000 标识这是一个模块

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

字段表集合

字段表结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

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

标志名称 标志值 含义
ACC_PUBLIC Ox0001 字段是否public
ACC_PRIVATE Ox0002 字段是否 private
ACC_PROTECTED ox0004 字段是否 protected
ACC_STATIC Ox0008 字段是否static
ACC FINAL 0x0010 字段是否final
ACC_VOLATILE Ox0040 字段是否 volatile
ACC_TRANSIENT ox0080 字段是否transient
ACC_SYNTHETIC ox1000 字段是否由编译器自动产生
ACC_ENUM Ox4000 字段是否 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_PUBLIC Ox0001 方法是否为public
ACC_PRIVATE Ox0002 方法是否为private
ACC_PROTECTED Ox0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
AcC_SYNCHRONIZED Ox0020 方法是否为synchronized
ACC_BRIDGE Ox0040 方法是不是由编译器产生的桥接方法
AcC_VARARGS Ox0080 方法是否接受不定参数
ACC_NATIVE Ox0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
AcC_STRICT Ox0800 方法是否为strictfp
ACC_SYNTHETIC Ox1000 方法是否由编译器自动产生

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

属性表集合

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 由final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated 的方法和字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法类文件
InncrClasses Code属性 内部类列表
LineNumberTable Codc属性 Java 源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类、方法表、字段表 JDK 5中新增的属性,用于支持范型情况下的方法签名。在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的范型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录范型中的相关信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK 5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号JSR 45提案为这些非Java类文件语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加人的调试信息
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的
LocalVariablcTypeTable JDK 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中得到
Module JDK 9中新增的属性,用于记录一个Module的名称以及相关信息(requires.exports.opens, uses .provides)
ModulePackages JDK9中新增的属性,用于记录一个模块中所有被exports或者opens 的包
ModuleMainClass JDK9中新增的属性,用于指定一个模块的主类
NestHost JDK 11中新增的属性,用于支持嵌套类(Java中类的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类
NestMembers JDK 11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自已己有哪些内部类

属性表结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

1.Code属性

类型 名称 数量 说明
u2 attribute_name_index 1 attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为"Code",它代表了该属性的属性名称
u4 attribute_length l 属性值的长度
u2 max_stack l 代表了操作数栈(Operand Stack)深度的最大值
u2 max_locals 1 代表了局部变量表所需的存储空间(32位以下(包含)的变量占用一个槽)
u4 code_length 1 执行的字节码长度(《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令)
u1 code code_length 存放执行的字节码
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
u2 attributes_count 1
attribute_info attributes attributes_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

异常表:

类型 名称 数量 说明
u2 start_pc 1 异常捕获起始行
u2 end_pc 1 异常捕获结束行(不包含本行)
u2 handler_pc 1 发生异常后跳转的位置
u2 catch_type 1 异常的类型

2.Exceptions属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

3.LineNumberTable属性

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

4.LocalVariableTable及LocalVariableTypeTable属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

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

类型 名称 数量 说明
u2 start_pc 1 生命周期开始的字节码偏移量
u2 length 1 作用范围覆盖的长度
u2 name_index 1 局部变量的名称
u2 descriptor_index 1 局部变量的描述符
u2 index 1 局部变量在栈帧的局部变量表中变量槽的位置

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

5.SourceFile及SourceDebugExtension属性

类型 名称 数量 说明
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1 指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名

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

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

6.ConstantValue属性

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

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

类型 名称 数量 说明
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1 所以这里的常量最多只能为64bit

7.InnerClasses属性

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

类型 名称 数量 说明
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1 表需要记录多少个内部类信息
inner_class_info inner_classes number_of_classes 记录的内部类信息
类型 名称 数量 说明
u2 inner_class_info_index 1 内部类的符号引用
u2 outer_class_info_index 1 宿主类的符号引用
u2 inner_name_index 1 代表这个内部类的名称,如果是匿名内部类,这项值为0
u2 inner_class_access flags 1

8.Deprecated及Synthetic属性

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

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

属性结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

9.StackMapTable属性

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

类型 名称 数量 说明
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

10.Signature属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

11.BootstrapMethods属性

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

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_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));

字节码的执行

  1. 解释执行
  2. 编译执行
  3. JIT编译与解释混合执行

截图录屏_选择区域_20200918145300

这样就造成机器在热机所承载的负载可能会比冷机的高

热点代码检测

还有一种新的编译方式,即所谓的 AOT(Ahead-of-Time Compilation),直接将字节码编译成机器代码,这是 graalvm 所做的

代码优化

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

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. 在前端编译器层面做的改进。这种改进被称作语法糖

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

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