Class文件结构

一、引言

众所周知,Java语言有一个很重要的特点是平台无关性,即用Java语言编写的程序可以在不同平台之间无缝迁移,Java对这个特性有一个著名的宣传口号:“一次编写,到处运行(Write Once,Run AnyWhere)”。Java能够实现平台无关性的原因是它在平台之上提供了一个Java运行环境,也就是JVM,Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码文件——Class文件。
其他语言是否可以使用java虚拟机作为产品交付媒介执行呢?答案是可以,例如Scala、JRuby、Groovy、Clojure等。
在这里插入图片描述

二、准备工作

因为下面的讲解使用Notepad++ 工具,分析class文件的16进制视图,需要用到Hex Editor插件,所以需要准备这Notepad++和Hex Editor插件。
安装过程可以参考:https://blog.csdn.net/Eric_Blog_CSDN/article/details/78904679
我采用的是方式1:
1.在notepad++官网下载7.6版本即可(https://notepad-plus-plus.org/download/v7.6.2.html)
2.然后在安装的notepad++ plugins目录中放入HexEditor插件,该版本需要新建一个文件夹,命名为HexEditor,插件放在这个目录下,在下面博客中提供了可用的插件https://download.csdn.net/download/mr_oldcold/9673604
在这里插入图片描述
在这里插入图片描述
打开Notepad++ ,插件,可以看到Hex-Editor插件已经可以使用
在这里插入图片描述
打开一个class文件,使用view in HEX,即16机制视图,如下:
在这里插入图片描述
在测试的过程中发现notepad编辑器安装有问题,有些class文件正确,魔数为cafebabe,有些不是,重新安装了sublime编辑器,正确

三、 Class文件的组成

在这里插入图片描述
class文件包含虚拟机指令、符号表、其他辅助信息三种内容,这三种内容都是由无符号数或表这两种数据结构组成。

四、Class文件格式

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。
在这里插入图片描述
表中,红框部分是告诉你文件是按照怎样的顺序和方式排列的,比如,最开始是魔数、然后是次版本号、主版本号、常量池计算器…(上表中实际给出的是每个部分的数据类型)
表中,红框下面的部分,是对红框中每一个部分的解释。

五、参数说明

class文件结构主要包含两种数据类型,其中u开头表示无符号数,无符号数就是数值
字段类型:u1、u2、u4、u8
u1:代表占用一个字节的无符号数
u2:代表占用两个字节的无符号数
u4:代表占用四个字节的无符号数
u8:代表占用八个字节的无符号数
另外一些为xxx_info,它是一种表结构,里面又包含了若干的无符号数和表结构。

1. magic

即魔数,u4类型,每个class文件开头4个字节。它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gir或者jpeg等在文件头中都存在魔数。文件格式的制定者可以自由地选择魔数值,只要这个值还没有被广泛采用过同时又不会引起混淆即可。Class文件的魔数富有“浪漫气息”,值为0xCAFEBABE(咖啡宝贝),预示着Java语言的logo,是固定的。

2. minor_version (次版本号)和major_version(主版本号)

第5和第6个字节是次版本号,第7和第8个字节是主版本号,表示编译器使用的JDK的副版本和主版本,高版本编译的class文件可以向下兼容低版本,即高版本JDK可以运行低版本的class,而低版本的JDK不能运行高版本的class。
在这里插入图片描述
示例:(`可以在Notepad++编辑器中安装HEX-Editor 插件,查看一个class文件的十六进制视图,如下

在这里插入图片描述
次版本: 00 00 转换为十进制为0
主版本: 00 33 转换为十进制为51——是JDK 1.7
主版本: 00 34 转换为十进制为52——是JDK 1.8

3. constant_pool_count

表示常量池计数器,表示常量池中有多少个常量,u2类型,第9和第10字节,使用2个字节,但这里有个比较特殊的地方,假如它的值为100,则常量池的常量数为99个。因为常量池的计数是从1开始的。
如上面截图中,00 16,转换为十进制为22,则常量个数为22-1=21(减1是jdk规范规定)

4. constant_pool

表示常量池的数据集合,常量池中每个常量都是一个表结构表示。
常量池主要存放两大类常量:字面量、符号引用
字面量:接近java语言层面的常量概念,如字符串、声明为final的常量值等,通俗的讲: int m=3;(字面量就是等号右边的东西)
在这里插入图片描述
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
目前有21中常量类型,每种常量都是表类型的数据项。主要包括:utf-8字符串、整型、浮点型、长整型、双精度浮点型、类或接口引用、字符串类型、字段引用、类方法引用、接口方法引用、名称和类型引用。见下表
在这里插入图片描述
这些常量类型各自均有自己的结构,均为表结构,表结构中,第一个元素都是u1类型的,标志该常量的类型,代表当前常量属于哪种常量类型,如是CONSTANT_Utf8_info还是CONSTANT_Interger_info类型等。
结构各不相同,如下表格所示:
在这里插入图片描述
4.1 示例
我们将一段简单Java程序使用JDK1.7编译后的Class文件为例进行说明


package com.april.test;

public class Demo {
    private int num = 1;

    public int add() {
        num = num + 2;
        return num;
    }
}

用十六进制视图打开class文件
在这里插入图片描述
可以看到前8个字节是该class文件的魔数和版本号,紧接着的一个十六进制数0x0013,即十进制的19,这就代表这个class文件的常量池中有18项常量,索引值为1~18。
然后就是第一个常量了,上面说过,每种类型的常量开始的第一位都是一个u1类型的标志位,代表该常量的类型,这里是0x0a,十进制的10,查上面的表可知是CONSTANT_Methodref_info类型的常量,说明这个常量是类中方法的符号引用。
接下来我们看下CONSTANT_Methodref_info类型常量的具体结构
在这里插入图片描述
tag是标志位,u1类型,值为10,标记该常量的类型
index是索引值,它指向常量池中其他常量。这里的第一个index,为u2类型,占2字节,值为0x0004,即指向常量池中的第4个常量,第二个index为u2类型,占2字节,值是0x000f,即指向常量池中的第15个常量。

我们再来看下第二个常量
其标志位的值为0x09,即9,查上面的表格可知,其对应的项目类型为CONSTANT_Fieldref_info,即字段的符号引用。其结构为:
在这里插入图片描述
同样也是4个字节,前后都是两个索引。分别指向第4项的索引和第10项的索引。
这里只介绍了几个常量,其他的常量大同小异,就不再一一介绍了。
实际上,我们可以用javap -v命令查看class文件的字节码内容,就可以一目了然,但是,通过我们手动去分析才知道这个结果是怎么出来的,要知其然知其所以然嘛~。
(或javap -verbose Demo.class命令)

E:\test\test\src\test>javap -v Demo.class
Classfile /E:/test/test/src/test/Demo.class
  Last modified 2019-1-12; size 293 bytes
  MD5 checksum f633b15c7b691d6e3641b0ab4c477665
  Compiled from "Demo.java"
public class test.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // test/Demo.num:I
   #3 = Class              #17            // test/Demo
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               num
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               add
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               Demo.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // num:I
  #17 = Utf8               test/Demo
  #18 = Utf8               java/lang/Object
{
  public test.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field num:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: aload_0
         2: getfield      #2                  // Field num:I
         5: iconst_2
         6: iadd
         7: putfield      #2                  // Field num:I
        10: aload_0
        11: getfield      #2                  // Field num:I
        14: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 10
}
SourceFile: "Demo.java"

可以看到和我们分析的一致,该class文件中确实有18项常量,从#1到#18,第一项常量是MethodRef类型的常量,也就是CONSTANT_Methodref_info类型。该常量中有两个index值,第一个index指向常量池中的第4个常量,可以看到第4个常量是Class类型(即CONSTANT_Class_info)。
而CONSTANT_Class_info类型的常量结构如下
在这里插入图片描述
它包含一个index,这个index也是指向常量池中的其他常量,从上图可以看到这里指向的是第18个常量,第18个常量的类型是utf8(即CONSTANT_Utf8_info),class 文件中tag后面的length个字节就是该常量的值,CONSTANT_Utf8_info类型的常量结构如下
在这里插入图片描述
该常量存储的是UTF-8编码的字符串,从图中可以看到,这里的字符串值为“java/lang/Object.”。至此,我们终于翻译出了该class文件中第一个常量的第一个index代表的真正内容。
第一个常量中的第二个索引为0x0f,十进制为15,纸箱第15个常量,该常量为NameAndType_info,即CONSTANT_NameAndType_info常量类型,表示字段或函数的名字和类型(描述符)其结构为:
在这里插入图片描述
也包含两个索引项,值分别为指向常量7和8
常量7为CONSTANT_Utf8_info常量类型:
在这里插入图片描述
该常量存储的是UTF-8编码的字符串,这里的字符串值为“”
常量8为CONSTANT_Utf8_info常量类型,这里的字符串值为“<> v ”
得出的第一个常量就是:“java/lang/Object. ()v ”
()v表示参数和返回值
会被解释成:“ v java/lang/Object. () ”,V即为返回值,V代表void
为什么会调用Object的初始化方法,是因为其是所有类的父类
下面介绍一下init
在这里插入图片描述
init和clinit就是初始化的入口
没有定义构造器,java编译时会添加默认构造器

5. access_flags

表示访问标识,u2类型,占2个字节,用于标识类或接口的访问信息,例如是否为public类型、是否被声明为final、是否是一个接口、是否为abstract类型、是否为注解、是否为枚举等等。
在这里插入图片描述
示例中:
在这里插入图片描述
访问标识符为0x0021
但是在表中并没有找到这个数,实际是由两个16机制数按位或得到的: 0x0001 | 0x0020=0x0021 即是public class

6. this_class

类索引,U2类型,表示此类的全限定名(全限定名的格式:例如com.test.MyTomcat”类的全限定名为”com/test/MyTomcat;”)。
既然是索引那就表示在常量池中的位置编号
示例中,类索引为0x0003,十进制为3,即为常量池中的第三个常量,为Class,即CONSTANT_Class_info常量类型 :
在这里插入图片描述
该CONSTANT_Class_info 常量的索引值为17,17为CONSTANT_Utf8_info常量类型,其值为:
#3 = Class #17 // com/april/test/Demo

7. super_class

父类索引,u2类型,表示此类的父类的全限定名。
示例中,父类索引值0x0004,十进制为4,即为常量池中的第4个常量,同上面的分析 方法,得到结果:
#4 = Class #18 // java/lang/Object

8. interfaces_count

接口索引计数器,u2类型,表示此类实现接口的计数器,如果为3,则表示实现了3个接口。
示例中,为0x0000,十进制为0,即没有实现接口

9. interfaces

接口索引集合,表示类实现的接口集,每个接口U2类型,它的每个值都必须是常量池里的有效引用。
如果没有实现接口,则接口索引计算器后面没有索引集合,即后面直接接下面的fields count,不占用任何字节。

10. fields_count

表示字段计数器,u2类型。用来记录类或者接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量, 不包括从父类继承的字段。
示例中,值为0x0001,十进制为1,即只要一个变量(m)
在这里插入图片描述

11. fields

表示类的字段集,这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量,不包括从父类继承的字段。此外要说明的是, 编译器可能会自动生成字段, 也就是说, class文件中的字段的数量可能多于源文件中定义的字段的数量。 举例来说, 编译器会为内部类增加一个字段, 这个字段是指向外围类的对象的引用。
Java中描述一个字段可以包括的信息有:字段的作用域(public、protectded、private修饰符)、实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、字段的数据类型、字段名称。
每个字段由一个表结构表示:
在这里插入图片描述
access_flags代表字段修饰符,如private、public等,字段的访问标志有如下这些:
在这里插入图片描述
name_index两个字节, 这是一个指向常量池的索引, 它描述的是当前字段的字段名。 这个索引指向常量池中的一个CONSTANT_Utf8_info数据项。 这个CONSTANT_Utf8_info数据项中存放的字符串就是当前字段的字段名。

descriptor_index两个字节, 这是一个指向常量池的索引,代表字段描述符。而字段的描述符则是用来描述字段的数据类型,方法的描述符是返回类型和参数
描述符
我们知道在一个类中可以有若干字段和方法, 这些字段和方法在源文件中如何表述, 我们再熟悉不过了。 既然现在我们要学习class文件格式, 那么我们就要问, 一个字段或一个方法在class文件中是如何表述的? 在本文中, 我们会讨论方法和字段在class文件中的描述。 方法和字段的描述符并不会把方法和字段的所有信息全都描述出来, 毕竟描述符只是一个简单的字符串。

在讲解描述符之前, 要先说明一个问题, 那就是所有的类型在描述符中都有对应的字符或字符串来对应。

根据描述符规则,基本数据类型以及void类型(修饰方法返回值,后面会介绍)都用一个大写字符表示,而对象类型用字符L加对象的全限定名来表示。详见下表
在这里插入图片描述
对于数组类型,每一维度将使用一个前置的“[”字符描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为“[[Ljava/lang/String;”,一个整型数组将被记录为“[I",而String[] s的类型则为[Ljava/lang/String。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类的实例字段。
descriptor_index 下面是attributes_count和attributes 。 这是对当前字段所具有的属性的描述。 这里的属性和源文件中的属性不是同一个概念, 在源文件测层面中, 属性是字段的另一种叫法, 希望读者不要疑惑。读者也不要轻视class文件中的属性, 这些属性可以描述很多的额外信息。 我们会在后面的文章中进行介·绍。

attributes_count表示这个字段有几个属性。
attributes 是一个属性表集合, 每一个属性都是一个attribute_info 表类型。可以出现在filed_info中的属性有三种, 分别是ConstantValue, Deprecated, 和 Synthetic。 这些属性会在后面的文章中进行介绍。

示例解读

只有一个字段, private int num = 1;
由于只有一个字段,还是比较简单的,直接看demo字节码中的值:
在这里插入图片描述
访问标志的值为0x0002,查询上面字段访问标志的表格,可得字段为private;
字段名索引的值为0x0005,查询常量池中的第5项,可得:

#5 = Utf8 num
描述符索引的值为0x0006,查询常量池中的第6项,可得:

#6 = Utf8 I
属性计数器的值为0x0000,即没有任何的属性。
解释过来就是: private int num
注意事项
字段表集合中不会列出从父类或者父接口中继承而来的字段。
内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的.

12. methods_count

表示方法计数器,u2类型,表示类或接口中的方法数目。
示例2中为0x0002,十进制为2,即有两个方法,一个是默认构造器,一个是add()

13. methods

表示类的方法集,每个方法是一个表结构描述,包括:访问标识、方法名引用、方法描述符引用、属性计数器、属性表结构集等等。
方法表用来描述类或接口中的方法。它的结构和字段表一致,依次包括了访问标志(access_flag)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,见下表。
方法表的结构实际跟字段表是一样的,方法表结构如下:
在这里插入图片描述
需要注意的是与字段描述符不同,方法描述符需要描述方法的参数列表和返回值,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。返回值的描述规则与描述字段的数据类型一样。举个例子,
方法void inc()的描述符为“()V”,
方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,
方法”String get(long id,String name)”的描述符为”(J,Ljava/lang/String;)Ljava/lang/String;”,
方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII)I”。
跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:
在这里插入图片描述
示例解读
public int add() {
num = num + 2;
return num;
}

只有一个自定义的方法。但是上面方法表计数器明明是2个,这是为啥呢?
这是因为它包含了默认的构造方法,我们来看下下面的分析就懂了,先看下Demo字节码中的值:
在这里插入图片描述
这是第一个方法表,我们来解读一下这里面的16进制:
访问标志的值为0x0001,查询上面字段访问标志的表格,可得为public;

方法名索引的值为0x0007,查询常量池中的第7项,可得:

#7 = Utf8
这个名为的方法实际上就是默认的构造方法了。

描述符索引的值为0x0008,查询常量池中的第8项,可得:

#8 = Utf8 ()V

属性计数器的值为0x0001,即这个方法表有一个属性。
属性计数器后面就是属性表了,由于只有一个属性,所以这里也只有一个属性表。
由于涉及到属性表,这里简单说下,下一节会详细介绍。
属性表的前两个字节是属性名称索引,这里的值为0x0009,查下常量池中的第9项:

#9 = Utf8 Code
即这是一个Code属性,我们方法里面的代码就是存放在这个Code属性里面。相关细节暂且不表。下一节会详细介绍Code属性。

先跳过属性表,我们再来看下第二个方法:
在这里插入图片描述
访问标志的值为0x0001,查询上面字段访问标志的表格,可得为public;

方法名索引的值为0x000b,查询常量池中的第11项,可得:

#11 = Utf8 add
描述符索引的值为0x000c,查询常量池中的第12项,可得:
#12 = Utf8 ()I
属性计数器的值为0x0001,即这个方法表有一个属性。
属性名称索引的值同样也是0x0009,即这是一个Code属性。
可以看到,第二个方法表就是我们自定义的add()方法了。
注意事项
如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。
编译器可能会自动添加方法,最典型的便是类构造方法(静态构造方法)方法和默认实例构造方法方法。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

14. attributes_count

表示class的属性计数器。属性不仅在前面的字段集合和方法集合部分有,在class文件中也有,指描述字段或方法或class文件的额外信息。
此处的计算器记录的是描述class文件额外信息的属性个数。
示例中,为0x0000,即无属性描述class文件。

15. attributes

表示class的属性,属性是一个表结构集不要求严格的顺序,只要属性名称不与存在的属性名重复即可。
本文示例中class文件的属性计数为0,无属性。

虚拟机规范中规定了9个属性,除此之外还可以自己定义一些属性并自己实现编译器把属性添加到class文件中,JVM运行时会把不能识别的属性忽略,而不会影响运行。
下面主要介绍一下字段集合、方法集合、class文件的属性
15.1. 属性类型
属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,下面这些是虚拟机中预定义的属性:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
15.2 属性表结构
属性表的结构比较灵活,各种不同的属性只要满足以下结构即可:
在这里插入图片描述
即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义。
attribute_length是指后面自定义的属性长度,即attribute_length个字节,不包括属性名索引和属性长度这两个所占的长度,因为这哥俩占的长度都是固定6个字节。

15.3 部分属性详解
下面针对部分常见的一些属性进行详解

15.3.1 Code属性
前面我们看到的属性表都是Code属性,我们这里重点来看下。
Code属性就是存放方法体里面的代码,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。
先来看下Code属性表的结构,如下图:
在这里插入图片描述
可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。
Code属性解读
同样,解读Code属性只需按照上面的表格逐一解读即可。
我们先来看下第一个方法表中的Code属性:
在这里插入图片描述
属性名索引的值为0x0009,上面也说过了,这是一个Code属性;
属性长度的值为0x00000026,即长度为38,注意,这里的长度是指后面自定义的属性长度,不包括属性名索引和属性长度这两个所占的长度,因为这哥俩占的长度都是固定6个字节了,所以往后38个字节都是Code属性的内容
max_stack的值为0x0002,即操作数栈深度的最大值为2;
max_locals在栈帧中需要分配的局部表量表的大小。注意, 这个数字并不是局部变量的个数, 因为根据局部变量的作用域不同, 在执行到一个局部变量以外时, 下一个局部变量可以重用上一个局部变量的空间(每个局部变量在局部变量表中占用一个或两个Slot)。 方法中的局部变量包括方法的参数, 方法的默认参数this, 方法体中定义的变量, catch语句中的异常对象。
值为0x0001;max_locals的单位是Slot(槽),Slot是虚拟机为局部变量分配内存所使用的最小单位。
code_length表示方法中所有指令所占用的字节,u4类型,值为0x00000000a,即字节码10字节;
指令需要对照jvm的指令表翻译
本来中的指令:0x2a b7 00 01 2a 04 b5 00 02b1,这里的值就代表一系列的字节码指令。一个字节代表一个指令,但是一个指令可能有参数也可能没参数,或可能有赋值操作等,则这些信息需要在该字节后面体现,如果有参数,则其后面字节码就是他的参数,一个参数2个字节(如上面的00 01和00 02,其是索引值),参数后面才是下一条指令;如果没参数,后面的字节码就是下一条指令。
这里我们来解读一下这些指令,文末最后的附录附有Java虚拟机字节码指令表,可以通过指令表来查询指令的含义。

2a 指令,查表可得指令为aload_0,其含义为:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
b7 指令,查表可得指令为invokespecial,其含义为:将操作数栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造器方法、private方法或者它的父类的方法。其后面紧跟着的2个字节即指向其具体要调用的方法。
`00 01`,指向常量池中的第1项,查询上面的常量池可得:#1 = Methodref #4.#15 // java/lang/Object."<init>":()V 。即这是要调用默认构造方法<init>。
2a 指令,同第1个。
04 指令,查表可得指令为iconst_1,其含义为:将int型常量值1推送至栈顶。
b5 指令,查表可得指令为putfield,其含义为:为指定的类的实例域赋值。其后的2个字节为要赋值的实例。
`00 02`,指向常量池中的第2项,查询上面的常量池可得:#2 = Fieldref #3.#16 // com/april/test/Demo.num:I。即这里要将num这个字段赋值为1。
b5 指令,查表可得指令为return,其含义为:返回此方法,并且返回值为void。这条指令执行完后,当前的方法也就结束了。

所以,上面的指令简单点来说就是,调用默认的构造方法,并初始化num的值为1。
同时,可以看到,这些操作都是基于栈来完成的。

如果要逐字逐字的去查每一个指令的意思,那是相当的麻烦,大概要查到猴年马月吧。实际上,只要一行命令,就能将这样字节码转化为指令了,还是javap命令哈:
javap -verbose Demo.class

public com.april.test.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field num:I
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4

在这里插入图片描述
看看,那是相当的简单。关于字节码指令,就到此为止了。继续往下看。

exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也就没有了;

attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表,后面就是这个其他属性的属性表了;
所有的属性都遵循属性表的结构,同样,这里的结构也不例外。
前两个字节为属性名索引,其值为0x000a,查看常量池中的第10项:

#10 = Utf8 LineNumberTable
即这是一个LineNumberTable属性。LineNumberTable属性先跳过,具体可以看下一小节。
再来看下第二个方法表中的的Code属性:
在这里插入图片描述
属性名索引的值同样为0x0009,所以,这也是一个Code属性;
属性长度的值为0x0000002b,即长度为43;
max_stack的值为0x0003,即操作数栈深度的最大值为3;
max_locals的值为0x0001,即局部变量表所需的存储空间为1;
code_length的值为0x00000000f,即字节码指令15字节;
code指令的值为0x2a 2a b4 20 02 05 60 b5 20 02 2a b4 20 02 ac,使用javap命令,可得:

 public int add();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: aload_0
         2: getfield      #2                  // Field num:I
         5: iconst_2
         6: iadd
         7: putfield      #2                  // Field num:I
        10: aload_0
        11: getfield      #2                  // Field num:I
        14: ireturn
      LineNumberTable:
        line 11: 0
        line 12: 10

可以看到,这就是我们自定义的add()方法;
exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也没有;
attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
属性名索引值为0x000a,即这同样也是一个LineNumberTable属性,LineNumberTable属性看下一小节。

15.3.2 LineNumberTable属性
LineNumberTable属性是用来描述Java源码行号与字节码行号之间的对应关系。
LineNumberTable属性表结构
在这里插入图片描述
line_number_info(行号表),其长度为4个字节,前两个为start_pc,即字节码行号;后两个为line_number,即Java源代码行号。

前面出现了两个LineNumberTable属性,先看第一个:
在这里插入图片描述
attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
属性名索引值为0x000a,查看常量池中的第10项:

#10 = Utf8 LineNumberTable
1
即这是一个LineNumberTable属性。
attribute_length的值为0x00 00 00 0a,即其长度为10,后面10个字节的都是LineNumberTable属性的内容;
line_number_table_length的值为0x0002,即其行号表长度长度为2,即有两个行号表,即有两行;
第一个行号表其值为0x00 00 00 07,即字节码第0行对应Java源码第7行;
第二个行号表其值为0x00 04 00 08,即字节码第4行对应Java源码第8行。
同样,使用javap命令也能看到:

LineNumberTable:
line 7: 0
line 8: 4

第二个LineNumberTable属性为:
在这里插入图片描述
这里就不逐一看了,同样使用javap命令可得:

  LineNumberTable:
    line 11: 0
    line 12: 10

所以这些行号是有什么用呢?当程序抛出异常时,我们就可以看到报错的行号了,这利于我们debug;使用断点时,也是根据源码的行号来设置的。

参考文献:
https://blog.csdn.net/IT_GJW/article/details/80447947
https://blog.csdn.net/u011810352/article/details/80316870
https://blog.csdn.net/zhangjg_blog/article/details/22091529

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值