JVM(4) 类文件结构

  一、实现“平台无关性”

  字节码(ByteCode)存储格式和虚拟机是实现语言无关性的基础。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Clas”文件这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。Java中的各种变量、关键字和运算符的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java本身更加强大。

  二、Class类文件的结构

  Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到占用8位字节以上空间的数据项时,会按照“大端序”即高位在前的方式分割成若干个8位字节进行存储。

  Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

  Class文件格式包括:magic(u4)--minor_version(u2)--major_version(u2)--constant_pool_count(u2)--constant_pool(cp_info)--access_flags(u2)--this_class(u2)--super_class(u2)--interfaces_count(u2)--interfaces(u2集合)--fields_count(u2)--fields(field_info)--methods_count(u2)--methods(method_info)--attributes_count(u2)--attributes(attribute_info)

  1.magic(4个字节):每个Class文件的头4个字节称为魔树(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。

  2-3.minor_version(2个字节):次版本号,major_version(2个字节):主版本号

  4-5.constant_pool_count(2个字节):常量池数目。constant_pool:常量池表。常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是Clas文件中第一个出现的表类型数据项目。常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。常量池中每一项常量都是一个表。

  6.access_flags(2个字节):用于识别一些类或者结构层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;是否被申明成final等。

  7-10:this_class(2个字节):类索引。super_class(2个字节):父类索引。interfaces_count(2个字节):接口计数器,表示接口索引表的容量。interfaces(一组u2类型的数据的集合):接口。Class文件中由这4项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名(由于Java不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0),接口索引集合就是用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。

  11-12:fields_count(2个字节):字段计数器。fields:字段表。字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。包括:字段的作用域(public、private、protected修饰符)、实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。字段表中只能表明上述内容是true还是false(例如,ACC_PUBLIC标志用来标明字段是否public),而字段叫什么名字、字段被定义成什么数据类型,都需要从常量池中的常量来描述。字段表包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表计数器(attributes_count)、和属性表(attribute_info)几项。

  13-14:methods_count(2个字节):方法计数器。methods:方法表。方法表和字段表类似,也包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表计数器(attributes_count)、和属性表(attribute_info)几项。

  15-16:attributes:属性表。在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。包括:Code属性、ConstantValue属性、Deprecated属性、Exceptions属性和EnclosingMethod属性等。

  三、字节码指令

  Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

  字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构。由于限制了Java虚拟机操作码的长度为一个字节(即0~255),又由于Class文件是一组以8位字节为基础单位的二进制流,这意味着指令集的操作码总数不可能操作256种;又由于Class文件格式放弃了编译后代码的操作数长度对齐,这就意味着虚拟机处理那些超过一个字节数据的时候,不得不在运行时从字节中重建出具体的数据结构,如果要将一个16位长度的无符号整数使用两个无符号字节存储起来,那么应该是(byte1 << 8) | byte2,这种操作在某种程度上会导致解释字节码时损失一些性能。但是优势是放弃了操作数长度对齐,就意味着可以省略很多填充和间隔符号;用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码。

  Java虚拟机的解释器使用下面的伪代码当做最基本的执行模型:

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

  1.对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。例如:iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。

  2.加载和存储指令

  • 将一个局部变量加载到操作栈(iload)、
  • 将一个数值从操作数栈存储到局部变量表(istore)、
  • 将一个常量加载到操作数栈(bipush)、
  • 扩充局部变量表的访问索引(wide)

  3.运算指令,

  • 加法(iadd)、减法(isub)、乘法(imul)、除法(idiv)、
  • 取余(irem)、取反(ineg)、位移(ishl、ishr、iushr)、
  • 按位或(ior)、按位与(iand)、按位异或(ixor)、
  • 局部变量自增(iinc)、比较(dcmpg、dcmpl、fcmpg、fcmpl、lcmp)

  4.类型转换指令

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

  • int类型到long、float或者double
  • long到float、double
  • float到double

  (2)相对的,处理窄化类型数据时,就必须显式地使用转换指令完成。包括:i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l和d2f

  5.对象创建与访问指令

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

  6.操作数栈管理指令。

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

  7.控制转移指令。

  • 条件分支(ifeq、iflt等)、
  • 复合条件分支(tableswitch、lookupswitch)、
  • 无条件分支(goto、goto_w、jsr、jsr_w、ret)

  8.方法调用和返回指令。

  • invokevirtual用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式
  • invokeinterface用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用
  • invokespecial用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
  • invokestatic用于调用类方法(static方法)
  • invokedynamic用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法

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

  方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn、lreturn、freturn、dretur、areturn和return指令声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

  9.异常处理指令

  • throw语句是由athrow指令来实现,另外还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况是自动抛出,例如idiv会在除数为零时抛出异常。
  • 处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。

  10.同步指令

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

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

  方法内部一段指令序列的同步是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要javac编译器与Java虚拟机两者共同协作支持。

  

  四、公有设计和私有实现

  公有设计是指:Class文件格式以及字节码指令集。这些内容与硬件、操作系统及具体的Java虚拟机实现之间是完全独立的。

  私有实现是指:在满足虚拟机规范的约束下,对具体实现做出修改和优化也是完全可行的,只要优化后的Class文件依然可以被正确读取,并且包含在其中的语义能得到完整的保持,那实现者就可以选择任何方式去实现这些语义,虚拟机后台如何处理Class文件完全是实现者自己的事情。

  虚拟机实现者可以使用这种伸缩性来让Java虚拟机获得更高的性能、更低的内存消耗或者更好的可移植性。

  虚拟机的实现方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
  • 将输入的Java虚拟机代码在加载或执行时翻译成宿主主机CPU的本地指令集(即JIT代码生成技术。)

  

  五、Class文件生成与字节码对照关系

  

  1.首先编写一个TestClass.java文件

package testClass;

public class TestClass {

    private int m;
    
    public int inc() {
        return m + 1;
    }
}

  2.然后,通过javac编译出一个.class文件

C:\Users\BigJun>cd C:\Users\BigJun\eclipse-workspace\JVMTest\src

C:\Users\BigJun\eclipse-workspace\JVMTest\src>javac testClass\TestClass.java

  3.先通过JDK自带的专门用于分析Class文件字节码的工具javap来看一下文件字节码内容:

C:\Users\BigJun\eclipse-workspace\JVMTest\src>javap -verbose testClass\TestClass.class
Classfile /C:/Users/BigJun/eclipse-workspace/JVMTest/src/testClass/TestClass.class
  Last modified 2018-9-19; size 285 bytes
  MD5 checksum 9b9a63aef843afd6d4548faa6a28b39c
  Compiled from "TestClass.java"
public class testClass.TestClass
  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         // testClass/TestClass.m:I
   #3 = Class              #17            // testClass/TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               testClass/TestClass
  #18 = Utf8               java/lang/Object
{
  public testClass.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0
}
SourceFile: "TestClass.java"

  4.然后通过十六进制编辑器UltraEdit打开TestClass.class文件查看文件结构

  

  (1)前面8个字节是固定的:CA FE BA BE

  (2)随后8个字节表示版本号:00 00 00 34对应JDK版本号为52,即JDK1.8

  (3)随后2个字节表示常量池个数:00 13,而16进制的13是10进制的19,因为这里的计数器是从1开始的(0表示不引用任何一个常量池项目),所以有18项常量,对应于通过javap查看到的Constant pool中的18项。紧接着就是常量池了。

    常量池Constant Pool:

    #1:0A是第一个常量,对应十进制的10,根据“常量池的项目类型表”得到0A对应的类型是CONSTANT_Methodref_info,此类型的常量代表着一个类中方法的符号引用,也就是代表一个方法。而CONSTANT_Methodref_info型常量的结构是:

CONSTANT_Methodref_info { 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

  因此,0A表示tag,随后的00 04表示class_index即指向第4个常量所表示的类,再随后的00 0F表示name_and_type_index即指向第15个常量所表示的方法

     #2:09 00 03 00 10,09表示这是一个CONSTANT_Fieldref_info结构,表示字段的符号引用,同理00 03表示指向第3个变量所表示的类或接口,00 10表示指向第16个变量所指向的字段。

                    CONSTANT_Fieldref_info {
                         u1 tag;            // 9
                         u2 class_index;       // 指向常量池中CONSTANT_Class_info常量池项,表示此field字段所在的类
                         u2 name_and_type_index;  // 指向常量池中CONSTANT_NameAndType_info常量池项,表示field字段的名称和数据类型
               }

    #3:07 00 11,07表示这是一个CONSTANT_Class_info结构,表示类或接口的符号引用,00 11表示指向第17个全限定名常量的索引

     CONSTANT_Class_info {
                         u1 tag;
                         u2 name_index;   // 指向了常量池中某一个CONSTANT_Utf8_info类型的常量池项,用来表示这个字段或者方法的名称
               }

    #4:07 00 12,同理也是一个CONSTANT_Class_info结构,00 12表示指向第18个全限定名常量的索引

    #5:01 00 01 6D,01表示这是一个CONSTANT_Utf8_info 结构,其中6D的ASCII表示“m”

                    CONSTANT_Utf8_info {
                                   u1 tag;
                                   u2 length;
                                   u1 bytes[length];
                    }

    #6:01 00 01 49,其中49表示“I”

    #7:01 00 06 3C 69 6E 69 74 3E,表示“<init>”

    #8:01 00 03 28 29 56,表示“()V”

    #9:01 00 04 43 6F 64 65,表示“Code”

    #10:01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65,表示“LineNumberTable”

    #11:01 00 03 69 6E 63,表示“inc”

    #12:01 00 03 28 29 49,表示“()I”

    #13:01 00 0A 53 6F 75 72 63 65 46 69 6C 65,表示“SourceFile”

    #14:01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61,表示“TestClass.java”

    #15:0C 00 07 00 08,0C表示这是一个CONSTANT_NameAndType_info结构,表示字段或方法的部分符号引用。00 07表示指向第7个变量即“<init>”,00 08表示指向第8个变量即“()V”

CONSTANT_NameAndType_info{
    u1 tag;        // 12
    u2 name_index;    // 指向了常量池中某一个CONSTANT_Utf8_info类型的常量池项,用来表示这个字段或者方法的名称
    u2 descriptor_index  // 指向了常量池中某一个CONSTANT_Utf8_info类型的常量池项,用来表示这个字段的数据类型或者是方法的返回类型
}

    #16:0C 00 05 00 06,00 05表示指向第5个变量,即“m”,00 06表示指向第6个变量“I”

    #17:01 00 13 74 65 73 74 43 6C 61 73 73 2F 54 65 74 74 43 6C 61 73 73,表示“testClass/TestClass”

    #18:01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74,表示“java/lang/Object”

  常量池就到这里就结束了,其中的“I”、“V”、“<init>”、“m”、“LineNumberTable”等这部分自动生成的常量的确没有在Java代码里面直接出现过,但它们会被后面的字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,它们用来描述一些不方便使用“固定字节”进行表达的内容。例如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

  (4)访问标志(2个字节):00 21

  

  由于TestClass是一个普通类,只是被public修饰并且使用了JDK1.2之后的编译器进行编译,因此它的ACC_PUBLIC与ACC_SUPER应该为真,其他标识都为假,因此它的Access Flags标志应该为0x0001 | 0x0020 = 0x0021。

  (5)类索引(this_class_name):00 03表示,表示本类的名称是常量池的第3个常量,即“testClass/TestClass”

  (6)父类索引(super_class_name):00 04,表示父类的名称是常量池的第4个常量,即“java/lang/Object”

  (7)接口(interface_count + interfaces_info):00 00,接口数量为0表示没有接口

  (8)字段(filed_count + fields_info):00 01 + 00 02 00 05 00 06 00 00

               field_info {
                         u2 access_flags; 0002
                         u2 name_index; 0005
                         u2 descriptor_index; 0006
                         u2 attributes_count; 0000
                         attribute_info attributes[attributes_count];
               }

  access_flags字段访问标识包括:ACC_PUBLIC(0x0001)/ACC_PRIVATE(0x0002)/ACC_PROTECTED(0x0004)/ACC_STATIC(0x0008)/ACC_FINAL(0x0010)/ACC_VOLATILE(0x0040)/ACC_TRANSIENT(0x0080)/ACC_SYNTHETIC(0x1000)(字段是否由编译器自动产生的)/ACC_ENUM(0x4000)

  name_index代表着字段的简单名称,即“m”

  descriptor_index代表着字段和方法的描述符,其中包括:B(byte类型)、C(char类型)、D(double类型)、F(float类型)、I(int类型)、J(long类型)、S(short类型)、Z(boolean类型)、V(特殊类型void)、L(对象类型,如Ljava/lang/Object)、[(数组类型,例如二维数组java.lang.String[]表示为“[[Ljava/lang/String”,一维整型数组int[]表示为“[I”)

  attributes_count代表着字段后面的属性表,例如,如果“final static int m = 123”,那么就会存在一项名为ConstanValue的属性,其值指向常量123

  字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

  则00 02 00 05 00 06 00 00表示的就是ACC_PRIVATE + "m" + "I" + 没有属性,即可以得到"private int m;"

  (9)方法表(method_count + methods_info): 00 02 + 方法1表 + 方法2表       00 01 00 07 00 08 00 01 00 09 + (Code属性表)

                    method_info {
                         u2 access_flags;  
                         u2 name_index;    
                         u2 descriptor_index;  
                         u2 attributes_count;  
                         attribute_info attributes[attributes_count]; 
                    }

  00 02 表示有2个方法

  • 方法1:方法表+属性表:00 01 00 07 00 08 00 01 00 09 + (Code属性表)

  00 01表示只有ACC_PUBLIC为真

  0007表示名称索引为“<init>”,

  0008表示描述符为“()V”,

  0001表示此方法的属性表集合中仅有一项属性,接下来是属性表

  0009表示属性名称为“Code”,即接下来是Code属性表

  access_flags方法的访问标识对比与字段的访问标识来说,由于voliatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_PUBLIC和ACC_TRANSIENT标志。但是,synchronized、native、strictfp和abstract关键字可以修饰方法,因此方法表的所有标志位及其取值为:

  

   Code属性表的结构为:

  

  对照字节码来看:00 09 + 00 00 00 1D + 00 01+00 01 + 00 00 00 05 + 2A B7 00 01 B1 + 00 00 + 00 01 + 一个属性表

  00 09表示这是一个Code属性表

  00 00 00 1D表示属性长度为29

  00 01表示max_stack,即操作数栈的深度的最大值。在方法执行任务的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。

  00 01表示max_locals,即局部变量表所需的存储空间,单位是Slot。并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals,而是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用。

  00 00 00 05表示Java源程序编译后生成的字节码指令的长度为5,由于每个指令就是一个u1类型的单字节,说明接下来的5个字节都是字节码指令。所以当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。Code属性时Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的代码)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,其他的所有数据项目都用于描述元数据。

  字节码2A:对应的指令为aload_0,表示将第0个Slot中位reference类型的本地变量推送到操作数栈的栈顶。

  字节码B7 00 01:对应的指令为invokespecial,作用是以栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造方法、private方法或者它的父类方法,u2类型的参数说明具体调用哪一个方法。即具体调用的是“java/lang/Object."<init>":()V”引用。

  字节码B1:对应的指令为return,含义是返回此方法,并且返回值为void,表示当前方法结束。

  00 00 表示异常表的数量为0

  00 01表示该方法只有一个属性表,接下来是属性表:00 0A + 00 00 00 06 + 00 01 + 1个line_number_table

  00 0A表示这是一个LineNumberTable属性表,这个属性用于描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系。如果选择不生成此属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试的时候,也无法按照源码行来设置断点。

  

  line_number_info表包括了start_pc和line_numberl两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。因此接下来的00 00 + 00 03

  即字节码的第0行对应Java源文件的第3行,也就是:“public class TestClass {”

  到这里,第一个方法“<init>”方法才算是分析完成。通过Javap指令查看的内容和分析的结果完全一致。

  public testClass.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
  • 方法2:同理,方法2对应的字节码“00 01 00 0B 00 0C 00 01 00 09 00 00 00 1F 00 02 00 01 00 00 00 07 2A B4 00 02 04 60 AC 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 08”翻译过来,也就是第二个方法inc()的描述
  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 8: 0

  需要注意的是,在Javap查看的时候,两个方法的Code属性的第一行是:

    Code:
      stack=1, locals=1, args_size=1

    Code:
      stack=2, locals=1, args_size=

  除了stack属性,locals和args_size都是1,但是无论在参数列表里还是方法体内,都没有定义任何局部变量,locals为什么是1。同理这个类的两个方法--实例构造器<init>()和inc()都是没有参数的,为什么args_size还会是1。这是因为:在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制的实现仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算,这个处理只对实例方法有效,如果inc()方法声明为static,那么args_size就不会等于1而是0了。

  (10)属性表(attribute_count + attributes_info): 00 01 +  00 0D  00 00 00 02  00 0E

  00 01表示只有一个全局属性,00 0D表示属性名称为“SourceFile”,00 00 00 02表示属性长度为2,00 0E表示源文件名称的属性值为“"TestClass.java"”

  

 

  

 

posted @ 2018-09-09 10:55  BigJunOba  阅读(319)  评论(0编辑  收藏  举报