likes
comments
collection
share

深度解析字节码文件

作者站长头像
站长
· 阅读数 13

字节码/class文件

什么是字节码?JVM可以理解的代码(即扩展名为 .class 的文件),源代码通过编译器编译为字节码,再通过类加载子系统加载到JVM中运行。比如 Kotlin也是基于JVM的编程语言。class文件就是字节码文件

深度解析字节码文件

为什么要学习字节码文件

学习字节码文件可以帮助你更好地理解类加载的整个过程,区分符号引用与直接引用,弄明白方法调用的过程,多态的实现原理。字节码文件也与注解,异常的实现原理息息相关。字节码文件是基础的基础。

class文件特点

class文件/字节码文件本质上是一个以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中,中间没有添加任何分隔符。jvm根据其特定的规则解析该二进制数据,从而得到相关信息。

RednaxelaFX:

用字节码来解释性能问题很抱歉也是比较不靠谱的。

字节码用于解释 “语义问题” 很靠谱,但看字节码是看不出性能问题的——超过它的抽象层次了

class文件的两种数据类型

只有两种数据类型:“无符号数”和“表”

  • 无符号数:以u1/u2/u4/u8代表1/2/4/8个字节数的无符号数,可以用来描述数字、索引引用、数量值、字符串值
  • 表:由无符号数和其它表构成,命名以“_info”结尾。

整个class文件也是一张表。

class文件结构

深度解析字节码文件

0、魔数:CAFEBABE

文件开头的4个字节("cafe babe")称之为 魔数,唯有以"cafe babe"开头的class文件方可被虚拟机所接受,这4个字节就是字节码文件的身份识别

1、两个版本号

minor:次要版本;major:主要版本

它们各占两个字节

2、常量池(重要)

常量池占一个Class文件的很大一部分空间

常量池的入口有一个u2,代表个数(constant_pool_count)

Class文件结构中只有常量池的容量计数是从1开始

常量池用于存储字面量和符号引用,字面量包括字符串"abd",和用final修饰的字段。

符号引用包括:

  • 类和接口的全限定名
  • 字段的简单名称和描述符
  • 方法的简单名称和描述符

比如:一个方法在方法表,存储的是索引,指向常量池内的方法名称/描述符

以及可能不太眼熟的:

  • 被模块导出或者开放的包
  • 动态调用点和动态常量
  • 方法句柄和方法类型

截至JDK13,常量表中分别有17种不同类型的常量。

类的符号引用:CONSTANT_Class_info

CONSTANT_Class_info还是一个索引,它的结构是:

  • 一个tag,u1,标识当前常量类型为CONSTANT_Class_info
  • 一个name_index,u2,指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定名

笼统的符号引用是:本身可以是任何形式的字面量,它的实现取决于JVM,只要能定位到目标即可。

所以,类的符号引用在HotSpot的实现就是全类名

Utf8字符串:CONSTANT_Utf8_info

这就是存储字符串字面值的。它的结构是:

  • 一个tag,u1,所有常量池的类型都有一个tag用于标识自己的类型,后续不再赘述
  • 一个length,u2,代表当前字符串的长度
  • bytes,u1的集合,个数取决于length

3、类访问标记

包括访问修饰符,是否接口,是否抽象,是否枚举

深度解析字节码文件

4、当前类,父类

 u2             this_class;  // 当前类
 u2             super_class; // 父类

它们都指向一个类型为CONSTANT_Class_info的类描述符常量,从而找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

可以理解为:this_class -> CONSTANT_Class_info -> CONSTANT_Utf8_info -> 全类名

有这个链条关系,就可以知道当前类和父类了,「5、接口索引集合」同理

如果没看懂为什么就再去看一遍「2、常量池」吧

5、接口索引集合

u2的集合         interfaces;  // 接口索引集合 

接口索引集合存放着当前类实现的所有接口,如果当前是接口,则是所有继承的接口

它的查找过程和类/父类一样。

6、字段表集合

字段包括了类变量(static修饰)和普通变量(非static修饰),类变量由access_flags的ACC_STATIC标识。

字段表结构:

1、access_flags:类似「3、类访问标记」,以及一些字段独有的比如:volatile,transient

2、name_index和descriptor_index,是对常量池项的引用,分别代表着字段的简单名称以及字段的描述符

再强调一下,这两个是索引,真正的符号引用/字面量在常量池,因此是u2是定长的

深度解析字节码文件

字段的简单名称:纯的字段名。int m字段的简单名称就是“m”

字段描述符:描述字段的数据类型

如一个定义为“java.lang.String”类型的二维数组将被记录成“[[Ljava/lang/String;” 一个整型数组“int[]”将被记录成“[I”。

3、attribute_info:额外信息,详见『8、属性表集合』

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就是合法的

7、方法表集合

method_info和field_info几乎完全一致

仅仅是access_flag略有不同。比如:

  • 方法独有:是否synchronized,native;
  • 而字段独有:volatile,transient

name_index和descriptor_index的引用也分别代表了方法的简单名称以及方法的描述符

方法的简单名称:纯的方法名。inc()方法就是“inc”

方法描述符:包括 方法的参数列表(包括数量、类型以及顺序)和返回值

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

实际的方法代码存储在哪里?

方法原本也是一段java代码,因此会被编译成字节码指令,存储在: 方法属性表集合中一个名为“Code”的属性里面

详见『8、属性表集合』

8、属性表集合🚩

属性表集合用于描述某些场景专有的信息

像前文提到的字段,方法都依赖属性表集合

  • 方法表依赖Code 存储实际方法的字节码指令
  • 字段表依赖ConstantValue 存储final定义的常量值

声明final static int m=123;那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。

属性表还包括:

  • Exceptions:方法可能抛出的所有异常
  • Inner classes:内部类

还有非常多,一共接近30个,详见《深入理解Java虚拟机 第三版》P318,这里暂时只看两个:

ConstantValue:常量值索引

static字段专属。

static字段的初始化有两种方法:

  • 类构造器< clinit >()方法中
  • 使用ConstantValue属性

目前:被static final修饰,且为基本数据类型或者java.lang.String的,用ConstantValue;其余用< clinit >()

ConstantValue是常量池索引,常量池只有基本数据类型和String,因此这看似是一种ConstantValue对User的束缚,实际ConstantValue自己是被常量池所束缚

Code:方法体

Code用于描述一个方法。

// Java源程序编译后生成的字节码指令
u4 code length  // 字节码指令的长度
u1 code         // 字节码指令 有code length 个 code
    // 一个u1 8位,可以表达0~255 共256条指令,目前Java已经定义了两百多条指令。
    // 可见 《深入理解Java虚拟机 第三版》附录C“虚拟机字节码指令表
元数据
// 一个Code还包括很多其他元数据 包括栈最大深度 异常表等信息

虽然code length是u4,但《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令。

Code内部的异常表

异常表是Code的一部分。异常表不是必须的。是实现try catch finally代码块的核心。

在Code中,异常表紧跟code:

    u2                  exception_table_length // 一个u2 异常表长度
    exception_info      exception_table        // 异常表信息

对于一个exception_info是这样的:

u2 start_pc     // 左闭右开的 start_pc ~ end_pc 
u2 end_pc        
u2 handler_pc   // 跳转行
u2 catch_type   // 出现了类型为catch_type或者其子类的异常

代表在一个「左闭右开」的 start_pc ~ end_pc 行之间,一旦出现了类型为catch_type或者其子类的异常,就转而到第handler_pc行继续处理。

对于finally来说 catch_type是any

javap反编译出来的异常表结果:

Exception table:
from        to      target      type
0           5       10          Class java/lang/Exception
0           5       21           any
10          16      21           any
Exceptions:方法抛出的异常

更确切地说:Exceptions是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。

Exceptions和异常表是不同的

  • Exceptions用于描述方法的throws关键字后面列举的异常
  • 异常表是try...代码块

因此,Exceptions与Code属性是平级的。

结构和其他的差不多,就不再列举了

InnerClasses:内部类集合

如果一个类有内部类,就会有这个属性。

用若干个inner_classes_info表示。

Signature:标签

任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(ParameterizedType),则Signature属性会为它记录泛型签名信息。

Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性

Annotations:运行时注解相关

RuntimeVisibleAnnotations:运行时可见注解

使用反射API获取注解时,就来自属性表的注解属性

一个注解对应一个annotation属性

一个annotation属性包含注解的全类名,以及它的所有属性,以键值对的形式存储,可以通过反射获取

字节码指令集

Java虚拟机的指令由一个字节长度的Opcode操作码,加若干Operand操作数,Java虚拟机采用面向操作数栈,因此大多数指令都不包含操作数。

基本工作模型:

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

具体的字节码指令就不一一列举了,网上很方便能够查到

javap:查看方法的字节码指令

JDK内置了很多有用的小工具,javap是其中一个。javap可以帮助我们看到字节码指令

字节码指令可以帮助我们更好地理解“语义”。在此简单介绍javap的使用:

javap基本使用

首先在「idea整合javap」,然后只需要右键类,External Tools,javap即可。

也可以在控制台直接拼下面的命令

javap参数
  • javap -l :会输出行号和本地变量表信息;
  • javap -c :会对当前class字节码进行反编译生成汇编代码;
  • javap -v: class字节码文件中除了包-c参数包含的内容外,还会输出行号、局部变量表信息、常量池等信息;

最全的:javap -verbose xxx.class

参考文献

周志明 《深入理解Java虚拟机 第三版》

转载自:https://juejin.cn/post/7275943600780853289
评论
请登录