【深入理解JVM虚拟机】第6章 类文件结构

6.1 概述

现在越来越多的程序语言选择了与操作系统无关和机器指令无关的、平台中立的格式作为程序编译后的存储格式。

6.2 无关性的基石

Java虚拟机提供的语言无关性.png | left | 523x300

6.3 Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

按照Java虚拟机规范,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种结构中只有两种数据类型:无符号数和表。

  • __无符号数__属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值。
  • __表__是由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯性地以"_info"结尾。表用于描述有层次关系的复合数据结构的数据,整个Class文件本质上就是一张表,它由如下所示的数据项构成:
类型名称解释数量
u4magic魔数1
u2minor_version次版本号1
u2major_version主版本号1
u2constant_pool_count常量池常量个数1
cp_infoconstant_pool常量池constant_pool_count - 1
u2access_flags访问标记1
u2this_class类索引1
u2super_class父类索引1
u2interfaces_count接口索引数量1
u2interfaces接口内容interfaces_count
u2field_count字段表字段数量1
field_infofields字段表field_count
u2methods_count方法表方法数量1
method_infomethods方法表methods_count
u2attributes_count属性表属性数量1
attribute_infoattributes属性表attributes_count

无论是无符号数还是表,当需要描述同一类型单数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据位某一类型的集合。

代码文件

/**
 * 一个类可以抽象为:数据、指令、控制
 */
public class HelloWorldDemo {
    //常量、静态变量
    private final int i=0;
    private static int k=0;
    //成员变量
    private Object obj = new Object();
    private int sss=0;

    //局部变量
    public void methodOne(int i){
        int j=0;
        int sum=i+j;
        Object acb =obj;
        long start = System.currentTimeMillis();
        methodTwo();
        return;
    }

    private void methodTwo() {
        File file = new File("");
    }

    private static void methodThree(){
        methodThree();
    }

    public static void main(String[] args) {
        methodThree();
    }
}

.class文件的二进制内容

cafe babe 0000 0034 0041 0a00 0300 2f09
000d 0030 0700 3109 000d 0032 0900 0d00
330a 0034 0035 0a00 0d00 3607 0037 0800
380a 0008 0039 0a00 0d00 3a09 000d 003b
0700 3c01 0001 6901 0001 4901 000d 436f
6e73 7461 6e74 5661 6c75 6503 0000 0000
0100 016b 0100 036f 626a 0100 124c 6a61
7661 2f6c 616e 672f 4f62 6a65 6374 3b01
0003 7373 7301 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0018 4c63 6f6d
2f6a 766d 2f48 656c 6c6f 576f 726c 6444
656d 6f3b 0100 096d 6574 686f 644f 6e65
0100 0428 4929 5601 0001 6a01 0003 7375
6d01 0003 6163 6201 0005 7374 6172 7401
0001 4a01 0009 6d65 7468 6f64 5477 6f01
0004 6669 6c65 0100 0e4c 6a61 7661 2f69
6f2f 4669 6c65 3b01 000b 6d65 7468 6f64
5468 7265 6501 0004 6d61 696e 0100 1628
5b4c 6a61 7661 2f6c 616e 672f 5374 7269
6e67 3b29 5601 0004 6172 6773 0100 135b
4c6a 6176 612f 6c61 6e67 2f53 7472 696e
673b 0100 083c 636c 696e 6974 3e01 000a
536f 7572 6365 4669 6c65 0100 1348 656c
6c6f 576f 726c 6444 656d 6f2e 6a61 7661
0c00 1600 170c 000e 000f 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 740c 0013
0014 0c00 1500 0f07 003d 0c00 3e00 3f0c
0024 0017 0100 0c6a 6176 612f 696f 2f46
696c 6501 0000 0c00 1600 400c 0027 0017
0c00 1200 0f01 0016 636f 6d2f 6a76 6d2f
4865 6c6c 6f57 6f72 6c64 4465 6d6f 0100
106a 6176 612f 6c61 6e67 2f53 7973 7465
6d01 0011 6375 7272 656e 7454 696d 654d
696c 6c69 7301 0003 2829 4a01 0015 284c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b29 5600 2100 0d00 0300 0000 0400 1200
0e00 0f00 0100 1000 0000 0200 1100 0a00
1200 0f00 0000 0200 1300 1400 0000 0200
1500 0f00 0000 0600 0100 1600 1700 0100
1800 0000 5000 0300 0100 0000 1a2a b700
012a 03b5 0002 2abb 0003 59b7 0001 b500
042a 03b5 0005 b100 0000 0200 1900 0000
1200 0400 0000 0800 0400 0a00 0900 0d00
1400 0e00 1a00 0000 0c00 0100 0000 1a00
1b00 1c00 0000 0100 1d00 1e00 0100 1800
0000 8600 0200 0700 0000 1603 3d1b 1c60
3e2a b400 043a 04b8 0006 3705 2ab7 0007
b100 0000 0200 1900 0000 1a00 0600 0000
1200 0200 1300 0600 1400 0c00 1500 1100
1600 1500 1700 1a00 0000 3e00 0600 0000
1600 1b00 1c00 0000 0000 1600 0e00 0f00
0100 0200 1400 1f00 0f00 0200 0600 1000
2000 0f00 0300 0c00 0a00 2100 1400 0400
1100 0500 2200 2300 0500 0200 2400 1700
0100 1800 0000 4300 0300 0200 0000 0bbb
0008 5912 09b7 000a 4cb1 0000 0002 0019
0000 000a 0002 0000 001b 000a 001c 001a
0000 0016 0002 0000 000b 001b 001c 0000
000a 0001 0025 0026 0001 000a 0027 0017
0001 0018 0000 0020 0000 0000 0000 0004
b800 0bb1 0000 0001 0019 0000 000a 0002
0000 001f 0003 0020 0009 0028 0029 0001
0018 0000 0032 0000 0001 0000 0004 b800
0bb1 0000 0002 0019 0000 000a 0002 0000
0023 0003 0024 001a 0000 000c 0001 0000
0004 002a 002b 0000 0008 002c 0017 0001
0018 0000 001d 0001 0000 0000 0005 03b3
000c b100 0000 0100 1900 0000 0600 0100
0000 0b00 0100 2d00 0000 0200 2e

对字节码文件进行反编译

javap -c -v HelloWorldDemo.class >p.txt

得到结果如下

Classfile /j:/java-64/jdk1.8/bin/HelloWorldDemo.class
  Last modified 2018-10-15; size 1149 bytes
  MD5 checksum 6096f36225e1bb852ac023c4efa13a68
  Compiled from "HelloWorldDemo.java"
public class com.jvm.HelloWorldDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#47         // java/lang/Object."<init>":()V
   #2 = Fieldref           #13.#48        // com/jvm/HelloWorldDemo.i:I
   #3 = Class              #49            // java/lang/Object
   #4 = Fieldref           #13.#50        // com/jvm/HelloWorldDemo.obj:Ljava/lang/Object;
   #5 = Fieldref           #13.#51        // com/jvm/HelloWorldDemo.sss:I
   #6 = Methodref          #52.#53        // java/lang/System.currentTimeMillis:()J
   #7 = Methodref          #13.#54        // com/jvm/HelloWorldDemo.methodTwo:()V
   #8 = Class              #55            // java/io/File
   #9 = String             #56            //
  #10 = Methodref          #8.#57         // java/io/File."<init>":(Ljava/lang/String;)V
  #11 = Methodref          #13.#58        // com/jvm/HelloWorldDemo.methodThree:()V
  #12 = Fieldref           #13.#59        // com/jvm/HelloWorldDemo.k:I
  #13 = Class              #60            // com/jvm/HelloWorldDemo
  #14 = Utf8               i
  #15 = Utf8               I
  #16 = Utf8               ConstantValue
  #17 = Integer            0
  #18 = Utf8               k
  #19 = Utf8               obj
  #20 = Utf8               Ljava/lang/Object;
  #21 = Utf8               sss
  #22 = Utf8               <init>
  #23 = Utf8               ()V
  #24 = Utf8               Code
  #25 = Utf8               LineNumberTable
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               Lcom/jvm/HelloWorldDemo;
  #29 = Utf8               methodOne
  #30 = Utf8               (I)V
  #31 = Utf8               j
  #32 = Utf8               sum
  #33 = Utf8               acb
  #34 = Utf8               start
  #35 = Utf8               J
  #36 = Utf8               methodTwo
  #37 = Utf8               file
  #38 = Utf8               Ljava/io/File;
  #39 = Utf8               methodThree
  #40 = Utf8               main
  #41 = Utf8               ([Ljava/lang/String;)V
  #42 = Utf8               args
  #43 = Utf8               [Ljava/lang/String;
  #44 = Utf8               <clinit>
  #45 = Utf8               SourceFile
  #46 = Utf8               HelloWorldDemo.java
  #47 = NameAndType        #22:#23        // "<init>":()V
  #48 = NameAndType        #14:#15        // i:I
  #49 = Utf8               java/lang/Object
  #50 = NameAndType        #19:#20        // obj:Ljava/lang/Object;
  #51 = NameAndType        #21:#15        // sss:I
  #52 = Class              #61            // java/lang/System
  #53 = NameAndType        #62:#63        // currentTimeMillis:()J
  #54 = NameAndType        #36:#23        // methodTwo:()V
  #55 = Utf8               java/io/File
  #56 = Utf8
  #57 = NameAndType        #22:#64        // "<init>":(Ljava/lang/String;)V
  #58 = NameAndType        #39:#23        // methodThree:()V
  #59 = NameAndType        #18:#15        // k:I
  #60 = Utf8               com/jvm/HelloWorldDemo
  #61 = Utf8               java/lang/System
  #62 = Utf8               currentTimeMillis
  #63 = Utf8               ()J
  #64 = Utf8               (Ljava/lang/String;)V
{
  public com.jvm.HelloWorldDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field i:I
         9: aload_0
        10: new           #3                  // class java/lang/Object
        13: dup
        14: invokespecial #1                  // Method java/lang/Object."<init>":()V
        17: putfield      #4                  // Field obj:Ljava/lang/Object;
        20: aload_0
        21: iconst_0
        22: putfield      #5                  // Field sss:I
        25: return
      LineNumberTable:
        line 8: 0
        line 10: 4
        line 13: 9
        line 14: 20
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  this   Lcom/jvm/HelloWorldDemo;

  public void methodOne(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=7, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_1
         3: iload_2
         4: iadd
         5: istore_3
         6: aload_0
         7: getfield      #4                  // Field obj:Ljava/lang/Object;
        10: astore        4
        12: invokestatic  #6                  // Method java/lang/System.currentTimeMillis:()J
        15: lstore        5
        17: aload_0
        18: invokespecial #7                  // Method methodTwo:()V
        21: return
      LineNumberTable:
        line 18: 0
        line 19: 2
        line 20: 6
        line 21: 12
        line 22: 17
        line 23: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  this   Lcom/jvm/HelloWorldDemo;
            0      22     1     i   I
            2      20     2     j   I
            6      16     3   sum   I
           12      10     4   acb   Ljava/lang/Object;
           17       5     5 start   J

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #11                 // Method methodThree:()V
         3: return
      LineNumberTable:
        line 35: 0
        line 36: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_0
         1: putstatic     #12                 // Field k:I
         4: return
      LineNumberTable:
        line 11: 0
}
SourceFile: "HelloWorldDemo.java"

6.3.1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用时用语确定这个文件是否作为一个能被虚拟机接受的Class文件。Class文件的魔数值是0xCAFEBABE(咖啡宝贝?)。

紧挨着魔数的4个字节存储的是Class文件的版本号:第5和6个字节是次版本号(Minor Version),第7和8个字节是主版本号(Major Version)。Java的版本号是从45开始的,以后每个大版本发布后,主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生变化。

6.3.2 常量池

紧接着主次版本号之后的是常量池入口,它是Class文件中与其它项目关联最紧密的数据类型,也是占用Class文件空间最大的数据项目之一,同时还是Class文件中第一个出现的表类型数据项目。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近Java语言的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Quaified Name)
  • 字段的名称和描述
  • 方法的名称和描述符

6.3.3 访问标志

访问标志(access_flag):用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话,是否被声明为final,等等。

具体的标志位及含义,见下表:

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.2发生过改变,为了区别这条指令使用哪种语义,JDK1.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这时一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这时一个注解
ACC_ENUM0x4000标识这时一个枚举

6.3.4 类索引、父类索引、接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一个u2类型的数据集合,class文件由这三个数据来确定这个类的继承关系。

6.3.5 字段表集合

字段表(field_info):用于描述类或者接口的声明的变量,不包括方法的内部声明变量。

Java中一个字段(field)可以包含的信息包括:字段的作用域(public、private、protected)、类级别还是实例级别(static)、可变性(final)、并发可见性(volatile是否强制从主内存读写)、可否序列化(transient)、字段数据类型(基本类型、对象、数组)、名称。

6.3.6 方法表集合

方法表的结构如同 【Chapter 6.3.5 字段表集合】一样,一次包括了,访问标识(access_flag)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)。

6.3.7 属性表集合

属性表集合(attribute_info):用于描述某些场景(Class文件、字段表、方法表)的专有信息。

6.3.7.1 Code属性

Java程序方法体里面的代码经过javac编译器处理之后,最终会变成字节码指令存储在Code属性内。

6.3.7.2 Exceptions属性

列举出方法体中可能抛出的受查异常(checked exception),也就是方法描述时在throws关键字后面的异常。

6.3.7.3 LineNumberTable属性

用于描述Java字节码行号与源代码行号之间的对应关系(字节码的偏移量)。

6.3.7.4 LocalVariableTable属性

用于描述栈帧中局部变量表中的变量和Java源代码中定义的变量之间的关系。

6.3.7.5 SourceFile属性

用于记录生成这个Class文件的源码文件的名称。

6.3.7.6 ConstantValue属性

通知虚拟机自动为静态变量赋值。

6.3.7.7 InnerClasses属性

用于记录内部类与宿主之间的关联。

6.3.7.8 Deprecated和Synthetic属性

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定位废弃不推荐使用,可以在代码中通过 @deprecated 注释来设置。

Synthetic属性代表字段或方法并不是由Java源代码直接产生的,而是由编译器自行添加的。

6.4 字节码指令简介

6.4.1 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之间来回传输,这类指令包括如下内容。

将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。

将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。

将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。

扩充局部变量表的访问索引的指令:wide。

6.4.2 运算指令

加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

6.4.3 类型转换指令

Java虚拟机直接支持(即转换时无需显式的转换指令)以下数值类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换):

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

处理窄化类型转换(Narrowing Numeric Conversions)
这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

6.4.4 对象访问与创建

创建类实例的指令: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.4.5 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,
包括:
将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
将栈最顶端的两个数值互换:swap

6.4.6 控制转移指令

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
复合条件分支:tableswitch、lookupswitch。
无条件分支:goto、goto_w、jsr、jsr_w、ret。

6.4.7 方法调用和返回

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,

invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用

6.4.8 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现

除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

例如,在前面介绍的整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。

而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。

6.4.9 同步指令

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的
Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义

参考

简析java类文件结构
JVM指令详解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值