CLASS文件格式学习

来源:互联网 发布:形容男生的网络词语 编辑:程序博客网 时间:2024/05/01 03:40

写在前面

真知出于实践,最近在学习JAVA虚拟机的我看了半本《深入理解JAVA虚拟机》觉得也没有get到点。只是觉得第6章“类文件结构”还比较容易实践,那就实践下熟络熟络吧。

第一个例子

当然从helloworld开始了。不过既然是学习CLASS文件结构,那就换成hellojavaclass给自己来点互动的赶脚吧。

public class Main {    public static void main(String[] args) {        System.out.println("Hello JAVA Class");    }}

生成的class文件为
这里写图片描述

魔术&版本

头四字节标识了Class文件魔术,用以指导识别Class文件;次四字节标识了JAVA十进制版本号——52.0,也就是JDK 1.8.0
这里写图片描述

常量池

版本号后跟的是常量池。
首先是2字节的常量长度
这里写图片描述
标识了常量池个数为34个。注意常量池从1开始索引,索引0用以标识空引用。
紧接着的1字节07标识了常量类型为CONSTANCT_Class_info类型,而后2字节0002标识了类名称CONSTANCT_Class_info.name_index,指向了第二个常量。第二个常量的常量类型标识01表示该常量为CONSTANT_Utf8_info类型,其后0004标识了该字符串的长度为4字节,而后四字节4D6169标识了该字符串的二进制符,其utf8字符串为”Main”。至此,一个常量的解析完毕 。
借助javap -verbose,整个文件的常量池概览如

Constant pool:   #1 = Class              #2             // Main   #2 = Utf8               Main   #3 = Class              #4             // java/lang/Object   #4 = Utf8               java/lang/Object   #5 = Utf8               <init>   #6 = Utf8               ()V   #7 = Utf8               Code   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V   #9 = NameAndType        #5:#6          // "<init>":()V  #10 = Utf8               LineNumberTable  #11 = Utf8               LocalVariableTable  #12 = Utf8               this  #13 = Utf8               LMain;  #14 = Utf8               main  #15 = Utf8               ([Ljava/lang/String;)V  #16 = Fieldref           #17.#19        // java/lang/System.out:Ljava/io/PrintStream;  #17 = Class              #18            // java/lang/System  #18 = Utf8               java/lang/System  #19 = NameAndType        #20:#21        // out:Ljava/io/PrintStream;  #20 = Utf8               out  #21 = Utf8               Ljava/io/PrintStream;  #22 = String             #23            // Hello JAVA Class  #23 = Utf8               Hello JAVA Class  #24 = Methodref          #25.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V  #25 = Class              #26            // java/io/PrintStream  #26 = Utf8               java/io/PrintStream  #27 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V  #28 = Utf8               println  #29 = Utf8               (Ljava/lang/String;)V  #30 = Utf8               args  #31 = Utf8               [Ljava/lang/String;  #32 = Utf8               SourceFile  #33 = Utf8               Main.java

访问标志

常量池结束后,接2字节代表访问标志。
这里写图片描述
0021标识ACC_PUBLIC|ACC_SUPER,表示接下来描述的类定义为public类型,且允许invokespecial字节码指令的新语义。

类索引、父类索引与接口索引

访问标志后连着两个2字节数据00010003分别表示类索引,父类索引。紧接着的2字节数据为接口索引表容量,如果有索引表后面将继续接索引表。本例中没有索引表,所以接的是0000

字段表集合

类索引后跟的是字段表,首发的是4字节字段表长度0000,本例中表示类Main无字段。

方法表集合

同字段表集合,方法表集合首发的是4字节方法表长度0002,表示方法表中有两个方法
这里写图片描述
两个红框分别代表了两个方法。

第一个方法的结构为

  • accesss_flag: 0001 //表示public类型
  • name_index: 0005 //
  • descripter_index: 0006 //()V 表示void()类型方法
  • attribute_count: 0001 //表示属性表集合有一项属性
  • atrribute_name_index: 0007 //Code 说明此属性是方法的字节码描述
  • atribute_length: 0000002F //Code长度
  • info: …

第二个方法的结构为

  • accesss_flag: 0009 //表示public static类型
  • name_index: 000E //main
  • descripter_index: 000F //([Ljava/lang/String;)V 表示参量类型为String[],返回值为void
  • attribute_count: 0001 //表示属性表集合有一项属性
  • atrribute_name_index: 0007 //Code 说明此属性是方法的字节码描述
  • attribute_length: 00000037 //Code长度
  • info: …

属性表集合

这个是重头戏了。上一节中提及的Code属性中,只给出了Code长度部分,而没有展开info部分,本节将展开。
第一个方法<Init>的属性表信息如
- attribute_name_index: 0007 //属性类型:Code
- attribute_length: 0000002F //属性表长度
- max_stack: 0001 //操作数栈最大长度
- max_locals: 0001 //局部变量表所需存储空间,单位Slot
- code_length: 00000005 //字节码长度
- code: 2AB70008B1 //字节码
- exception_table_length: 0000 //异常属性为空
- attribute_count: 0002 //两个属性

这段字节码代表的含义可用javap -verbose解读得

         0: aload_0         1: invokespecial #8                  // Method java/lang/Object."<init>":()V         4: return

这段代码将this变量压倒操作数栈顶,并以之为参数调用java/lang/Object."<init>":()V方法,最后返回。看样子java是默认给构造了一个构造函数,类似于

    public Main() {        super();    }

这段Code属性的两个额外属性分隔开,如
这里写图片描述
前者为LineNumberTable,后者为LocalVariableTable。
用javap -verbose可以解析到LineNumberTable信息为

      LineNumberTable:        line 1: 0

表示Java源码第1行对于字节码的第0行。
LocalVariableNumberTable信息为

      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   LMain;

表示局部变量this类型为Class Main,作用域为从0开始的字节码,长度为5字节,它在栈中的Slot地址为0。

第二个方法main的属性表信息如
- attribute_name_index: 0007 //属性类型:Code
- attribute_length: 00000037 //属性表长度
- max_stack: 0002 //操作数栈最大长度
- max_locals: 0001 //局部变量表所需存储空间,单位Slot
- code_length: 00000009 //字节码长度
- code: B200101216B60018B1 //字节码
- exception_table_length: 0000 //异常属性为空
- attribute_count: 0002 //两个属性
两个属性分别为

      LineNumberTable:        line 4: 0        line 5: 8
      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       9     0  args   [Ljava/lang/String;

表示代码段第0行对应字节码第8个字节;局部变量args[] java/lang/String类型,并且其作用域为从字节码0开始的9个字节,最后,args局部变量的存储位置位于栈中的Slot0,也就是方法的第一个参数。

题外话

细心的朋友可能留意到,类的索引个数,字段个数,方法个数都是无符号2字节整型。同时,《深入理解JAVA虚拟机》还指出虚拟机对Code字节码长度的限制为65535字节。这样看来,索引个数,字段个数,方法个数的上限是65535,而方法内的代码长度也不能太长以至于超出虚拟机的限制。让我们来试试方法内代码超限的话会发生什么吧。

public class TestMethodCodeLen{    public void test()    {    int a = 1;    int b = 2;    a += b;    b += a;    ...//中间省略65536行a += b;b += a;    System.out.println(a+b);    }}

javac编译,结果显示

TestMethodCodeLen.java:3: 错误: 代码过长    public void test()                ^1 个错误

还真是。

另一个例子

对于刚才的例子,没有继承,没有接口实现,没有类属性,没有方法局部变量——用例不具一般性。
这回来个像模像样的例子

public class Person {    protected int gender;    public String who()    {        return new String("Person");    }}
import java.util.Queue;public class Employee extends Person implements Comparable<Object>{    public int age = 20;    static public String country = "China";    protected Queue<Integer> qi;    @Override    public int compareTo(Object arg0) {        // TODO Auto-generated method stub        return this.age - ((Employee) arg0).age;    }    public String who()    {        return new String("Employee");    }}

好了,这回再看看Employee.class的编译结果
这里写图片描述
其常量池为

Constant pool:   #1 = Class              #2             // Employee   #2 = Utf8               Employee   #3 = Class              #4             // Person   #4 = Utf8               Person   #5 = Class              #6             // java/lang/Comparable   #6 = Utf8               java/lang/Comparable   #7 = Utf8               age   #8 = Utf8               I   #9 = Utf8               country  #10 = Utf8               Ljava/lang/String;  #11 = Utf8               qi  #12 = Utf8               Ljava/util/Queue;  #13 = Utf8               Signature  #14 = Utf8               Ljava/util/Queue<Ljava/lang/Integer;>;  #15 = Utf8               <clinit>  #16 = Utf8               ()V  #17 = Utf8               Code  #18 = String             #19            // China  #19 = Utf8               China  #20 = Fieldref           #1.#21         // Employee.country:Ljava/lang/String;  #21 = NameAndType        #9:#10         // country:Ljava/lang/String;  #22 = Utf8               LineNumberTable  #23 = Utf8               LocalVariableTable  #24 = Utf8               <init>  #25 = Methodref          #3.#26         // Person."<init>":()V  #26 = NameAndType        #24:#16        // "<init>":()V  #27 = Fieldref           #1.#28         // Employee.age:I  #28 = NameAndType        #7:#8          // age:I  #29 = Utf8               this  #30 = Utf8               LEmployee;  #31 = Utf8               compareTo  #32 = Utf8               (Ljava/lang/Object;)I  #33 = Utf8               arg0  #34 = Utf8               Ljava/lang/Object;  #35 = Utf8               who  #36 = Utf8               ()Ljava/lang/String;  #37 = Class              #38            // java/lang/String  #38 = Utf8               java/lang/String  #39 = String             #2             // Employee  #40 = Methodref          #37.#41        // java/lang/String."<init>":(Ljava/lang/String;)V  #41 = NameAndType        #24:#42        // "<init>":(Ljava/lang/String;)V  #42 = Utf8               (Ljava/lang/String;)V  #43 = Utf8               SourceFile  #44 = Utf8               Employee.java  #45 = Utf8               LPerson;Ljava/lang/Comparable<Ljava/lang/Object;>;

这回就不再细细对了,只关注与前一个例子不同之处。

类索引,父类索引和接口集合

地址0x00000226开始的3个4字节分别为
- 0001: 标识类为Employee
- 0003: 标识父类为Person
- 0001: 标识接口个数为1个
而后接的接口集合为
0005指向常量池中的java/lang/Comparable

字段表集合

地址0x0000022e开始的2字节标识了字段表字段个数为0003,即3个。
第一个字段信息为

  • access_flag: 0001//pulic属性
  • name_index: 0007//索引常量池7,表示字段名为age
  • descriptor_index: 0008//索引常量池8,表示int型变量
  • attribute_count: 0000

第二个字段信息为

  • access_flag: 0009//static public 属性
  • name_index: 0009//表示变量名为country
  • descriptor_index: 000A//表示变量类型为Object String
  • attribute_count: 0000

第三个字段信息为

  • access_flag: 0004//protected属性
  • name_index: 000B//表示变量名为qi
  • descriptor_index: 000C//表示变量类型为Object java.util.Queue
  • attribute_count: 0001//表示有一个属性信息

该属性信息为

  • attribute_name_index: 000D//表示属性名称为Signature
  • attribute_length: 00000002//表示属性长度为2字节
  • signature_index: 000E//表示泛型信息为Ljava/util/Queue<Ljava/lang/Integer;>; 也就是
    Object java.util.Queue<Integer>
    看,对于泛型类型,会额外增加字段的泛型签名属性,以标识泛型类型。

方法表集合

地址0x00000250开始的2字节标识了方法个数0004。为什么是4个呢?构造函数,compareTo,who,还有一个编译器生成的static方法用来初始化static变量。
来看下方法概览

  static {};    descriptor: ()V    flags: ACC_STATIC    Code:      stack=1, locals=0, args_size=0         0: ldc           #18                 // String China         2: putstatic     #20                 // Field country:Ljava/lang/String;         5: return      LineNumberTable:        line 5: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature  public Employee();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=2, locals=1, args_size=1         0: aload_0         1: invokespecial #25                 // Method Person."<init>":()V         4: aload_0         5: bipush        20         7: putfield      #27                 // Field age:I        10: return      LineNumberTable:        line 3: 0        line 4: 4        line 3: 10      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      11     0  this   LEmployee;  public int compareTo(java.lang.Object);    descriptor: (Ljava/lang/Object;)I    flags: ACC_PUBLIC    Code:      stack=2, locals=2, args_size=2         0: aload_0         1: getfield      #27                 // Field age:I         4: aload_1         5: checkcast     #1                  // class Employee         8: getfield      #27                 // Field age:I        11: isub        12: ireturn      LineNumberTable:        line 10: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      13     0  this   LEmployee;            0      13     1  arg0   Ljava/lang/Object;  public java.lang.String who();    descriptor: ()Ljava/lang/String;    flags: ACC_PUBLIC    Code:      stack=3, locals=1, args_size=1         0: new           #37                 // class java/lang/String         3: dup         4: ldc           #39                 // String Employee         6: invokespecial #40                 // Method java/lang/String."<init>":(Ljava/lang/String;)V         9: areturn      LineNumberTable:        line 14: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      10     0  this   LEmployee;

可以看到几个好玩的地方。

  • static变量contry在编译器生成的static方法中初始化
  • 对象变量age在构造函数中被初始化。所以私以为代码中将对象属性的初始化放在声明中和放在构造方法中基本没有多大性能差别。(有兴趣的朋友可以看看C++相关编译器是怎么处理对象属性的初始化的,做个对比)
  • 编译器为Employee生成的构造方法先调用了父类的构造方法,再处理本类属性的初始化
  • 在方法who中,编译器对语句return new String('Employee')的处理是产生一个无名String类,初始化为Employee然后返回。所以,编译器在这里并没有为这个类创建中间类名。这种代码风格比之String rst = new String('Employee'); return rst;是效率高的。
0 0
原创粉丝点击