深入Java虚拟机之字节码文件格式

这两天在研究JavaAgent,动力是想弄明白playframework到底是做到热修改的。从目前的少许了解,推测出它利用javaagent在类导入到JVM之前,对字节码进行了修改。然后再配合classloader,实现热修改。

从网上找到的相关资料上来看,如果想理解清楚其原理,必须要了解字节码文件的格式和指令,也就是我们的.java文件编译后生成的.class二进制文件。听起来挺难的,毕竟我们在编程时,只需要面对java文件,从不需要考虑.class,而且打开.class看到的都是无法识别的二进制代码。好在《深入Java虚拟机》这本书详细讲解了字节码,真是雪中送炭。边看边试,发现并没有想像中的那么难以理解。

.class文件中的内容,可以分为两部分,一是结构描述,二是代码指令。这其实跟我们写的java文件是非常相似的,只不过它为了性能考虑,生成了二进制文件。下面以HelloWorld来举例说明。

首先我们定义一个最简单的test/HelloWorld.java:

package test;

public class HelloWorld {
public static void main(String[] args) {
System.out.println(“Hello, World!”);
}
}

编译后,生成一个545字节的HelloWorld.class文件。我们可以使用以下代码,读取其内容并打印出来:

import java.io.FileInputStream;
import org.apache.commons.io.HexDump;
import org.apache.commons.io.IOUtils;

public class ClassReader {

public static void main(String[] args) throws Exception {
String file = “E:/WORKSPACE/TestJava/bin/test/HelloWorld.class”;
FileInputStream input = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(input);
HexDump.dump(bytes, 0, System.out, 0);
}
}

其中HexDump是commons-io提供的一个类,非常实用。该程序将向控制台上打印出以下内容:

00000000 CA FE BA BE 00 00 00 32 00 22 07 00 02 01 00 0F …….2.”……
00000010 74 65 73 74 2F 48 65 6C 6C 6F 57 6F 72 6C 64 07 test/HelloWorld.
00000020 00 04 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F …..java/lang/O
00000030 62 6A 65 63 74 01 00 06 3C 69 6E 69 74 3E 01 00 bject…<init>..
00000040 03 28 29 56 01 00 04 43 6F 64 65 0A 00 03 00 09 .()V…Code…..
00000050 0C 00 05 00 06 01 00 0F 4C 69 6E 65 4E 75 6D 62 ……..LineNumb
00000060 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 erTable…LocalV
00000070 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 ariableTable…t
00000080 68 69 73 01 00 11 4C 74 65 73 74 2F 48 65 6C 6C his…Ltest/Hell
00000090 6F 57 6F 72 6C 64 3B 01 00 04 6D 61 69 6E 01 00 oWorld;…main..
000000A0 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 .([Ljava/lang/St
000000B0 72 69 6E 67 3B 29 56 09 00 11 00 13 07 00 12 01 ring;)V………
000000C0 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 ..java/lang/Syst
000000D0 65 6D 0C 00 14 00 15 01 00 03 6F 75 74 01 00 15 em……..out…
000000E0 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 Ljava/io/PrintSt
000000F0 72 65 61 6D 3B 08 00 17 01 00 0D 48 65 6C 6C 6F ream;……Hello
00000100 2C 20 57 6F 72 6C 64 21 0A 00 19 00 1B 07 00 1A , World!……..
00000110 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 …java/io/Print
00000120 53 74 72 65 61 6D 0C 00 1C 00 1D 01 00 07 70 72 Stream……..pr
00000130 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 61 2F 6C intln…(Ljava/l
00000140 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 01 00 04 ang/String;)V…
00000150 61 72 67 73 01 00 13 5B 4C 6A 61 76 61 2F 6C 61 args…[Ljava/la
00000160 6E 67 2F 53 74 72 69 6E 67 3B 01 00 0A 53 6F 75 ng/String;…Sou
00000170 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57 rceFile…HelloW
00000180 6F 72 6C 64 2E 6A 61 76 61 00 21 00 01 00 03 00 orld.java.!…..
00000190 00 00 00 00 02 00 01 00 05 00 06 00 01 00 07 00 …………….
000001A0 00 00 2F 00 01 00 01 00 00 00 05 2A B7 00 08 B1 ../……..*….
000001B0 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 03 …………….
000001C0 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D …………….
000001D0 00 00 00 09 00 0E 00 0F 00 01 00 07 00 00 00 37 ……………7
000001E0 00 02 00 01 00 00 00 09 B2 00 10 12 16 B6 00 18 …………….
000001F0 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 …………….
00000200 05 00 08 00 06 00 0B 00 00 00 0C 00 01 00 00 00 …………….
00000210 09 00 1E 00 1F 00 00 00 01 00 20 00 00 00 02 00 ………. …..
00000220 21                                              !

上面这些内容,分可为三列

《深入Java虚拟机之字节码文件格式》

每一列有点像行号,它表示当前行的内容是从原字节流中哪一位开始。所以第一行是00000000,表示从头开始。第一行打印了16个字节的内容,所以第二行是00000010。

第二列是字节的16进制值,其取值范围从00-FF,每行16个字节。

第三列是每个字节对应的ASCII码形式,所以数字字母等,可以正确显示出来。超出ASCII码范围的,就用“点”表示了。

看到这一堆数字,是不是很头晕?不用怕,只要知道如何解读,其实不难。它包含了HelloWorld.java中包含的全部信息(如类名,方法名等),以及编译器自己加入的一些内容(如初始化函数,调试信息如行号等)。只不过它使用了利于机器阅读的格式,只要知道了格式定义,就很容易翻译成人读的格式。

下表就是.class文件的格式定义。我们只需要从第一个字节读起,一遍下来,整个.class文件的内容就解析完了。

读取四个字节magic

CA FE BA BE00 00 00 32 00 22 07 00 02 01 00 0F

用于快速判断一个文件是不是java class文件的“魔数”:0xCAFEBABE (它是一个咖啡名称)。如果一个文件不是以它开头,说明绝对不是字节码文件。如果是,还需要再进一步判断后面的内容。

读取两个字节minor_version

CA FE BA BE 00 0000 32 00 22 07 00 02 01 00 0F

表示子版本号,这里是0

读取两个字节major_version

CA FE BA BE 00 00 00 3200 22 07 00 02 01 00 0F …….2.”……

表示主版本号,32表示50,即jdk1.6

读取两个字节constant_pool_count

CA FE BA BE 00 00 00 32 00 2207 00 02 01 00 0F

0022表示34,说明一共定义了33个常量(索引为0的那个未使用)

接着就是33个常量定义

每个常量的第一个字节是一个标签(tag),表示一种类型的常量,每个类型都有自己的格式,长度不定。在jdk1.2中,一共有11种不同类型的常量,现在应该增加了不少新的常量:

常量名                                              标识

CONSTANT_Utf8                                1

CONSTANT_Integer                           3

CONSTANT_Float                              4

CONSTANT_Long                              5

CONSTANT_Double                           6

CONSTANT_Class                             7

CONSTANT_String                             8

CONSTANT_Fieldref                          9

CONSTANT_Methodref                     10

CONSTANT_InterfaceMethodref       11

CONSTANT_NameAndType              12

我们要读取33个常量,分别

读取常量(索引为1)的第一个字节tag

CA FE BA BE 00 00 00 32 00 22 0700 02 01 00 0F

07表示Class_info,其格式定义为:

1字节 tag

2字节 对应的常量池索引

所以它一共有3个字节,我们还需要再读2个字节:

CA FE BA BE 00 00 00 32 00 22 07 00 0201 00 0F

0002即2,表示这个class对应的常量索引为2

读取下一个常量(索引为2)的第一个字节tag

CA FE BA BE 00 00 00 32 00 22 07 00 02 0100 0F

01表示一个UTF8字符串,它的格式定义为:

1字节 tag

2字节 length

n字节 与length长度相等

往下读2个字节00 0F ,表示长度为15,然后再从下一行读取15个字节:

74 65 73 74 2F 48 65 6C 6C 6F 57 6F 72 6C 6407

这个字符串的内容是“test/HelloWorld”,其长度为15。

一直读到第33个常量。

继续读2个字节access_flags

读2个字节this_class

读2个字节super_class

读2个字节interfaces_count

读2个字节interfaces

读2个字节access_flags

====================================================================================================================================

今天把之前在Evernote中的笔记重新整理了一下,发上来供对java class 文件结构的有兴趣的同学参考一下。

学习Java的朋友应该都知道Java从刚开始的时候就打着平台无关性的旗号,说“一次编写,到处运行”,其实说到无关性,Java平台还有另外一个无关 性那就是语言无关性,要实现语言无关性,那么Java体系中的class的文件结构或者说是字节码就显得相当重要了,其实Java从刚开始的时候就有两套 规范,一个是Java语言规范,另外一个是Java虚拟机规范,Java语言规范只是规定了Java语言相关的约束以及规则,而虚拟机规范则才是真正从跨 平台的角度去设计的。今天我们就以一个实际的例子来看看,到底Java中一个Class文件对应的字节码应该是什么样子。 这篇文章将首先总体上阐述一下Class到底由哪些内容构成,然后再用一个实际的Java类入手去分析class的文件结构。

在继续之前,我们首先需要明确如下几点:

1)Class文件是有8个字节为基础的字节流构成的,这些字节流之间都严格按照规定的顺序排列,并且字节之间不存在任何空隙,对于超过8个字节的数据,将按 照Big-Endian的顺序存储的,也就是说高位字节存储在低的地址上面,而低位字节存储到高地址上面,其实这也是class文件要跨平台的关键,因为 PowerPC架构的处理采用Big-Endian的存储顺序,而x86系列的处理器则采用Little-Endian的存储顺序,因此为了Class文 件在各中处理器架构下保持统一的存储顺序,虚拟机规范必须对起进行统一。

2) Class文件结构采用类似C语言的结构体来存储数据的,主要有两类数据项,无符号数和表,无符号数用来表述数字,索引引用以及字符串等,比如 u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数,而表是有多个无符号数以及其它的表组成的复合结构。可能大家看到这里 对无符号数和表到底是上面也不是很清楚,不过不要紧,等下面实例的时候,我会再以实例来解释。

明确了上面的两点以后,我们接下来后来看看Class文件中按照严格的顺序排列的字节流都具体包含些什么数据:

《深入Java虚拟机之字节码文件格式》

(上图来自The Java Virtual Machine Specification Java SE 7 Edition)

在看上图的时候,有一点我们需要注意,比如cp_info,cp_info表示常量池,上图中用 constant_pool[constant_pool_count-1]的方式来表示常量池有constant_pool_count-1个常量,它 这里是采用数组的表现形式,但是大家不要误以为所有的常量池的常量长度都是一样的,其实这个地方只是为了方便描述采用了数组的方式,但是这里并不像编程语 言那里,一个int型的数组,每个int长度都一样。明确了这一点以后,我们在回过头来看看上图中每一项都具体代表了什么含义。

1)u4 magic 表示魔数,并且魔数占用了4个字节,魔数到底是做什么的呢?它其实就是表示一下这个文件的类型是一个Class文件,而不是一张JPG图片,或者AVI的电影。而Class文件对应的魔数是0xCAFEBABE.

2)u2 minor_version 表示Class文件的次版本号,并且此版本号是u2类型的无符号数表示。

3) u2 major_version 表示Class文件的主版本号,并且主版本号是u2类型的无符号数表示。major_version和minor_version主要用来表示当前的虚拟 机是否接受当前这种版本的Class文件。不同版本的Java编译器编译的Class文件对应的版本是不一样的。高版本的虚拟机支持低版本的编译器编译的 Class文件结构。比如Java SE 6.0对应的虚拟机支持Java SE 5.0的编译器编译的Class文件结构,反之则不行。

4) u2 constant_pool_count 表示常量池的数量。这里我们需要重点来说一下常量池是什么东西,请大家不要与Jvm内存模型中的运行时常量池混淆了,Class文件中常量池主要存储了字 面量以及符号引用,其中字面量主要包括字符串,final常量的值或者某个属性的初始值等等,而符号引用主要存储类和接口的全限定名称,字段的名称以及描 述符,方法的名称以及描述符,这里名称可能大家都容易理解,至于描述符的概念,放到下面说字段表以及方法表的时候再说。另外大家都知道Jvm的内存模型中 有堆,栈,方法区,程序计数器构成,而方法区中又存在一块区域叫运行时常量池,运行时常量池中存放的东西其实也就是编译器长生的各种字面量以及符号引用, 只不过运行时常量池具有动态性,它可以在运行的时候向其中增加其它的常量进去,最具代表性的就是String的intern方法。

5)cp_info 表示常量池,这里面就存在了上面说的各种各样的字面量和符号引用。放到常量池的中数据项在The Java Virtual Machine Specification Java SE 7 Edition 中一共有14个常量,每一种常量都是一个表,并且每种常量都用一个公共的部分tag来表示是哪种类型的常量。

下面分别简单描述一下具体细节等到后面的实例 中我们再细化。

  • CONSTANT_Utf8_info      tag标志位为1,   UTF-8编码的字符串
  • CONSTANT_Integer_info  tag标志位为3, 整形字面量
  • CONSTANT_Float_info     tag标志位为4, 浮点型字面量
  • CONSTANT_Long_info     tag标志位为5, 长整形字面量
  • CONSTANT_Double_info  tag标志位为6, 双精度字面量
  • CONSTANT_Class_info    tag标志位为7, 类或接口的符号引用
  • CONSTANT_String_info    tag标志位为8,字符串类型的字面量
  • CONSTANT_Fieldref_info  tag标志位为9,  字段的符号引用
  • CONSTANT_Methodref_info  tag标志位为10,类中方法的符号引用
  • CONSTANT_InterfaceMethodref_info tag标志位为11, 接口中方法的符号引用
  • CONSTANT_NameAndType_info tag 标志位为12,字段和方法的名称以及类型的符号引用

6) u2 access_flags 表示类或者接口的访问信息,具体如下图所示:
《深入Java虚拟机之字节码文件格式》

7)u2 this_class 表示类的常量池索引,指向常量池中CONSTANT_Class_info的常量

8)u2 super_class 表示超类的索引,指向常量池中CONSTANT_Class_info的常量

9)u2 interface_counts 表示接口的数量

10)u2 interface[interface_counts]表示接口表,它里面每一项都指向常量池中CONSTANT_Class_info常量

11)u2 fields_count 表示类的实例变量和类变量的数量

12) field_info fields[fields_count]表示字段表的信息,其中字段表的结构如下图所示:

《深入Java虚拟机之字节码文件格式》

上图中access_flags表示字段的访问表示,比如字段是public,private,protect 等,name_index表示字段名 称,指向常量池中类型是CONSTANT_UTF8_info的常量,descriptor_index表示字段的描述符,它也指向常量池中类型为 CONSTANT_UTF8_info的常量,attributes_count表示字段表中的属性表的数量,而属性表是则是一种用与描述字段,方法以及 类的属性的可扩展的结构,不同版本的Java虚拟机所支持的属性表的数量是不同的。

13) u2 methods_count表示方法表的数量

14)method_info 表示方法表,方法表的具体结构如下图所示:

《深入Java虚拟机之字节码文件格式》
其中access_flags表示方法的访问表示,name_index表示名称的索引,descriptor_index表示方法的描述 符,attributes_count以及attribute_info类似字段表中的属性表,只不过字段表和方法表中属性表中的属性是不同的,比如方法 表中就Code属性,表示方法的代码,而字段表中就没有Code属性。其中具体Class中到底有多少种属性,等到Class文件结构中的属性表的时候再 说说。

15) attribute_count表示属性表的数量,说到属性表,我们需要明确以下几点:

  • 属性表存在于Class文件结构的最后,字段表,方法表以及Code属性中,也就是说属性表中也可以存在属性表
  • 属性表的长度是不固定的,不同的属性,属性表的长度是不同的

上面说完了Class文件结构中每一项的构成以后,我们以一个实际的例子来解释以下上面所说的内容。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.ejushang.TestClass;   public class TestClass implements Super{   private static final int staticVar = 0 ;   private int instanceVar= 0 ;   public int instanceMethod( int param){   return param+ 1 ;   }   }   interface Super{ }

通过jdk1.6.0_37的javac 编译后的TestClass.java对应的TestClass.class的二进制结构如下图所示:

《深入Java虚拟机之字节码文件格式》

下面我们就根据前面所说的Class的文件结构来解析以下上图中字节流。

1)魔数
从Class的文件结构我们知道,刚开始的4个字节是魔数,上图中从地址00000000h-00000003h的内容就是魔数,从上图可知Class的文件的魔数是0xCAFEBABE。

2)主次版本号
接下来的4个字节是主次版本号,有上图可知从00000004h-00000005h对应的是0×0000,因此Class的minor_version 为0×0000,从00000006h-00000007h对应的内容为0×0032,因此Class文件的major_version版本为 0×0032,这正好就是jdk1.6.0不带target参数编译后的Class对应的主次版本。

3)常量池的数量
接下来的2个字节从00000008h-00000009h表示常量池的数量,由上图可以知道其值为0×0018,十进制为24个,但是对于常量池的数量 需要明确一点,常量池的数量是constant_pool_count-1,为什么减一,是因为索引0表示class中的数据项不引用任何常量池中的常 量。

4)常量池
我们上面说了常量池中有不同类型的常量,下面就来看看TestClass.class的第一个常量,我们知道每个常量都有一个u1类型的tag标识来表示 常量的类型,上图中0000000ah处的内容为0x0A,转换成二级制是10,有上面的关于常量类型的描述可知tag为10的常量是Constant_Methodref_info,而Constant_Methodref_info的结够如下图所示:

《深入Java虚拟机之字节码文件格式》

其中class_index指向常量池中类型为CONSTANT_Class_info的常量,从TestClass的二进制文件结构中可以看出 class_index的值为0×0004(地址为0000000bh-0000000ch),也就是说指向第四个常量。

name_and_type_index指向常量池中类型为CONSTANT_NameAndType_info常量。从上图可以看出name_and_type_index的值为0×0013,表示指向常量池中的第19个常量。

接下来又可以通过同样的方法来找到常量池中的所有常量。不过JDK提供了一个方便的工具可以让我们查看常量池中所包含的常量。通过javap -verbose TestClass 即可得到所有常量池中的常量,截图如下:

《深入Java虚拟机之字节码文件格式》

从上图我们可以清楚的看到,TestClass中常量池有24个常量,不要忘记了第0个常量,因为第0个常量被用来表示 Class中的数据项不引用任何常量池中的常量。从上面的分析中我们得知TestClass的第一个常量表示方法,其中class_index指向的第四 个常量为java/lang/Object,name_and_type_index指向的第19个常量值为<init>:()V,从这里可 以看出第一个表示方法的常量表示的是java编译器生成的实例构造器方法。通过同样的方法可以分析常量池的其它常量。OK,分析完常量池,我们接下来再分 析下access_flags。
5)u2 access_flags 表示类或者接口方面的访问信息,比如Class表示的是类还是接口,是否为public,static,final等。具体访问标示的含义之前已经说过 了,下面我们就来看看TestClass的访问标示。Class的访问标示是从0000010dh-0000010e,期值为0×0021,根据前面说的 各种访问标示的标志位,我们可以知道:0×0021=0×0001|0×0020 也即ACC_PUBLIC 和 ACC_SUPER为真,其中ACC_PUBLIC大家好理解,ACC_SUPER是jdk1.2之后编译的类都会带有的标志。

6)u2 this_class 表示类的索引值,用来表示类的全限定名称,类的索引值如下图所示:

《深入Java虚拟机之字节码文件格式》

从上图可以清楚到看到,类索引值为0×0003,对应常量池的第三个常量,通过javap的结果,我们知道第三个常量为 CONSTANT_Class_info类型的常量,通过它可以知道类的全限定名称为:com/ejushang/TestClass /TestClass

7)u2 super_class 表示当前类的父类的索引值,索引值所指向的常量池中类型为CONSTANT_Class_info的常量,父类的索引值如下图所示,其值为0×0004, 查看常量池的第四个常量,可知TestClass的父类的全限定名称为:java/lang/Object

《深入Java虚拟机之字节码文件格式》

8)interfaces_count和  interfaces[interfaces_count]表示接口数量以及具体的每一个接口,TestClass的接口数量以及接口如下图所示,其中 0×0001表示接口数量为1,而0×0005表示接口在常量池的索引值,找到常量池的第五个常量,其类型为CONSTANT_Class_info,其 值为:com/ejushang/TestClass/Super

《深入Java虚拟机之字节码文件格式》

9)fields_count 和 field_info, fields_count表示类中field_info表的数量,而field_info表示类的实例变量和类变量,这里需要注意的是 field_info不包含从父类继承过来的字段,field_info的结构如下图所示:
《深入Java虚拟机之字节码文件格式》

其中access_flags表示字段的访问标示,比如public,private,protected,static,final等,access_flags的取值如下图所示:
《深入Java虚拟机之字节码文件格式》

其中name_index 和 descriptor_index都是常量池的索引值,分别表示字段的名称和字段的描述符,字段的名称容易理解,但是字段的描述符如何理解呢?其实在JVM 规范中,对于字段的描述符规定如下图所示:
《深入Java虚拟机之字节码文件格式》
其中大家需要关注一下上图最后一行,它表示的是对一维数组的描述符,对于String[][]的描述符将是[[ Ljava/lang/String,而对于int[][]的描述符为[[I。接下来的attributes_count以及 attribute_info分别表示属性表的数量以及属性表。下面我们还是以上面的TestClass为例,来看看TestClass的字段表吧。

首先我们来看一下字段的数量,TestClass的字段的数量如下图所示:

《深入Java虚拟机之字节码文件格式》

从上图中可以看出TestClass有两个字段,查看TestClass的源代码可知,确实也只有两个字段,接下来我们看看第一个字段,我们知道第一个字段应该为private int staticVar,它在Class文件中的二进制表示如下图所示:

《深入Java虚拟机之字节码文件格式》
其中0x001A表示访问标示,通过查看access_flags表可知,其为ACC_PRIVATE,ACC_STATIC,ACC_FINAL,接下 来0×0006和0×0007分别表示常量池中第6和第7个常量,通过查看常量池可知,其值分别为:staticVar和I,其中staticVar为字 段名称,而I为字段的描述符,通过上面对描述符的解释,I所描述的是int类型的变量,接下来0×0001表示staticVar这个字段表中的属性表的 数量,从上图可以staticVar字段对应的属性表有1个,0×0008表示常量池中的第8个常量,查看常量池可以得知此属性为 ConstantValue属性,而ConstantValue属性的格式如下图所示:
《深入Java虚拟机之字节码文件格式》

其中attribute_name_index表述属性名的常量池索引,本例中为ConstantValue,而ConstantValue的 attribute_length固定长度为2,而constantValue_index表示常量池中的引用,本例中,其中为0×0009,查看第9个 常量可以知道,它表示一个类型为CONSTANT_Integer_info的常量,其值为0。

上面说完了private static final int staticVar=0,下面我们接着说一下TestClass的private int instanceVar=0,在本例中对instanceVar的二进制表示如下图所示:

《深入Java虚拟机之字节码文件格式》
其中0×0002表示访问标示为ACC_PRIVATE,0x000A表示字段的名称,它指向常量池中的第10个常量,查看常量池可以知道字段名称为 instanceVar,而0×0007表示字段的描述符,它指向常量池中的第7个常量,查看常量池可以知道第7个常量为I,表示类型为 instanceVar的类型为I,最后0×0000表示属性表的数量为0.

10)methods_count 和 method_info ,其中methods_count表示方法的数量,而method_info表示的方法表,其中方法表的结构如下图所示:

《深入Java虚拟机之字节码文件格式》

从上图可以看出method_info和field_info的结构是很类似的,方法表的access_flag的所有标志位以及取值如下图所示:

《深入Java虚拟机之字节码文件格式》

其中name_index和descriptor_index表示的是方法的名称和描述符,他们分别是指向常量池的索引。这里需要结解释一下方法的描述 符,方法的描述符的结构为:(参数列表)返回值,比如public int instanceMethod(int param)的描述符为:(I)I,表示带有一个int类型参数且返回值也为int类型的方法,接下来就是属性数量以及属性表了,方法表和字段表虽然都有 属性数量和属性表,但是他们里面所包含的属性是不同。接下来我们就以TestClass来看一下方法表的二进制表示。首先来看一下方法表数量,截图如下:

《深入Java虚拟机之字节码文件格式》
从上图可以看出方法表的数量为0×0002表示有两个方法,接下来我们来分析第一个方法,我们首先来看一下TestClass的第一个方法的access_flag,name_index,descriptor_index,截图如下:

《深入Java虚拟机之字节码文件格式》
从上图可以知道access_flags为0×0001,从上面对access_flags标志位的描述,可知方法的access_flags的取值为 ACC_PUBLIC,name_index为0x000B,查看常量池中的第11个常量,知道方法的名称为<init>,0x000C表示 descriptor_index表示常量池中的第12常量,其值为()V,表示<init>方法没有参数和返回值,其实这是编译器自动生成 的实例构造器方法。接下来的0×0001表示<init>方法的方法表有1个属性,属性截图如下:
《深入Java虚拟机之字节码文件格式》
从上图可以看出0x000D对应的常量池中的常量为Code,表示的方法的Code属性,所以到这里大家应该明白方法的那些代码是存储在Class文件方法表中的属性表中的Code属性中。接下来我们在分析一下Code属性,Code属性的结构如下图所示:
《深入Java虚拟机之字节码文件格式》

其中attribute_name_index指向常量池中值为Code的常量,attribute_length的长度表示Code属性表的长度(这里 需要注意的时候长度不包括attribute_name_index和attribute_length的6个字节的长度)。

max_stack表示最大栈深度,虚拟机在运行时根据这个值来分配栈帧中操作数的深度,而max_locals代表了局部变量表的存储空间。

max_locals的单位为slot,slot是虚拟机为局部变量分配内存的最小单元,在运行时,对于不超过32位类型的数据类型,比如 byte,char,int等占用1个slot,而double和Long这种64位的数据类型则需要分配2个slot,另外max_locals的值并 不是所有局部变量所需要的内存数量之和,因为slot是可以重用的,当局部变量超过了它的作用域以后,局部变量所占用的slot就会被重用。

code_length代表了字节码指令的数量,而code表示的时候字节码指令,从上图可以知道code的类型为u1,一个u1类型的取值为0×00-0xFF,对应的十进制为0-255,目前虚拟机规范已经定义了200多条指令。

exception_table_length以及exception_table分别代表方法对应的异常信息。

attributes_count和attribute_info分别表示了Code属性中的属性数量和属性表,从这里可以看出Class的文件结构中,属性表是很灵活的,它可以存在于Class文件,方法表,字段表以及Code属性中。

接下来我们继续以上面的例子来分析一下,从上面init方法的Code属性的截图中可以看出,属性表的长度为0×00000026,max_stack的 值为0×0002,max_locals的取值为0×0001,code_length的长度为0x0000000A,那么00000149h- 00000152h为字节码,接下来exception_table_length的长度为0×0000,而attribute_count的值为 0×0001,00000157h-00000158h的值为0x000E,它表示常量池中属性的名称,查看常量池得知第14个常量的值为 LineNumberTable,LineNumberTable用于描述java源代码的行号和字节码行号的对应关系,它不是运行时必需的属性,如果通 过-g:none的编译器参数来取消生成这项信息的话,最大的影响就是异常发生的时候,堆栈中不能显示出出错的行号,调试的时候也不能按照源代码来设置断 点,接下来我们再看一下LineNumberTable的结构如下图所示:

《深入Java虚拟机之字节码文件格式》

其中attribute_name_index上面已经提到过,表示常量池的索引,attribute_length表示属性长度,而start_pc和 line_number分表表示字节码的行号和源代码的行号。本例中LineNumberTable属性的字节流如下图所示:

《深入Java虚拟机之字节码文件格式》

上面分析完了TestClass的第一个方法,通过同样的方式我们可以分析出TestClass的第二个方法,截图如下:

《深入Java虚拟机之字节码文件格式》

其中access_flags为0×0001,name_index为0x000F,descriptor_index为0×0010,通过查看常量池可 以知道此方法为public int instanceMethod(int param)方法。通过和上面类似的方法我们可以知道instanceMethod的Code属性为下图所示:

《深入Java虚拟机之字节码文件格式》

最后我们来分析一下,Class文件的属性,从00000191h-00000199h为Class文件中的属性表,其中0×0011表示属性的名称,查看常量池可以知道属性名称为SourceFile,我们再来看看SourceFile的结构如下图所示:

《深入Java虚拟机之字节码文件格式》

其中attribute_length为属性的长度,sourcefile_index指向常量池中值为源代码文件名称的常量,在本例中SourceFile属性截图如下:

《深入Java虚拟机之字节码文件格式》
其中attribute_length为0×00000002表示长度为2个字节,而soucefile_index的值为0×0012,查看常量池的第18个常量可以知道源代码文件的名称为TestClass.java

最后,希望对技术感兴趣的朋友多交流。个人微博:(http://weibo.com/xmuzyq)

(全文完)

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/dac55300424/article/details/12393849
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞