JVM-CLASS文件结构

介绍

介绍Java类文件结构。Java到.class文件的过程:

一、Class文件组成内容

  • 无符号数

    就是数值:1,2,3…

  • 就是一个结构,表的结构类似:

    1
    2
    3
    4
    5
    6
    xxx_XXX_info{
    u1
    u2
    u4
    ...
    }

1. Class文件格式

  • u1:一个字节
  • u2:两个字节
  • u4:四个字节
  • u8:八个字节

演示案例:

1
2
3
4
5
6
7
public class TestM {
private int m;

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

对应的字节码文件:

  1. 魔数

    第一个u4字节代表这是一个class文件:cafe babe(固定的)。

  2. 版本

    第二个u4字节代表JDK编译的版本号。

    • JDK7:51
    • JDK8:52(34是16进制,转换成10进制是52)
  3. 常量池

    • 子面量

      1
      int i = 3;

      字面量就是=右边的东西(3)。

    • 符号引用

      包括:

      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符

    常量池总共有21个常量(HEX:00 16)(22-1)

    常量池结构表-1

    常量池结构表-2

    分析:

    • 0a:===》10 (对应到常量池结构表查找tag值)代表的Methodref_info

      00 06,00 18 ===》#6,#24

    • 09:===》09 代表的Fieldref_info

      03,19 ===》#3,#25

    • 07:===》07 代表的Class_info

      1a ===》#26

    验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    javap -v TestM.class

    Constant pool:
    #1 = Methodref #6.#24 // java/lang/Object."<init>":()V
    #2 = Fieldref #3.#25 // com/example/dodd/jvm/TestM.m:I
    #3 = Class #26 // com/example/dodd/jvm/TestM
    #4 = Methodref #3.#24 // com/example/dodd/jvm/TestM."<init>":()V
    #5 = Methodref #3.#27 // com/example/dodd/jvm/TestM.inc:()I
    #6 = Class #28 // java/lang/Object
    #7 = Utf8 m
    #8 = Utf8 I
    #9 = Utf8 <init>
    #10 = Utf8 ()V
    #11 = Utf8 Code
    #12 = Utf8 LineNumberTable
    #13 = Utf8 LocalVariableTable
    #14 = Utf8 this
    #15 = Utf8 Lcom/example/dodd/jvm/TestM;
    #16 = Utf8 inc
    #17 = Utf8 ()I
    #18 = Utf8 main
    #19 = Utf8 ([Ljava/lang/String;)V
    #20 = Utf8 args
    #21 = Utf8 [Ljava/lang/String;
    #22 = Utf8 SourceFile
    #23 = Utf8 TestM.java
    #24 = NameAndType #9:#10 // "<init>":()V
    #25 = NameAndType #7:#8 // m:I
    #26 = Utf8 com/example/dodd/jvm/TestM
    #27 = NameAndType #16:#17 // inc:()I
    #28 = Utf8 java/lang/Object
    • init和clinit

      init是实例化初始化方法。使用场景:

      • 调用new初始化对象的时候
      • 调用反射的时候newInstance()
      • 调用clone方法的时候
      • ObjectInpustream.getObject()序列化的时候

      clinit是类和接口的初始化。使用场景:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class Test{
      static int m = 3;
      }

      class Test{
      static int m;
      static{
      m = 3;
      }
      }

      所有的类变量初始化语句和静态初始化语句都被java编译收集到一起,采用clinit初始化方式。采用javap命令查看字节码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 类静态变量初始化后出现
      static {};
      descriptor: ()V
      flags: ACC_STATIC
      Code:
      stack=1, locals=0, args_size=0
      0: iconst_2
      1: putstatic #3 // Field a:I
      4: return
      LineNumberTable:
      line 10: 0
    • 解读示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public com.example.dodd.jvm.TestM();
      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 8: 0
      LocalVariableTable:
      Start Length Slot Name Signature
      0 5 0 this Lcom/example/dodd/jvm/TestM;

      这是TestM的默认构造方法,首先通过invokespecial #1找到虚拟机的第一条指令#1。

      • 第一步,到常量池中找#1常量(#1 = Methodref #6.#24)

        #6 指向Class_info的索引项(u2类型数据)

        #26 指向NameAndType的索引项(u2类型数据)

      • 第二步,找#6号常量(#6号常量是Class:java/lang/Object)

      • 第三步,找#24号常量(#9:#10)

        NameAndType的数据结构(u2:名称常量项的索引,u2:描述符常量项的索引)

      • 第四步,#9对应\

      • 第五步,#10对应()V

      至此,搜索的结果就是:V java/lang/Object.\<init>()

    ps:类名应该多长?不能超过256。

  4. 访问标志

    • 字节码:00 21固定

      0x0001|0x0020=0x0021,这就是为什么字节码是00 21

  5. 类索引、父类索引、接口计数器、接口1、接口2…

    字节码:00 03 00 06 00 00

    • 00 03代表#3常量索引:com/example/dodd/jvm/TestM

    • 00 06代表#6常量索引:java/lang/Object

    • 00 00代表有几个接口,当前是0个

    • 如果有接口,后续字节码表示接口的索引项,如果该类没有实现任何接口,则计数器值为0,后面的接口索引表不再占用任何字节。

  6. 字段表集合

    TestClass.class

    关于方法表查找方法示例:

    • 1.找到访问控制access_flag(00 01—>public)
    • 2.找到简单名字name_index(00 17 —> inc)
    • 3.找到描述符descriptor_index(00 18 —>()I)

    翻译过来:public int inc()

    • 4.找到attribute_count(00 01—>代表有一个属性表)
    • 5.对照属性表attribute_info(u2,u4,u1*length)
    • 6.找到attribute_name_index(00 11—>Code)
    • 7.找到attribute_length(00 00 00 be—>190个u1)

    max_stacks:方法的栈深。

    max_locals:方法的本地变量数量。

    args_size:方法的参数有多少个(默认是this(无参也是1),如果方法是static那么就是0)。

    • 8.找到max_stacks最大栈深(00 01—>1 max_stacks)

    • 9.找到max_locals最大变量数(00 05—> 5 max_locals)

    • 10.找到code_length代码行数(00 00 00 18—>24)

    • 11.对应的字节码

      参考JVM指令表

      • 0x04:iconst_1
      • 0x3c:istore_1
      • putstatic #3:b3 xx xx
    • 12.异常表

      1
      2
      3
      from to target
      0 4 8
      index:#2(java/lang/Exception)

      如果没有异常表:

      • debug断点的问题
      • 错误日志没有行号
  7. 方法表集合

  8. 属性表集合

    本地变量表(LocalVariableTable):

    • start+length:一个本地变量的作用域
    • slot:几个槽来存储
    • name:简单名字(变量名对应 )
    • signature:伪泛型,泛型擦除,对泛型的标识