专栏首页程序猿讲故事了解一下Java字节码

了解一下Java字节码

【 说明:本文严重参考了两本图书《深入理解Java虚拟机》和《实战Java虚拟机:Java故障诊断与性能优化》。在相关内容的基础上做了调整,并添加了自己的理解。】

一 Class文件格式

1. 一次编写、到处运行

Sun推出Java语言时,一句展示其跨平台特性的口号:WORA (Write Once Run Anywhere)。

刚推出时,主要是指编写一次Java文件,就可以在各个环境中运行。

随着时间的流逝,越来越多的语言被改编或设计运行在JVM上。除了java语言,比较知名的JVM上的编程语言还有:

  • Groovy
  • Scala
  • Kotlin
  • Clojure

2. class 文件格式

class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。

2.1 基本数据类型

class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。可以把u1, u2, u3, u4看做class文件数据项的“类型” 。对于字符串,则使用u1数组进行表示。

数据类型

含义

u1

无符号单字节整数

u2

无符号2字节整数

u4

无符号4字节整数

u8

无符号8字节整数

u1数组

字符串

2.2 class文件内容

Class文件的结构严格按照该结构体的定义:

  1. 文件以一个4字节的Magic(被称为魔数)开头,紧跟着大、小版本号。
  2. 在版本号之后是常量池,常量池的个数为constant_pool_count,常量池中的表项有constant_pool_count1项。
  3. 常量池之后是类的访问修饰符、代表自身类的引用、父类引用及接口数量和实现的接口引用。
  4. 在接口之后,有字段的数量和字段描述、方法数量及方法的描述。
  5. 存放类文件的属性信息。

类型

数量

名称

中文含义

u4

1

magic

魔数

u2

1

minor_version

小版本号

u2

1

major_version

大版本号

u2

1

constant_pool_count

常量数

cp_info

constant_pool_count - 1

constant_pool

常量池

u2

1

access_flags

访问标记

u2

1

this_class

当前类

u2

1

super_class

父类

u2

1

interfaces_count

实现的接口数

u2

interfaces_count

interfaces

接口列表

u2

1

fields_count

字段个数

field_info

fields_count

fields

字段列表

u2

1

methods_count

方法个数

method_info

methods_count

methods

方法列表

u2

1

attribute_count

属性个数

attribute_info

attributes_count

attributes

属性列表

2.3 Class文件的标志-魔数

魔数(MagicNumber)作为Class文件的标志,用来告诉Java虚拟机,这是一个Class文件。魔数是一个4字节的无符号整数,它固定为0xCAFEBABE。谐音 “Cafe Baby”。

我们先创建一个空的Java类

package bytecode;
/**
 * 这是一个空的Java类
 *
 * @author liaojunyong
 */
public class EmptyClass {
}

看看生成的 class 文件的内容

2.4 Class文件的版本号

在魔数后面,紧跟着Class的小版本号和大版本号。这表示当前Class文件是由哪个版本的编译器编译产生的。首先出现的是小版本号,是一个2字节的无符号整数,在此之后为大版本号,也用2字节表示。

Class文件的版本号和Java编译器的对应关系:

大版本(十进制)

大版本(HEX)

小版本

java版本

45

2D

3

1.1

46

2E

0

1.2

47

2F

0

1.3

48

30

0

1.4

49

31

0

1.5(5)

50

32

0

1.6(6)

51

33

0

1.7(7)

52

34

0

1.8(8)

53

35

0

9

54

36

0

10

55

37

0

11

56

38

0

12

57

39

0

13

58

3A

0

14

回到刚才的class文件查看版本,可以看到版本号是 0000 0034,对应java版本是8。

版本兼容性

向下兼容,否则报错

如果低版本JVM运行高版本class文件,会抛出异常。比如,JVM7运行编译版本为8的class。

java.lang.UsupportedClassVersionError: Unsupported major.minor version 52.0

如何编译指定版本的class文件

以Maven为例

查看maven命令使用的JDK版本
% mvn -version

Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-27T23:06:16+08:00)
Maven home: /usr/local/Cellar/maven/3.6.2/libexec
Java version: 13, vendor: Oracle Corporation, runtime: /jdk-13.jdk/Contents/Home
Default locale: zh_CN_#Hans, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.1", arch: "x86_64", family: "mac"

如果电脑上装了多个jdk,有可能同java -version不一致。

pom.xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <source>13</source>
        <target>13</target>
        <encoding>UTF-8</encoding>
    </configuration>
</plugin>
重新编译并查看生成的class版本
% mvn clean compile

一个额外的话题:应该用什么版本编译?

2.5 存放所有常数-常量池

常量池是Class文件中内容最丰富的区域之一。随着Java虚拟机的不断发展,常量池的内容也日渐丰富。同时,常量池对于Class文件中的字段和方法解析也有至关重要的作用,可以说,常量池是整个Class文件的基石。在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

刚才的EmptyClass字节码,可以看到常量个数为 0010=16。

常量池表项的类型

常量池类型

TAG

常量池类型

TAG

CONSTANT_Class

7

CONSTANT_Fieldref

9

CONSTANT_Methodref

10

CONSTANT_InterfaceMethodref

11

CONSTANT_String

8

CONSTANT_Integer

3

CONSTANT_Float

4

CONSTANT_Long

5

CONSTANT_Double

6

CONSTANT_NameAndType

12

CONSTANT_Utf8

1

CONSTANT_MethodHandle

15

CONSTANT_MethodType

16

CONSTANT_InvokeDynamic

18

常量池底层的数据类型:CONSTANT_Utf8、CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double。

CONSTANT_Utf8 : UTF字符串
CONSTANT_Utf8_info {
  u1 tag;
  u2 length;
  u1 bytes[length];
}
CONSTANT_Integer : 整数(int,short,char,byte,boolean)
CONSTANT_Integer_info {
  u1 tag;
  u4 bytes;
}
CONSTANT_Float : 浮点数(float)
CONSTANT_Float_info {
  u1 tag;
  u4 bytes;
}
CONSTANT_Long : 长整数(long)
CONSTANT_Long_info {
  u1 tag;
  u4 high_bytes;
  u4 low_bytes;
}
CONSTANT_Double : 双精度浮点数(double)
CONSTANT_Double_info {
  u1 tag;
  u4 high_bytes;
  u4 low_bytes;
}
CONSTANT_String :字符串(String)
CONSTANT_String_info {
  u1 tag;
  u2 string_index;
}
CONSTANT_Class : 类

会引用其他 UTF8 常量

CONSTANT_Class_info {
  u1 tag;
  u2 name_index;
}
CONSTANT_NameAndType : 名称和类型
CONSTANT_NameAndType_info {
  u1 tag;
  u2 name_index;
  u2 decription_index;
}
CONSTANT_Methodref : 方法常量
CONSTANT_Methodref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}
CONSTANT_Fieldref : 字段常量
CONSTANT_Fieldref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref : 接口的方法
CONSTANT_Fieldref_info {
  u1 tag;
  u2 class_index;
  u2 name_and_type_index;
}
CONSTANT_MethodType : 函数句柄
CONSTANT_MethodType_info {
  u1 tag;
  u2 description_index;
}
CONSTANT_InvokeDynamic : 动态调用
CONSTANT_InvokeDynamic_info {
  u1 tag;
  u2 bootstrap_method_attr_index;  // 定位到一个引导方法
  u2 name_and_type_index;
}
CONSTANT_MethodHandle : 方法句柄

它可以用来表示方法、类的字段或者构造函数等。方法句柄指向一个方法、字段,和C语言中的函数指针或者C#中的委托有些类似。

CONSTANT_MethodHandle_info {
  u1 tag;
  u2 reference_kind;
  u2 reference_index;
}

EmptyClass的常量池解析

Index

HEX

常量类型

引用1

引用2

1

0A 0002 0003

Methodref

2

3

2

07 0004

Class

4

3

0C 0005 0006

NameAndType

5

6

4

01 0010 6A...74(16字符)

Utf8

java/lang/Object

5

01 0006 3C696E69743E

Utf8

<init>

6

01 0003 282956

Utf8

()V

7

07 0008

Class

8

8

01 0013 62...73(19字符)

Utf8

bytecode/EmptyClass

9

01 0004 436F6465

Utf8

Code

10

01 000F 4C...65(15字符)

Utf8

LineNumberTable

11

01 0012 4C...65(18字符)

Utf8

LocalVariableTable

12

01 0004 74686973

Utf8

this

13

01 0015 4C...3B(21字符)

Utf8

Lbytecode/EmptyClass;

14

01 000A 53...65(10字符)

Utf8

SourceFile

15

01 000F 45...61(15字符)

Utf8

EmptyClass.java

图示

明明是空类却有一个 init() 方法?

Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "()" 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 "()" 方法.

在类中添加一个无参数构造函数,再重新看看生成的常量池。

package bytecode;
/**
 * 这是一个空的Java类
 *
 * @author liaojunyong
 */
public class EmptyClass {
    /**
     * 缺省构造函数
     */
    public EmptyClass() {
        super();
    }
}

可以看到常量池的个数没有变化,仍然只有一个 Methodref,引用指向 <init>()。

写一个稍微复杂的Java类,看看常量池

这个类有属性、有函数

package bytecode;

/**
 * 这是一个简单的Java类
 *
 * @author liaojunyong
 */
public class PageClass {
    /** 定义常量 */
    private static final int size = 10;
    /** 当前第几页 */
    private int page;
    
    /** 构造函数 */
    public PageClass(int page) {
        this.page = page;
    }

    /** 计算偏移量 */
    public int calculateOffset() {
        return page * size;
    }

    /** 程序入口 */
    public static void main(String[] args) {
        PageClass page = new PageClass(3);
        System.out.println(page.calculateOffset());
    }
}

打开class看二进制内容,常量池中常量个数已经变成 002D(45)

整理后的常量池对象(白色背景是 EmptyClass 也有的常量)

2.6 Class的访问标记(Access Flag)

在常量池后,紧跟着访问标记。该标记使用2字节表示,用于表明该类的访问信息,如public、final、abstract等。从表可以看出,每一种类型的表示都是通过设置访问标记32位中的特定位来实现的。比如,若是publicfinal的类,则该标记为ACC_PUBLIC|ACC_FINAL。

标记名称

数值

描述

ACC_PUBLIC

0x0001

public 类

ACC_FINAL

0x0010

final 类

ACC_SUPER

0x0020

使用增强的方法调用父类(?)

ACC_INTERFACE

0x0200

是否为接口

ACC_ABSTRACT

0x0400

是否为抽象类

ACC_SYNTHETIC

0x1000

由编译器产生的类,没有源码

ACC_ANNOTATION

0x2000

是否为注解

ACC_ENUM

0x4000

是否为枚举

看看我们PageClass的访问标记,0021,public

2.7 当前类、父类和接口

在访问标记后,会指定该类的类别、父类类别及实现的接口,格式如下:

u2 : 当前类 (指向常量池中的Class)
u2 : 父类 (指向常量池中的Class)
u2 : 实现的接口数 (实现的接口个数,没有实现接口为0)
u2 : 接口数组 (指向常量池中的Class,必须是接口)

没有实现任何接口的PageClass相关数据。

changd

数据长度

HEX

对应常量值

当前类

u2

0008

bytecode/PageClass

父类

u2

0002

Java/lang/Object

接口个数

u2

0000

0

为了看看实现了接口后的数据,为PageClass增加两个接口 Serializable, Comparable<PageClass>,重新编译。代码

public class PageClass implements Serializable, Comparable<PageClass>{...}

字节码变化,原来的 0000 变成了 0002 0023 0025。(增加了接口,常量池也会发生变化)

page-class-class2

changd

数据长度

HEX

对应常量值

当前类

u2

0008

bytecode/PageClass

父类

u2

0002

Java/lang/Object

接口个数

u2

0002

2

接口数组

u2*2

0023 0025

java/io/Serializable java/lang/Comparable

2.8 属性表 - attribute_info

后续内容会包括各种属性表,先做一些介绍。

属性表(attribute_info),在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

统一定义

方法也可以附带若干个属性,用于描述一些额外信息,比如方法字节码等,attributes_count表示该方法中属性的数量,紧接着就是attributes_count个属性的描述。对于属性来说,它们的统一格式为:

attribute_info {
  u2 : attribute_name_index;  (当前attribute的名称)
  u4 : attribute_length;  (当前attribute的长度)
  u1 : info[attribute_length]; (当前attribute的值)
}

下面是一些常见属性。

属性名称

使用位置

含义

Code

方法表

Java代码编译成的字节码指令

ConstantValue

字段表

final关键字定义的常量值

Deprecated

类、方法表、字段表

被声明为deprecated的类、方法、字段

Exceptions

方法表

方法抛出的异常

EnclosingMethod

类文件

一个类为局部类或匿名类时才有这个属性,用于标示这个类所在的外围方法

InnerClasses

类文件

内部类列表

LineNumberTable

Code属性

Java源码的行号与字节码的对应关系

LocalVariableTable

Code属性

方法的局部变量描述

StackMapTable

Code属性

供类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配。

Signature

类、方法表、字段表

支持范型情况下的方法签名

SourceFile

类文件

源文件名称

SourceDebugExtension

类文件

用于存储额外的调试信息,比如使用JSP开发,可以用于记录JSP的行号

Synthetic

类、方法表、字段表

表示方法或字段为编译器自动生成的

LocalVariableTypeTable

使用特征签名代替描述符,用于描述范型参数化类型

RuntimeVisibleAnnotations

类、方法表、字段表

为动态注解提供支持,表示注解是运行时可见的

RuntimeInvisibleAnnotations

类、方法表、字段表

为动态注解提供支持,表示注解是运行时不可见的

RuntimeVisible ParameterAnnotations

方法表

类似RuntimeVisibleAnnotations,作用于方法

RuntimeInvisible ParameterAnnotations

方法表

类似RuntimeInvisibleAnnotations,作用于方法

AnnotationDefault

方法表

注解类元素的默认值

BootstrapMethods

类文件

保存invokedynamic指令引用的引导方法限定符

属性-Code

方法的主要内容存放在其属性中,其中最重要的一个属性就是Code,它存放着方法的字节码等信息,结构如下:

Code_attribute{
  u2 : attribute_name_index; (属性名,指向常量池的常量,总是 Code)
  u4 : attribute_length;  (Code属性的剩余长度,不包括前6个字节 u2+u4)
  u2 : max_stack;  (操作数栈的最大深度)
  u2 : max_locals;  (局部变量的最大个数)
  u4 : code_length;  (字节码长度)
  u1 : code[code_length];  (字节码内容)
  u2 : exception_table_length;  (异常表的个数)
  exception_info : exception_table[exception_table_length];  (异常表)
  u2 : attributes_count;
  attribute_info : attributes[attributes_count];
} 

exception_info{
  u2 : start_pc;  (字节码的开始偏移量)
  u2 : end_pc;  (字节码的结束偏移量)
  u2 : handler_pc;  (catch异常的处理代码偏移量)
  u2 : catch_type;  (需要catch的异常类型,指向常量池的索引)
}

属性-LineNumberTable

LineNumberTable用来记录字节码偏移量和行号的对应关系,在软件调试时,该属性有至关重要的作用,若没有它,调试器无法定位到对应的源码。LineNumberTable属性的结构如下:其中,attribute_name_index为指向常量池的索引,在LineNumberTable属性中,该值为“LineNumberTable”,attribute_length为4字节无符号整数,表示属性的长度(不含前6字节),line_number_table_length表明表项有多少条记录,line_number_table为表的实际内容,它包含line_number_table_length个<start_pc,line_number>元组,其中,start_pc为字节码偏移量,line_number为对应的行号。

数据结构定义

{
  u2 : attribute_name_index;
  u4 : attribute_length;
  u2 : line_number_table_length;
  line_number_info : line_number_tables[line_number_table_length];
}

line_number_info {
  u2 : start_pc;
  u2 : line_number;
}

属性-LocalVariableTable

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用g:none或g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

{
  u2 : attribute_name_index;
  u4 : attribute_length;
  u2 : local_variable_table_length;
  local_variable_info : local_variable_tables[local_variable_table_length];
}

local_variable_info{
  u2 : start_pc;  (变量声明周期的开始量)
  u2 : length;  (变量声明周期的结束量)
  u2 : name_index; (常量池引用)
  u2 : decriptor_index;  (描述引用)
  u2 : index;  (局部变量在栈帧局部变量表中Slot的位置,如果是64位变量long/double,占用两个Slot)
}

属性-SourceFile

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的g:none或g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

{
  u2 : attribute_name_index;
  u4 : attribute_length;
  u2 : sourcefile_index;  (常量池引用,文件名)
}

属性-ConstantValue

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。类似int x=123static intx=123这样的变量定义在Java程序中是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。

虽然有final关键字才更符合"ConstantValue"的语义,但虚拟机规范中并没有强制要求字段必须设置了ACC_FINAL标志,只要求了有ConstantValue属性的字段必须设置ACC_STATIC标志而已,对final关键字的要求是Javac编译器自己加入的限制。而对ConstantValue的属性值只能限于基本类型和String,不过笔者不认为这是什么限制,因为此属性的属性值只是一个常量池的索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算ConstantValue属性想支持别的类型也无能为力。

{
  u2 : attribute_name;
  u4 : attribute_length;
  u2 : constant_value_index;  (常量池引用)
}

2.9 Class文件的字段

在接口描述后,会有类的字段信息。由于一个类会有多个字段,所以需要首先指明字段的个数:

u2 : field_count
field_info : fields[field_count]

字段数量fields_count是一个2字节无符号整数,在PageClass生成的字节码中,这个值是0002,表示有两个字段。字段数量之后为字段的具体信息,每一个字段为一个field_info的结构,该结构如下:

field_info {
  u2 : access_flags;  (类似于class 的 访问标记)
  u2 : name_index; (指向常量池中的 Utf8)
  u2 : descriptor_index; (指向常量池中的 Utf8)
  u2 : attributes_count; (字段可能还有一些额外的属性,这是属性个数)
  attribute_info : attributes[attributes_count]
}

access_flags定义

标记名称

数值

描述

ACC_PUBLIC

0x0001

public

ACC_PRIVATE

0x0002

private

ACC_PROTECTED

0x0004

protected

ACC_STATIC

0x0008

static 静态字段

ACC_FINAL

0x0010

final 字段

ACC_VOLATILE

0x0040

是否为volatile

ACC_TRANSIENT

0x0080

是否为临时字段,不序列化

ACC_SYNTHETIC

0x1000

由编译器产生的字段,没有源码

ACC_ENUM

0x4000

是否为枚举

PageClass中的字段

第一个字段 :size

private static final int size = 10;

001A 001F 000C 0001 0020 0000 0002 0021

field_info {
  u2 : 001A;  (final + private + static)
  u2 : 001F;  (指向常量池中的 31-Utf8 : size)
  u2 : 000C;  (指向常量池中的 12-Utf8 : l)
  u2 : 0001;  (一个额外属性)
  attribute_info : {
    u2 : 0020; (指向常量池中的 32-Utf8 : Constant Value)
    u4 : 0000 0002; (2个字节)
    u2 : 0021; (指向常量池中的 33-Integer : 10)
  }
}
第二个字段 :page

private int page;

0002 000B 000C 0000

field_info {
  u2 : 0002;  (private)
  u2 : 000B;  (指向常量池中的 11-Utf8 : page)
  u2 : 000C;  (指向常量池中的 12-Utf8 : l)
  u2 : 0000;  (没有额外属性)
}

2.10 Class文件的方法

在字段之后,就是类的方法信息。方法信息和字段类似,由两部分组成:

u2 : methods_count;
method_info : methods[methods_count];

其中methods_count为2字节整数,表示该类中有几个方法。接着就是methods_count个method_info结构,每一个method_info结构表示一个方法,如下所示:

method_info {
  u2 : access_flags; (访问标记)
  u2 : name_index; (方法名,指向常量池的索引)
  u2 : descriptor_index;  (描述符,指向常量池的索引)
  u2 : attributes_count;
  attribute_info : attributes[attributes_count];
}

access_flag 方法访问标记

方法的访问标记,用于标明方法的权限及相关特性

标记名称

数值

描述

ACC_PUBLIC

0x0001

public

ACC_PRIVATE

0x0002

private

ACC_PROTECTED

0x0004

protected

ACC_STATIC

0x0008

static 静态方法

ACC_FINAL

0x0010

final 字段

ACC_SYNCHRONIZED

0x0020

synchronized同步方法

ACC_BRIDGE

0x0040

由编译器产生的桥接方法

ACC_VARARGS

0x0080

可变参数方法

ACC_NATIVE

0x0080

native方法

ACC_ABSTRACT

0x0400

抽象方法

ACC_STRICT

0x0800

浮点模式为 FP-Strict

ACC_SYNTHETIC

0x1000

由编译器产生的方法,没有源码

descriptor_index 方法描述符

descriptor_index为方法描述符,它也是指向常量池的索引,是一个字符串,表示方法的签名(参数、返回值等),同时对方法签名的表示做了一些规定。它将函数的参数类型写在一对小括号中,并在括号右侧给出方法的返回值。比如,若有如下方法:

Object m(int i, double d, Thread t){...}

则它的方法描述符为:

(IDLjava/lang/Thread;)Ljava/lang/Object;

可以看到,方法的参数统一列在一对小括号中,“I”表示int,“D”表示double,“Ljava/lang/Thread;”表示Thread对象。小括号右侧的Ljava/lang/Object;表示方法的返回值为Object对象。

为了测试一下各种基础变量的符号,在PageClass中临时添加一个方法

/**
 * 用于测试参数类型
 *
 * @param a
 * @param b
 * @param c
 * @param d
 * @param e
 * @param f
 * @param g
 * @param h
 * @param i
 * @param j
 * @return
 */
public static PageClass of(long a, float b, double c, int d, short e, 
        char f, byte g, boolean h, String i, EmptyClass j) {
    return new PageClass(0);
}

重新编译后生成的字符串常量为

(JFDISCBZLjava/lang/String;Lbytecode/EmptyClass;)Lbytecode/PageClass;

因此可以看出各个基础数据类型的对应

long

float

double

int

short

char

byte

boolean

J

F

D

I

S

C

B

Z

PageClass中的方法定义

从图中可以看到类有三个方法。

第一个方法:构造函数
0001 0005 000F 0001 
  0022 00000046 0002 0002 0000000A 
    2AB70001 2A1BB500 07B1  // 字节码
  0000 0002
    0023 0000000E 0003      // LineNumberTable
      0000  0018
      0004  0019
      0009  001A
    0024 00000016 0002      // LocalVariableTable
      0000 000A 0025 0026 0000 
      0000 000A 000B 000C 0001

{
  u2 : 0001;  (public)
  u2 : 0005;  (5-Utf8 : <init>)
  u2 : 000F;  (15-Utf8 : (l)V)
  u2 : 0001;  (一个属性)
  attribute_info {
    u2 : 0022;  (34-Utf8 : Code)
    u4 : 0000 0046;  (数据长度为70)
    u2 : 0002;  (操作数栈的最大深度)
    u2 : 0002;  (局部变量的最大个数)
    u4 : 0000000A;  (字节码长度=10)
    Code {    //2AB70001 2A1BB500 07B1;  (字节码内容)
      0 aload_0
      1 invokespecial #1 <java/lang/Object.<init>>
      4 aload_0
      5 iload_1
      6 putfield #7 <bytecode/PageClass.page>
      9 return
    }
    u2 : 0000;  (异常表的个数:0)
    u2 : 0002;  (两个属性)
    attributes [
      {
        u2 : 0023;  (35-Utf8 : LineNumberTable,源代码行号)
        u4 : 0000000E;  (长度 : 14)
        u2 : 0003;  (三个 line_number_info)
        line_number_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 0018;  (line_number : 24)  // public PageClass(int page) {
          },
          {
            u2 : 0004;  (start_pc : 4)
            u2 : 0019;  (line_number : 25)  // this.page = page;
          },
          {
            u2 : 0009;  (start_pc : 9)
            u2 : 001A;  (line_number : 26)  // }
          }
        ]
      },
      {
        u2 : 0024;  (36-Utf8 : LocalVariableTable , 内部变量)
        u4 : 00000016;  (长度 : 22)
        u1 : 0002;  (内部变量个数 : 2)
        local_variable_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 000A;  (length : 10)
            u2 : 0025;  (name_index,37-Utf8 : this)
            u2 : 0026;  (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
            u2 : 0000;  (index : 0 , 第1个内部变量)
          },
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 000A;  (length : 10)
            u2 : 000B;  (name_index,11-Utf8 : page)
            u2 : 000C;  (descriptor_index,12-Utf8 : l)
            u2 : 0001;  (index : 1 , 第2个内部变量)
          }
        ]
      }
    ]
  }
}
第二个方法:calculateOffset
0001 0018 0019 0001 
  0022 00000032 0002 0001 00000008
    2AB40007 100A68AC   // 字节码
  0000 0002
    0023 00000006 0001  // LineNumberTable
      0000 0022
    0024 0000000C 0001  // LocalVariableTable
      0000 0008 0025 0026 0000

{
  u2 : 0001;  (public)
  u2 : 0018;  (24-Utf8 : calculateOffset)
  u2 : 0019;  (25-Utf8 : ()l)
  u2 : 0001;  (一个属性)
  Code_attribute{
    u2 : 0022;  (34-Utf8 : Code)
    u4 : 0000 0032;  (数据长度为50)
    u2 : 0002;  (操作数栈的最大深度)
    u2 : 0001;  (局部变量的最大个数)
    u4 : 00000008;  (字节码长度=8)
    Code {    //2AB40007 100A68AC;  (字节码内容)
      0 aload_0
      1 getfield #7 <bytecode/PageClass.page>
      4 bipush 10
      6 imul
      7 ireturn  
    }
    u2 : 0000;  (异常表的个数:0)
    u2 : 0002;  (两个属性)
    attributes[
      {
        u2 : 0023;  (35-Utf8 : LineNumberTable,源代码行号)
        u4 : 00000006;  (长度 : 6)
        u2 : 0001;  (1个 line_number_info)
        line_number_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 0022;  (line_number : 34)  // return page * size;
          }
        ]
      },
      {
        u2 : 0024;  (36-Utf8 : LocalVariableTable , 内部变量)
        u4 : 0000000C;  (长度 : 12)
        u1 : 0001;  (内部变量个数 : 1)
        local_variable_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 0008;  (length : 8)
            u2 : 0025;  (name_index,37-Utf8 : this)
            u2 : 0026;  (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
            u2 : 0000;  (index : 0 , 第1个内部变量)
          }
        ]
      }
    ]
  } 
}
第三个方法:main
0009 0027 0028 0001 
  0022 00000050 0003 0002 00000014 
    BB000859 06B7000D 4CB20010 2BB60016 B6001AB1    // 字节码
  0000 0002
    0023 0000000E 0003    //LineNumberTable
      0000 002B
      0009 002C
      0013 002D 
    0024 00000016 0002    // LocalVariableTable
      0000 0014 0029 002A 0000 
      0009 000B 000B 0026 0001

{
  u2 : 0009;  (public + static)
  u2 : 0027;  (39-Utf8 : main)
  u2 : 0028;  [ 40-Utf8 : ([Ljava/lang/String;)V  ]
  u2 : 0001;  (一个属性)
  Code_attribute{
    u2 : 0022;  (34-Utf8 : Code)
    u4 : 0000 0050;  (数据长度为80)
    u2 : 0003;  (操作数栈的最大深度)
    u2 : 0002;  (局部变量的最大个数)
    u4 : 00000014;  (字节码长度=20)
    u1[20] : BB000859 06B7000D 4CB20010 2BB60016 B6001AB1;  (字节码内容)
    {
       0 new #8 <bytecode/PageClass>
       3 dup
       4 iconst_3
       5 invokespecial #13 <bytecode/PageClass.<init>>
       8 astore_1
       9 getstatic #16 <java/lang/System.out>
      12 aload_1
      13 invokevirtual #22 <bytecode/PageClass.calculateOffset>
      16 invokevirtual #26 <java/io/PrintStream.println>
      19 return
    }
    u2 : 0000;  (异常表的个数:0)
    u2 : 0002;  (两个属性)
    attributes[
      {
        u2 : 0023;  (35-Utf8 : LineNumberTable,源代码行号)
        u4 : 0000000E;  (长度 : 14)
        u2 : 0003;  (3个 line_number_info)
        line_number_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 002B;  (line_number : 33)  // PageClass page = new PageClass(3);
          },
          {
            u2 : 0009;  (start_pc : 0)
            u2 : 002C;  (line_number : 44)  // System.out.println(page.calculateOffset());
          },
          {
            u2 : 0013;  (start_pc : 0)
            u2 : 002D;  (line_number : 45)  // }
          }
        ]
      }
      {
        u2 : 0024;  (36-Utf8 : LocalVariableTable , 内部变量)
        u4 : 00000016;  (长度 : 22)
        u1 : 0002;  (内部变量个数 : 2)
        local_variable_tables[
          {
            u2 : 0000;  (start_pc : 0)
            u2 : 0014;  (length : 20)
            u2 : 0029;  (name_index,41-Utf8 : args)
            u2 : 002A;  (descriptor_index,42-Utf8 : Ljava/lang/String;)
            u2 : 0000;  (index : 0 , 第1个内部变量)
          },
          {
            u2 : 0009;  (start_pc : 0)
            u2 : 000B;  (length : 20)
            u2 : 000B;  (name_index,11-Utf8 : page)
            u2 : 0026;  (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
            u2 : 0001;  (index : 1 , 第2个内部变量)
          }
        ]
      }
    ]
  } 
}

2.11 Class文件的其他属性

Class文件的最后一部分内容,是类额外的源代码信息。

{
  u2 : 0001; (有一个扩展属性)
  attributes[
    {
      u2 : 002B;  (43-Utf8 : SourceFile)
      u4 : 0000002C;  
      u2 : 002C;  (44-Utf8 : PageClass.java)
    }
  ]
}

3 class 文件内容查看工具

简单列一下常用的内容查看工具。

3.1 hex editor

16进制编辑器,看到的是原生态的数据,就是看着累。Mac上我下载了一个免费的 iHex。左边16进制,右边ascii码,状态栏可以显示十进制。

3.2 javap

JDK自带的查看工具

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

javap的用法格式:javap <options> <classes>,其中classes就是class文件。

在命令行中直接输入javapjavap -help可以看到javap的options有如下选项:

用法: javap <options> <classes>
其中, 可能的选项包括:
  -? -h --help -help               输出此帮助消息
  -version                         版本信息,javap 的版本,不是 class 文件的编译版本
  -v  -verbose                     输出附加信息
  -l                               输出行号和本地变量表
  -public                          仅显示公共类和成员
  -protected                       显示受保护的/公共类和成员
  -package                         显示程序包/受保护的/公共类
                                   和成员 (默认)
  -p  -private                     显示所有类和成员
  -c                               对代码进行反汇编
  -s                               输出内部类型签名
  -sysinfo                         显示正在处理的类的
                                   系统信息(路径、大小、日期、SHA-256 散列)
  -constants                       显示最终常量
  --module <模块>, -m <模块>       指定包含要反汇编的类的模块
  --module-path <路径>             指定查找应用程序模块的位置
  --system <jdk>                   指定查找系统模块的位置
  --class-path <路径>              指定查找用户类文件的位置
  -classpath <路径>                指定查找用户类文件的位置
  -cp <路径>                       指定查找用户类文件的位置
  -bootclasspath <路径>            覆盖引导类文件的位置
  --multi-release <version>        指定要在多发行版 JAR 文件中使用的版本

用javap查看一下前面分析的 PageClass.class 文件。使用选项 -v ,基本上能看到所有的内容,其他就不一一看了。

% javap -v target/classes/bytecode/PageClass.class 
Classfile /Users/liaojunyong/Workspaces/ResearchDistributed/research-java/target/classes/bytecode/PageClass.class
  Last modified 2019年12月18日; size 743 bytes
  SHA-256 checksum c8ea8361a2784888c50d6c7d3ad2fa825ffa34fa5945d437f1bdb194950dd58e
  Compiled from "PageClass.java"
  
public class bytecode.PageClass
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // bytecode/PageClass
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // bytecode/PageClass.page:I
   #8 = Class              #10            // bytecode/PageClass
   #9 = NameAndType        #11:#12        // page:I
  #10 = Utf8               bytecode/PageClass
  #11 = Utf8               page
  #12 = Utf8               I
  #13 = Methodref          #8.#14         // bytecode/PageClass."<init>":(I)V
  #14 = NameAndType        #5:#15         // "<init>":(I)V
  #15 = Utf8               (I)V
  #16 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Class              #19            // java/lang/System
  #18 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;
  #19 = Utf8               java/lang/System
  #20 = Utf8               out
  #21 = Utf8               Ljava/io/PrintStream;
  #22 = Methodref          #8.#23         // bytecode/PageClass.calculateOffset:()I
  #23 = NameAndType        #24:#25        // calculateOffset:()I
  #24 = Utf8               calculateOffset
  #25 = Utf8               ()I
  #26 = Methodref          #27.#28        // java/io/PrintStream.println:(I)V
  #27 = Class              #29            // java/io/PrintStream
  #28 = NameAndType        #30:#15        // println:(I)V
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               size
  #32 = Utf8               ConstantValue
  #33 = Integer            10
  #34 = Utf8               Code
  #35 = Utf8               LineNumberTable
  #36 = Utf8               LocalVariableTable
  #37 = Utf8               this
  #38 = Utf8               Lbytecode/PageClass;
  #39 = Utf8               main
  #40 = Utf8               ([Ljava/lang/String;)V
  #41 = Utf8               args
  #42 = Utf8               [Ljava/lang/String;
  #43 = Utf8               SourceFile
  #44 = Utf8               PageClass.java
{
  public bytecode.PageClass(int);
    descriptor: (I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #7                  // Field page:I
         9: return
      LineNumberTable:
        line 24: 0
        line 25: 4
        line 26: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lbytecode/PageClass;
            0      10     1  page   I

  public int calculateOffset();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field page:I
         4: bipush        10
         6: imul
         7: ireturn
      LineNumberTable:
        line 34: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lbytecode/PageClass;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #8                  // class bytecode/PageClass
         3: dup
         4: iconst_3
         5: invokespecial #13                 // Method "<init>":(I)V
         8: astore_1
         9: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        12: aload_1
        13: invokevirtual #22                 // Method calculateOffset:()I
        16: invokevirtual #26                 // Method java/io/PrintStream.println:(I)V
        19: return
      LineNumberTable:
        line 43: 0
        line 44: 9
        line 45: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  args   [Ljava/lang/String;
            9      11     1  page   Lbytecode/PageClass;
}
SourceFile: "PageClass.java"

3.3 jclasslib

图形化的字节码阅读器。https://github.com/ingokegel/jclasslib ,能在Windows/Mac/Linux下运行,也提供IDEA插件。

打开PageClass.class查看,前面我们分析的所有数据,都在这里面。

常量池的常量都有序号,各种 ref 也直接可以显示ref的具体值,比较方便。

4 编译属性

4.1 javac 参数

直接输入 javac 查看参数说明

% javac
用法: javac <options> <source files>
其中, 可能的选项包括:
  @<filename>                  从文件读取选项和文件名
  -Akey[=value]                传递给注释处理程序的选项
  --add-modules <模块>(,<模块>)*
        除了初始模块之外要解析的根模块; 如果 <module>
                为 ALL-MODULE-PATH, 则为模块路径中的所有模块。
  --boot-class-path <path>, -bootclasspath <path>
        覆盖引导类文件的位置
  --class-path <path>, -classpath <path>, -cp <path>
        指定查找用户类文件和注释处理程序的位置
  -d <directory>               指定放置生成的类文件的位置
  -deprecation                 输出使用已过时的 API 的源位置
  --enable-preview             启用预览语言功能。要与 -source 或 --release 一起使用。
  -encoding <encoding>         指定源文件使用的字符编码
  -endorseddirs <dirs>         覆盖签名的标准路径的位置
  -extdirs <dirs>              覆盖所安装扩展的位置
  -g                           生成所有调试信息
  -g:{lines,vars,source}       只生成某些调试信息
  -g:none                      不生成任何调试信息
  -h <directory>               指定放置生成的本机标头文件的位置
  --help, -help, -?            输出此帮助消息
  --help-extra, -X             输出额外选项的帮助
  -implicit:{none,class}       指定是否为隐式引用文件生成类文件
  -J<flag>                     直接将 <标记> 传递给运行时系统
  --limit-modules <模块>(,<模块>)*
        限制可观察模块的领域
  --module <模块>(,<模块>)*, -m <模块>(,<模块>)*
        只编译指定的模块,请检查时间戳
  --module-path <path>, -p <path>
        指定查找应用程序模块的位置
  --module-source-path <module-source-path>
        指定查找多个模块的输入源文件的位置
  --module-version <版本>        指定正在编译的模块版本
  -nowarn                      不生成任何警告
  -parameters                  生成元数据以用于方法参数的反射
  -proc:{none,only}            控制是否执行注释处理和/或编译。
  -processor <class1>[,<class2>,<class3>...]
        要运行的注释处理程序的名称; 绕过默认的搜索进程
  --processor-module-path <path>
        指定查找注释处理程序的模块路径
  --processor-path <path>, -processorpath <path>
        指定查找注释处理程序的位置
  -profile <profile>           请确保使用的 API 在指定的配置文件中可用
  --release <release>          为指定的 Java SE 发行版编译。支持的发行版:7, 8, 9, 10, 11, 12, 13
  -s <directory>               指定放置生成的源文件的位置
  --source <release>, -source <release>
        提供与指定的 Java SE 发行版的源兼容性。支持的发行版:7, 8, 9, 10, 11, 12, 13
  --source-path <path>, -sourcepath <path>
        指定查找输入源文件的位置
  --system <jdk>|none          覆盖系统模块位置
  --target <release>, -target <release>
        生成适合指定的 Java SE 发行版的类文件。支持的发行版:7, 8, 9, 10, 11, 12, 13
  --upgrade-module-path <path>
        覆盖可升级模块位置
  -verbose                     输出有关编译器正在执行的操作的消息
  --version, -version          版本信息
  -Werror                      出现警告时终止编译

4.2 Maven配置编译属性

在pom.xml中,可以为 compile 指定参数,如

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <source>8</source>
        <target>8</target>
        <encoding>UTF-8</encoding>
        <compilerArgs>
            <compilerArgument>-g:lines</compilerArgument>
            <compilerArgument>-g:vars</compilerArgument>
            <compilerArgument>-g:source</compilerArgument>
            <compilerArgument>-verbose</compilerArgument>
        </compilerArgs>
    </configuration>
</plugin>

4.3 编译时不输出任何调试信息

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <source>8</source>
        <target>8</target>
        <encoding>UTF-8</encoding>
        <compilerArgs>
            <compilerArgument>-g:none</compilerArgument>
        </compilerArgs>
    </configuration>
</plugin>

查看生成的class文件内容:没有LineNumberTable/LocalVariableTable/SourceFile信息。

5 反编译

5.1 反编译工具

jd-gui

在网站 http://java-decompiler.github.io/ 可以下载。在 MacBook 上执行下载的程序,可能会被macOS的安全认证拦截掉,提示“macOS无法验证App不包含恶意软件”。这时候我们假定jd-gui是安全的,就可以按住 Control键的时候打开软件。

IDEA集成的Fernflower decompiler

IDEA缺省集成了反编译工具,只需要在IDEA中直接打开一个class文件,就能看到反编译的结果。

5.2 反编译结果

缺省反编译的结果

// idea 反编译结果
package bytecode;
public class PageClass {
    private static final int size = 10;
    private int page;
    public PageClass(int page) {
        this.page = page;
    }
    public int calculateOffset() {
        return this.page * 10;
    }
    public static void main(String[] args) {
        PageClass page = new PageClass(3);
        System.out.println(page.calculateOffset());
    }
}

可以看到,区别只有常量10。calculateOffset()代码中的size,被替换成数字10。

不编译调试信息后的反编译

// idea 反编译结果
package bytecode;
public class PageClass {
    private static final int size = 10;
    private int page;
    public PageClass(int var1) {
        this.page = var1;
    }
    public int calculateOffset() {
        return this.page * 10;
    }
    public static void main(String[] var0) {
        PageClass var1 = new PageClass(3);
        System.out.println(var1.calculateOffset());
    }
}

可以看到,区别在于函数内的变量名被替换掉:

  • 构造函数 PageClass(int page) --> PageCass(int var1)
  • PageClass page = new PageClass(3); --> PageClass var1 = new PageClass(3);

这是因为字节码中缺少 LocalVariableTable 的原因。这时再看一下jad-gui的反编译结果,在变量命名上有一些区别:jad-gui使用了 paramInt。如果一个函数变量有多个相同类型的参数,会自动添加数字如 paramInt1 / paramInt2。

// jd-gui 反编译结果
package bytecode;
public class PageClass {
  private static final int size = 10;
  private int page; 
  public PageClass(int paramInt) { this.page = paramInt; }
  public int calculateOffset() { return this.page * 10; }
  public static void main(String[] paramArrayOfString) {
    PageClass pageClass = new PageClass(3);
    System.out.println(pageClass.calculateOffset());
  }
}

二 字节码

前面分析Class结构的时候,在属性Code中,有字节码的具体值。当时是直接从javap分析出的数据直接拷贝的内容。这里只是大概了解一下。

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。命令格式Opcode Operands。比如我们前面PageClass中的main()方法生成的字节码,红框内是 Opcode,蓝框内是 Operands。

1 运行时栈帧结构

栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(VirtualMachineStack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(CurrentStackFrame),与这个栈帧相关联的方法称为当前方法(CurrentMethod)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作,在概念模型上,典型的栈帧结构

1.1 局部变量表

是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以变量槽(VariableSlot,下称Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型。简单理解为每个Slot的大小为32位,如果需要保存long和double,就占用两个Slot。对于占了两个Slot的long和double,不允许直接访问其中Slot,否则在类加载的校验阶段就会抛异常。

这里还有很多高级的细节,比如:Slot的重用、变量逃逸分析等,这儿就不说了。

1.2 操作数栈

操作数栈(OperandStack)也常称为操作栈,它是一个后入先出(LastInFirstOut,LIFO)栈。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

1.3 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(DynamicLinking)。我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

1.4 方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者;另外一种退出方式是,在方法执行过程中遇到了异常被抛出,就会导致方法退出,这时不会给它的上层调用者产生任何返回值。

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

2 字节码与数据类型

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。

例如,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。

2.1 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容。<n>代表的是一组指令: _0 _1 _2

load - 局部变量 --> 到操作栈

iload、iload_<n>
lload、lload_<n>
fload、fload_<n>
dload、dload_<n>
aload、aload_<n>

store - 操作数栈 --> 局部变量表

istore、istore_<n>
lstore、lstore_<n>
fstore、fstore_<n>
dstore、dstore_<n>
astore、astore_<n>

常量 --> 操作数栈

bipush、sipush
ldc、ldc_w、ldc2_w
aconst_null
iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

扩充局部变量表的访问索引

wide

2.2 运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。

加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

2.3 类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

Java虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换)

int --> long、float double
long --> float、double
float --> double

相对的,处理窄化类型转换(NarrowingNumericConversions)时,必须显式地使用转换指令来完成,这些转换指令包括:

i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,转换过程很可能会导致数值的精度丢失。

2.4 对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下。

创建类实例的指令:new
创建数组的指令:newarray、anewarray、multianewarray
访问类字段(static)的指令: getstatic、putstatic
访问实例字段(非static)的指令:getfield、putfield
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。

2.5 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
将栈最顶端的两个数值互换:swap。

2.6 控制转移指令

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

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、
      ifnull、ifnonnull、
      if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、
      if_icmpge、if_acmpeq、if_acmpne。
复合条件分支:tableswitch、lookupswitch。
无条件分支:goto、goto_w、jsr、jsr_w、ret。

2.7 方法调用和返回指令

列举以下5条用于方法调用的指令。

invokevirtual指令

用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令

用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令

用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokestatic指令

用于调用类方法(static方法)。

invokedynamic指令

用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。

前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

2.8 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。例如,在整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。

而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。

2.9 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用Monitor来支持的。

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有Monitor,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放Monitor。在方法执行期间,执行线程持有了Monitor,其他任何线程都无法再获取到同一个Monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的Monitor将在异常抛到同步方法之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。

3 分析PageClass的字节码命令

以 PageClass.calculateOffset()为例来分析,足够简单

3.1 字节码和变量表

Code {
  0 aload_0
  1 getfield #7 <bytecode/PageClass.page>
  4 bipush 10
  6 imul
  7 ireturn  
}

local_variable_tables[
  {
    u2 : 0000;  (start_pc : 0)
    u2 : 0008;  (length : 8)
    u2 : 0025;  (name_index,37-Utf8 : this)
    u2 : 0026;  (descriptor_index,38-Utf8 : Lbytecode/PageClass;)
    u2 : 0000;  (index : 0 , 第1个内部变量)
  }
]

3.2 执行过程

调用 calculateOffset() 的代码如下,进入到方法后, page 变量的值为3。

PageClass page = new PageClass(3);
System.out.println(page.calculateOffset());

执行过程及操作数栈的变化

字节码

初始

aload_0

getfield #7

bipush 10

imul

ireturn

解释

加载变量0

读变量#7(page)

入栈10

乘法

返回

操作数栈

this

this

this

This

3

3

30

10

imul:取出栈顶的两个数相乘,并写回栈。

3思考

为什么代码是 page*size,但压栈的时候直接bipush 10?

测试

将size的final删掉并重新编译(修改前后的代码一起展示)

/**
 * 定义常量
 */
private static final int size = 10;


/**
 * 定义常量
 */
private static int size = 10;

再看calculateOffset()的字节码(修改前后的字节码一起展示)

0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 bipush 10
6 imul
7 ireturn	


0 aload_0
1 getfield #7 <bytecode/PageClass.page>
4 getstatic #13 <bytecode/PageClass.size>
7 imul
8 ireturn

本文分享自微信公众号 - 程序猿讲故事(codestory),作者:程序猿讲故事

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • ZooKeeper实现同步屏障(Barrier)

    按照维基百科的解释:同步屏障(Barrier)是并行计算中的一种同步方法。对于一群进程或线程,程序中的一个同步屏障意味着任何线程/进程执行到此后必须等待,直到所...

    程序猿讲故事
  • JSON金额解析BUG的解决过程

    这是在我们开发的一个支付系统中暴露的一个BUG,问题本身比较简单,有意思的是解决问题的过程。将过程分享出来,希望能够对大家有所帮助。

    程序猿讲故事
  • 基于ZooKeeper的三种分布式锁实现

    今天介绍基于ZooKeeper的分布式锁的简单实现,包括阻塞锁和非阻塞锁。同时增加了网上很少介绍的基于节点的非阻塞锁实现,主要是为了加深对ZooKeeper的理...

    程序猿讲故事
  • UIKit Dynamics:开始入门 —《Graphics & Animation系列一》

    翻译自raywenderlich网站iOS教程Graphics & Animation系列 介绍 UIKit Dynamics是一个集成到UIKit中的完整物理...

    用户3539187
  • Cloudera Manager分发Parcel异常分析

    在使用Cloudera Manager分发Parcel包时一直处于激活状态不变,相关CM日志及CM界面截图如下:

    Fayson
  • Java 中的协程库 - Quasar

    一个进程可以产生许多线程,每个线程有自己的上下文,当我们在使用多线程的时候,如果存在长时间的 I/O 操作,线程会一直处于阻塞状态,这个时候会存在很多线程处于空...

    JMCui
  • iOS四大对象之UIWindow及四大对象之间的关系1. UIWindow/使用纯代码加载根控制器2. UIWindow的创建过程3. 四大对象之间的关系

    stanbai
  • 如何在CM中启用YARN的使用率报告

    CDH的高级功能"群集利用率报告"(Cluster Utilization Report)是整个多租户方案体系里的一部分,可以用来查看租户的资源使用情况,并可以...

    Fayson
  • 8. Kotlin 函数声明与默认参数(Default argument)

    在 Java 中,当我们要实现同一种功能,但函数入参出参不一样的函数的时候,我们可以用到 Java 的函数重载功能。在 Android framework 中同...

    sickworm
  • 轻量级交互数据json格式初探

    什么是 JSON ? JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation) JSON 是轻量级的文本数据交...

    李海彬

扫码关注云+社区

领取腾讯云代金券