java字节码


1 首先我们来写一个java源文件

java字节码

javasrc.png

上面是我们写的一个java程序,很简单,只有一个成员变量a以及一个方法testMethod() 。

2 接下来我们用javac命令或者ide工具将该java源文件编译成java字节码文件。

java字节码

demo.png

上图是编译好的字节码文件,我们可以看到一堆16进制的字节。如果你使用IDE去打开,也许看到的是已经被反编译的我们所熟悉的java代码,而这才是纯正的字节码,这也是我们今天需要讲的内容重点。

也许你会对这样一堆字节码感到头疼,不过没关系,我们慢慢试着你看懂它,或许有不一样的收获。在开始之前我们先来看一张图

java字节码

java_byte.jpeg

这张图是一张java字节码的总览图,我们也就是按照上面的顺序来对字节码进行解读的。一共含有10部分,包含魔数,版本号,常量池等等,接下来我们按照顺序一步一步解读。

3.1 魔数

从上面的总览图中我们知道前4个字节表示的是魔数,对应我们Demo的是 0XCAFE BABE。什么是魔数?魔数是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE表示的是class文件,那么有人会问,文件类型可以通过文件名后缀来判断啊?是的,但是文件名是可以修改的(包括后缀),那么为了保证文件的安全性,将文件类型写在文件内部来保证不被篡改。
从java的字节码文件类型我们看到,CAFE BABE翻译过来是咖啡宝贝之意,然后再看看java图标。

java字节码

java_icon.png

CAFE BABE = 咖啡。

3.2 版本号

我们识别了文件类型之后,接下来要知道版本号。版本号含主版本号和次版本号,都是各占2个字节。在此Demo种为0X0000 0033。其中前面的0000是次版本号,后面的0033是主版本号。通过进制转换得到的是次版本号为0,主版本号为51。
从oracle官方网站我们能够知道,51对应的正式jdk1.7,而其次版本为0,所以该文件的版本为1.7.0。如果需要验证,可以在用java –version命令输出版本号,或者修改编译目标版本–target重新编译,查看编译后的字节码文件版本号是否做了相应的修改。

至此,我们共了解了前8字节的含义,下面讲讲常量池相关内容。

3.3 常量池

紧接着主版本号之后的就是常量池入口。常量池是Class文件中的资源仓库,在接下来的内容中我们会发现很多地方会涉及,如Class Name,Interfaces等。常量池中主要存储2大类常量:字面量和符号引用。字面量如文本字符串,java中声明为final的常量值等等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符。

为什么需要类和接口的全局限定名呢?系统引用类或者接口的时候不是通过内存地址进行操作吗?这里大家仔细想想,java虚拟机在没有将类加载到内存的时候根本都没有分配内存地址,也就不存在对内存的操作,所以java虚拟机首先需要将类加载到虚拟机中,那么这个过程设计对类的定位(需要加载A包下的B类,不能加载到别的包下面的别的类中),所以需要通过全局限定名来判别唯一性。这就是为什么叫做全局,限定的意思,也就是唯一性。

在进行具体常量池分析之前,我们先来了解一下常量池的项目类型表:

java字节码

jvm_constant.png

上面的表中描述了11中数据类型的结构,其实在jdk1.7之后又增加了3种(CONSTANT_MethodHandle_info,CONSTANT_MethodType_info以及CONSTANT_InvokeDynamic_info)。这样算起来一共是14种。接下来我们按照Demo的字节码进行逐一翻译。

0×0015:由于常量池的数量不固定(n+2),所以需要在常量池的入口处放置一项u2类型的数据代表常量池数量。因此该16进制是21,表示有20项常量,索引范围为1~20。明明是21,为何是20呢?因为Class文件格式规定,设计者就讲第0项保留出来了,以备后患。从这里我们知道接下来我们需要翻译出20项常量。
Constant #1 (一共有20个常量,这是第一个,以此类推…)
0x0a-:(化为十进制 10)从常量类型表中我们发现,第一个数据均是u1类型的tag,16进制的0a是十进制的10,对应表中的MethodRef_info。
0x-00 04-:Class_info索引项#4
0x-00 11-:NameAndType索引项#17
Constant #2
0x-09: FieldRef_info
0×0003 :Class_info索引项#3
0×0012:NameAndType索引项#18
Constant #3
0×07-: Class_info
0x-00 13-: 全局限定名常量索引为#19
Constant #4
0x-07 :Class_info
0×0014:全局限定名常量索引为#20
Constant #5
0×01:Utf-8_info
0x-00 01-:字符串长度为1(选择接下来的一个字节长度转义)
0x-61:”a”(十六进制转ASCII字符)
Constant #6
0×01:Utf-8_info
0x-00 01:字符串长度为1
0x-49:”I”
Constant #7
0×01:Utf-8_info
0x-00 06:字符串长度为6
0x-3c 696e 6974 3e-:”<init>”
Constant #8
0×01 :UTF-8_info
0×0003:字符串长度为3
0×2829 56:”()V”
Constant #9
0x-01:Utf-8_info
0×0004:字符串长度为4
0x436f 6465:”Code”
Constant #10
0×01:Utf-8_info
0×00 0f:字符串长度为15
0x4c 696e 654e 756d 6265 7254 6162 6c65:”LineNumberTable”
Constant #11
ox01: Utf-8_info
0×00 12字符串长度为18
0x-4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65:”LocalVariableTable”
Constant #12
0×01:Utf-8_info
0×0004 字符串长度为4
0×7468 6973 :”this”
Constant #13
0×01:Utf-8_info
0x0f:字符串长度为15
0x4c 636f 6d2f 6465 6d6f 2f44 656d 6f3b:”Lcom/demo/Demo;”
Constant #14
0×01:Utf-8_info
0×00 0a:字符串长度为10
ox74 6573 744d 6574 686f 64:”testMethod”
Constant #15
0×01:Utf-8_info
0x000a:字符串长度为10
0x536f 7572 6365 4669 6c65 :”SourceFile”
Constant #16
0×01:Utf-8_info
0×0009:字符串长度为9
0x-44 656d 6f2e 6a61 7661 :”Demo.java”
Constant #17
0x0c :NameAndType_info
0×0007:字段或者名字名称常量项索引#7
0×0008:字段或者方法描述符常量索引#8
Constant #18
0x0c:NameAndType_info
0×0005:字段或者名字名称常量项索引#5
0×0006:字段或者方法描述符常量索引#6
Constant #19
0×01:Utf-8_info
0×00 0d:字符串长度为13
0×63 6f6d 2f64 656d 6f2f 4465 6d6f:”com/demo/Demo”
Constant #20
0×01:Utf-8_info
0×00 10 :字符串长度为16
0x6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 :”java/lang/Object”
到这里为止我们解析了所有的常量。接下来是解析访问标志位。

3.4 Access_Flag 访问标志

访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public。

java字节码

access_flag.png

0x 00 21:是0×0020和0×0001的并集。其中0×0020这个标志值涉及到了字节码指令,后期会有专题对字节码指令进行讲解。期待中……

3.5 类索引

类索引用于确定类的全限定名
0×00 03 表示引用第3个常量,同时第3个常量引用第19个常量,查找得”com/demo/Demo”。#3.#19

3.6父类索引

0×00 04 同理:#4.#20(java/lang/Object)

3.7 接口索引

通过java_byte.jpeg图我们知道,这个接口有2+n个字节,前两个字节表示的是接口数量,后面跟着就是接口的表。我们这个类没有任何接口,所以应该是0000。果不其然,查找字节码文件得到的就是0000。

3.8 字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
同样,接下来就是2+n个字段属性。我们只有一个属性a,按道理应该是0001。查找文件果不其然是0001。
那么接下来我们要针对这样的字段进行解析。附上字段表结构图

java字节码

字段表结构.png

0×00 02 :访问标志为private(自行搜索字段访问标志)
0×00 05 : 字段名称索引为#5,对应的是”a”
0x 00 06 :描述符索引为#6,对应的是”I”
0x 00 00 :属性表数量为0,因此没有属性表。
tips:一些不太重要的表(字段,方法访问标志表)可以自行搜索,这里就不贴出来了,防止篇幅过大。

3.9 方法

我们只有一个方法testMethod,按照道理应该前2个字节是0001。通过查找发现是0×00 02。这是什么原因,这代表着有2个方法呢?且继续看……

java字节码

方法表结构.png

上图是一张方法表结构图,按照这个图我们分析下面的字节码:

第1个方法:

0×00 01:访问标志 ACC_PUBLIC,表明该方法是public。(可自行搜索方法访问标志表)
0×00 07:方法名索引为#7,对应的是”<init>”
0×00 08:方法描述符索引为#8,对应的是”()V”
0×00 01:属性表数量为1(一个属性表)
那么这里涉及到了属性表。什么是属性表呢?可以这么理解,它是为了描述一些专有信息的,上面的方法带有一张属性表。所有属性表的结构如下图:
一个u2的属性名称索引,一个u2的属性长度加上属性长度的info。
虚拟机规范预定义的属性有很多,比如Code,LineNumberTable,LocalVariableTable,SourceFile等等,这个网上可以搜索到。

java字节码

属性表结构.png

按照上面的表结构解析得到下面信息:
0×0009:名称索引为#9(“Code”)。
0×000 00038:属性长度为56字节。
那么接下来解析一个Code属性表,按照下图解析

java字节码

code.png

前面6个字节(名称索引2字节+属性长度4字节)已经解析过了,所以接下来就是解析剩下的56-6=50字节即可。
0×00 02 :max_stack=2
0×00 01 : max_locals=1
0×00 0000 0a : code_length=10
0x2a b700 012a 04b5 0002 b1 : 这是code代码,可以通过虚拟机字节码指令进行查找。
2a=aload_0(将第一个引用变量推送到栈顶)
b7=invokespecial(调用父类构造方法)
00=什么都不做
01 =将null推送到栈顶
2a=同上
04=iconst_1 将int型1推送到栈顶
b5=putfield 为指定的类的实例变量赋值
00= 同上
02=iconst_m1 将int型-1推送栈顶
b1=return 从当前方法返回void
整理,去除无动作指令得到下面
0 : aload_0
1 : invokespecial
4 : aload_0
5 : iconst_1
6 : putfield
9 : return
关于虚拟机字节码指令这块内容,后期会继续深入下去…… 目前只需要了解即可。接下来顺着Code属性表继续解析下去:
0×00 00 : exception_table_length=0
0×00 02 : attributes_count=2(Code属性表内部还含有2个属性表)
0×00 0a: 第一个属性表是”LineNumberTable”

java字节码

LineNumberTable.png

0×00 0000 0a : “属性长度为10″
0×00 02 :line_number_table_length=2
line_number_table是一个数量为line_number_table_length,类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号
0×00 00 : start_pc =0
0×00 03 : end_pc =3
0×00 04 : start_pc=4
0×00 04 : end_pc=4

0×00 0b 第二个属性表是:”LocalVariableTable”

java字节码
local_variable_table.png

java字节码

local_variable_info.png

0×00 0000 0c:属性长度为12
0×00 01 : local_variable_table_length=1
然后按照local_variable_info表结构进行解析:
0×00 00 : start_pc=0
0×00 0a:length=10
0x000c : name_index=”this”
0x000d : descriptor_index #13 (“Lcom/demo/Demo”)
0000 index=0
//——-到这里第一个方法就解析完成了——-//
Method(<init>)–1个属性Code表-2个属性表(LineNumberTable ,LocalVariableTable)接下来解析第二个方法

第2个方法:

0×00 04:”protected”
0×00 0e: #14(”testMethod”)
0×00 08 : “()V”
0×0001 : 属性数量=1
0×0009 :”Code”
0×0000 002b 属性长度为43
解析一个Code表
0000 :max_stack =0
0001 : max_local =1
0000 0001 : code_length =1
0xb1 : return(该方法返回void)
0×0000 异常表长度=0
0×0002 属性表长度为2
//第一个属性表
0x000a : #10,LineNumberTable
0×0000 0006 : 属性长度为6
0×0001 : line_number_length = 1
0×0000 : start_pc =0
0×0008 : end_pc =8
//第二个属性表
0x000b : #11 ,LocalVariableTable
0×0000 000c : 属性长度为12
0×0001 : local_variable_table_length =1
0×0000 :start_pc = 0
0×0001: length = 1
0x000c : name_index =#12 “this”
0x000d : 描述索引#13 “Lcom/demo/Demo;”
0000 index=0

//到这里为止,方法解析都完成了,回过头看看顶部解析顺序图,我们接下来就要解析Attributes了。

3.10 Attribute

0×0001 :同样的,表示有1个Attributes了。
0x000f : #15(“SourceFile”)
0×0000 0002 attribute_length=2
0×0010 : sourcefile_index = #16(“Demo.java”)
SourceFile属性用来记录生成该Class文件的源码文件名称。

java字节码

source_file.jpeg

4 另话

其实,我们写了这么多确实很麻烦,不过这种过程自己体验一遍的所获所得还是不同的。现在,使用java自带的反编译器来解析字节码文件。
javap -verbose Demo //不用带后缀.class

java字节码

1. 第一部分, 基础概念

变量

局部变量

JVM是一个基于栈的架构。方法执行的时候(包括main方法),在栈上会分配一个新的帧,这个栈帧包含一组局部变量。这组局部变量包含了方法运行过程中用到的所有变量,包括this引用,所有的方法参数,以及其它局部定义的变量。对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。

局部变量可以是以下这些类型:

* char
* long
* short
* int
* float
* double
* 引用
* 返回地址

除了long和double类型外,每个变量都只占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽,因为这些类型是64位的。

当一个新的变量创建的时候,操作数栈(operand stack)会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。

比如:

1
int i = 5;

编译后就成了

0: bipush      5
2: istore_0
 bipush  用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。
 istore_0 

这是istore_这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。

不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型数字存储到本地变量中

n代表的是局部变量区中的位置,并且只能是0,1,2,3。再多的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。

这条指令执行的时候,内存布局是这样的:

java字节码

class文件中的每一个方法都会包含一个局部变量表,如果这段代码在一个方法里面的话,你会在类文件的局部变量表中发现如下的一条记录。

1
2
3
LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      1      1     i         I
字段

Java类里面的字段是作为类对象实例的一部分,存储在堆里面的(类变量对应存储在类对象里面)。

关于字段的信息会添加到类文件里的field_info数组里,像下面这样:

java字节码
ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info contant_pool[constant_pool_count – 1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
java字节码

另外,如果变量被初始化了,那么初始化的字节码会加到构造方法里。

下面这段代码编译了之后:

public class SimpleClass {

    public int simpleField = 100;

}

如果你用javap进行反编译,这个被添加到了field_info数组里的字段会多出一段描述信息。

1 public int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC

初始化变量的字节码会被加到构造方法里,像下面这样:

java字节码
 1 public SimpleClass();
 2   Signature: ()V
 3   flags: ACC_PUBLIC
 4   Code:
 5     stack=2, locals=1, args_size=1
 6        0: aload_0
 7        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 8        4: aload_0
 9        5: bipush        100
10        7: putfield      #2                  // Field simpleField:I
11       10: return
java字节码
aload_0

局部变量数组中加载一个对象引用到操作数栈的栈顶。尽管这段代码看起来没有构造方法,

但是在编译器生成的默认的构造方法里,就会包含这段初始化的代码。第一个局部变量正好是this引用,

于是aload_0把this引用压到操作数栈中。aload_0是aload_指令集中的一条,这组指令会将引用加载到操作数栈中。

n对应的是局部变量数组中的位置,并且也只能是0,1,2,3。还有类似的加载指令,它们加载的并不是对象引用,

比如iload_,lload_,fload_,和dload_, 这里i代表int,l代表long,f代表float,d代表double。

局部变量的在数组中的位置大于3的,得通过iload,lload,fload,dload,和aload进行加载,

这些指令都接受一个操作数,它代表的是要加载的局部变量的在数组中的位置。

invokespecial

这条指令可以用来调用对象实例的构造方法,私有方法和父类中的方法

它是方法调用指令集中的一条,其它的还有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.

这里的invokespecial指令调用的是父类也就是java.lang.Object的构造方法。

bipush 它是用来把一个字节作为整型压到操作数栈中的,在这里100会被压到操作数栈里。
putfield

接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是simpleField。

赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都 会从操作数栈顶上pop出来。

前面的aload_0指令已经把包含这个字段的对象压到操作数栈上了,而后面的bipush又把100压到栈里

最后putfield指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的simpleField这个字段的值更新成了100。

上述代码执行的时候内存里面是这样的:

java字节码

这里的putfield指令的操作数引用的是常量池里的第二个位置。JVM会为每个类型维护一个常量池,

运行时的数据结构有点类似一个符号表,尽管它包含的信息更多。Java中的字节码操作需要对应的数据

但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,

而字节码包含一个常量池里的引用 。当类文件生成的时候,其中的一块就是常量池:

java字节码
 1 Constant pool:
 2    #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
 3    #2 = Fieldref           #3.#17         //  SimpleClass.simpleField:I
 4    #3 = Class              #13            //  SimpleClass
 5    #4 = Class              #19            //  java/lang/Object
 6    #5 = Utf8               simpleField
 7    #6 = Utf8               I
 8    #7 = Utf8               <init>
 9    #8 = Utf8               ()V
10    #9 = Utf8               Code
11   #10 = Utf8               LineNumberTable
12   #11 = Utf8               LocalVariableTable
13   #12 = Utf8               this
14   #13 = Utf8               SimpleClass
15   #14 = Utf8               SourceFile
16   #15 = Utf8               SimpleClass.java
17   #16 = NameAndType        #7:#8          //  "<init>":()V
18   #17 = NameAndType        #5:#6          //  simpleField:I
19   #18 = Utf8               LSimpleClass;
20   #19 = Utf8               java/lang/Object
java字节码
常量字段(类常量)

带有final标记的常量字段在class文件里会被标记成ACC_FINAL.

比如

1 public class SimpleClass {
2 
3     public final int simpleField = 100;
4 
5 }

字段的描述信息会标记成ACC_FINAL:

1 public static final int simpleField = 100;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_FINAL
4     ConstantValue: int 100

对应的初始化代码并不变:

1 4: aload_0
2 5: bipush        100
3 7: putfield      #2                  // Field simpleField:I
静态变量

带有static修饰符的静态变量则会被标记成ACC_STATIC:

1 public static int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_STATIC

不过在实例的构造方法中却再也找不到对应的初始化代码了。

因为static变量会在类的构造方法中进行初始化,并且它用的是putstatic指令而不是putfiled。

java字节码
1 static {};
2   Signature: ()V
3   flags: ACC_STATIC
4   Code:
5     stack=1, locals=0, args_size=0
6        0: bipush         100
7        2: putstatic      #2                  // Field simpleField:I
8        5: return
java字节码

java字节码