JVM深入学习(二十二) Class文件结构

162 阅读17分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

官网文档地址: docs.oracle.com/javase/spec…

1. Class的本质

Class类在java中都对应着一个类和接口的定义信息,但是Class不一定是文件的形式,也可能是流的形式,在类加载子系统中说到了类的加载也可以通过网络以流的形式来加载,所以Class的本质是8位字节为单位的二进制流.

2. Class文件的格式

ClassFile {
    u4             magic; 魔数
    u2             minor_version; 副版本号
    u2             major_version; 主版本号
    u2             constant_pool_count; 常量池计数器
    cp_info        constant_pool[constant_pool_count-1]; 常量池
    u2             access_flags; 访问标志
    u2             this_class; 类索引
    u2             super_class; 父类索引
    u2             interfaces_count; 接口索引计数器
    u2             interfaces[interfaces_count]; 接口索引
    u2             fields_count; 字段集合计数器
    field_info     fields[fields_count]; 字段集合
    u2             methods_count; 方法结合计数器
    method_info    methods[methods_count]; 方法集合
    u2             attributes_count; 属性集合计数器
    attribute_info attributes[attributes_count]; 属性集合
}

class文件的格式类似于c语言的结构存储格式,这种格式只有两种数据类型: 无符号数

无符号数: 基本数据类型,如上面的u1,u2,u4,u8分别代表1,2,4,8个字节的无符号数,无符号数用于描述数字,索引引用,数量值或字符串(UTF-8)

表: 由无符号数或者其他表组成的符合结构,Class文件可以看作一张表,表习惯性的以 _info结尾,如上文中的cp_info,表在定义的时候需要提前给定长度.

3. Class的内部结构

我们以反编译的一个class文件为例说明内部结构.

字节码如下:

反编译的文件如下:

Classfile /C:/Users/zy963/Desktop/ClassTest.class
  Last modified 2022-2-8; size 1062 bytes
  MD5 checksum f420c08327afed55d8bac02643163add
  Compiled from "ClassTest.java"
public class com.zy.study15.ClassTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#36        // java/lang/Object."<init>":()V
   #2 = Class              #37            // java/lang/StringBuilder
   #3 = Methodref          #2.#36         // java/lang/StringBuilder."<init>":()V
   #4 = Class              #38            // java/lang/String
   #5 = String             #39            // hello
   #6 = Methodref          #4.#40         // java/lang/String."<init>":(Ljava/lang/String;)V
   #7 = Methodref          #2.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = String             #42            // world
   #9 = Methodref          #2.#43         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = String             #44            // helloworld
  #11 = Fieldref           #45.#46        // java/lang/System.out:Ljava/io/PrintStream;
  #12 = Methodref          #47.#48        // java/io/PrintStream.println:(Z)V
  #13 = Class              #49            // com/zy/study15/ClassTest
  #14 = Class              #50            // java/lang/Object
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/zy/study15/ClassTest;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               helloWorld
  #27 = Utf8               Ljava/lang/String;
  #28 = Utf8               helloWorld1
  #29 = Utf8               helloworld2
  #30 = Utf8               StackMapTable
  #31 = Class              #25            // "[Ljava/lang/String;"
  #32 = Class              #38            // java/lang/String
  #33 = Class              #51            // java/io/PrintStream
  #34 = Utf8               SourceFile
  #35 = Utf8               ClassTest.java
  #36 = NameAndType        #15:#16        // "<init>":()V
  #37 = Utf8               java/lang/StringBuilder
  #38 = Utf8               java/lang/String
  #39 = Utf8               hello
  #40 = NameAndType        #15:#52        // "<init>":(Ljava/lang/String;)V
  #41 = NameAndType        #53:#54        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = Utf8               world
  #43 = NameAndType        #55:#56        // toString:()Ljava/lang/String;
  #44 = Utf8               helloworld
  #45 = Class              #57            // java/lang/System
  #46 = NameAndType        #58:#59        // out:Ljava/io/PrintStream;
  #47 = Class              #51            // java/io/PrintStream
  #48 = NameAndType        #60:#61        // println:(Z)V
  #49 = Utf8               com/zy/study15/ClassTest
  #50 = Utf8               java/lang/Object
  #51 = Utf8               java/io/PrintStream
  #52 = Utf8               (Ljava/lang/String;)V
  #53 = Utf8               append
  #54 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #55 = Utf8               toString
  #56 = Utf8               ()Ljava/lang/String;
  #57 = Utf8               java/lang/System
  #58 = Utf8               out
  #59 = Utf8               Ljava/io/PrintStream;
  #60 = Utf8               println
  #61 = Utf8               (Z)V
{
  public com.zy.study15.ClassTest();
    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/zy/study15/ClassTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=4, args_size=1
         0: new           #2                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
         7: new           #4                  // class java/lang/String
        10: dup
        11: ldc           #5                  // String hello
        13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: new           #4                  // class java/lang/String
        22: dup
        23: ldc           #8                  // String world
        25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        28: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        31: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: astore_1
        35: ldc           #10                 // String helloworld
        37: astore_2
        38: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        41: aload_1
        42: aload_2
        43: if_acmpne     50
        46: iconst_1
        47: goto          51
        50: iconst_0
        51: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        54: new           #4                  // class java/lang/String
        57: dup
        58: ldc           #10                 // String helloworld
        60: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        63: astore_3
        64: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        67: aload_2
        68: aload_3
        69: if_acmpne     76
        72: iconst_1
        73: goto          77
        76: iconst_0
        77: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V
        80: return
      LineNumberTable:
        line 11: 0
        line 13: 35
        line 16: 38
        line 19: 54
        line 21: 64
        line 23: 80
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      81     0  args   [Ljava/lang/String;
           35      46     1 helloWorld   Ljava/lang/String;
           38      43     2 helloWorld1   Ljava/lang/String;
           64      17     3 helloworld2   Ljava/lang/String;
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 50
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
        frame_type = 255 /* full_frame */
          offset_delta = 24
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream ]
        frame_type = 255 /* full_frame */
          offset_delta = 0
          locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String ]
          stack = [ class java/io/PrintStream, int ]
}
SourceFile: "ClassTest.java"

3.1 概述

class文件的结构会随着jdk版本的迭代有所修改,但是基本结构不会有太大的变动

总体结构如下:

  1. 魔数
  2. Class文件版本(副版本号)
  1. Class文件版本(主版本号)
  2. 常量池计数器
  1. 常量池表
  2. 访问标志
  1. 类索引
  2. 父类索引
  1. 接口计数器
  2. 接口索引表
  1. 字段计数器
  2. 字段表集合
  1. 方法计数器
  2. 方法表集合
  1. 属性计数器
  2. 属性表集合

这个结构可以参照文件格式中的class文件的格式.

3.2 魔数 magic

魔数用于来校验class文件是否是一个合法的class文件

文件的后缀名可以随意篡改,所以不能用后缀名来作为校验的规则.

u4             magic;

根据class文件的格式要求,class文件开头的4个字节应该是ca fe ba be

含义是(咖啡宝贝)也就是java的图标..

\

举例:

任意class文件的开头必须是这个魔数,如果不是就会出现非法错误.

举例:

我们随意以某一个图片文件,将其后缀篡改为.class后,使用javap -v 进行反编译,看下错误:

反编译的时候出现如下错误:

运行该class文件出现如下错误:

\

3.3 版本号 minor/major version

版本号分为主版本和副版本

具体版本号与jdk对照关系如下:

jdk版本主版本副版本
1.1453
1.2460
1.3470
1.4480
1.5490
1.6500
1.7510
1.8520
1.9530
1.10540
1.11550

版本号依次类推即可

对于版本号,经常性的会出现一个错误就是高版本jdk编译的class文件不能由低版本jdk执行

解释执行是向下兼容的,即低版本jdk编译的class文件可以由高版本jdk编译执行.

看下官网的解释:

minor_version, major_version
The values of the minor_version and major_version items are the minor and major version numbers of this class file. Together, a major and a minor version number determine the version of the class file format. If a class file has major version number M and minor version number m, we denote the version of its class file format as M.m. Thus, class file format versions may be ordered lexicographically, for example, 1.5 < 2.0 < 2.1.

A Java Virtual Machine implementation can support a class file format of version v if and only if v lies in some contiguous range Mi.0 ≤ v ≤ Mj.m. The release level of the Java SE platform to which a Java Virtual Machine implementation conforms is responsible for determining the range.

Oracle's Java Virtual Machine implementation in JDK release 1.0.2 supports class file format versions 45.0 through 45.3 inclusive. JDK releases 1.1.* support class file format versions in the range 45.0 through 45.65535 inclusive. For k ≥ 2, JDK release 1.k supports class file format versions in the range 45.0 through 44+k.0 inclusive.

翻译过来:

minor_version 和项目 的值是这个文件major_version的次要和主要版本号。class主要和次要版本号一起确定 class文件格式的版本。如果一个class文件有主版本号 M 和次要版本号 m,我们将其class文件格式的版本表示为 Mm 因此,class文件格式版本可以按字典顺序排序,例如,1.5 < 2.0 < 2.1class当且仅当 v 位于某个连续范围 Mi.0 ≤ v ≤ Mj.m时, Java 虚拟机实现才能支持版本 v 的文件格式。Java 虚拟机实现所遵循的 Java SE 平台的发布级别负责确定范围。

JDK 版本 1.0.2 中的 Oracle 的 Java 虚拟机实现支持class文件格式版本 45.045.3(含)。JDK 发行版 1.1.* 支持class45.045.65535(含)范围内的文件格式版本。对于 k ≥ 2,JDK 版本 1.k 支持class45.044+k.0(含)范围内的文件格式版本。

对于 k ≥ 2,JDK 版本 1.k 支持class45.0 到 44+k.0(含)范围内的文件格式版本。

这个就是高版本可以解释执行低版本的class文件的说明

当k为8时,根据计算得出 jdk1.8支持45到52范围内的class文件


3.4 常量池 Constant pool

常量池用于存放字面量符号引用. 在被加载后,常量池会进入到方法区的运行时常量池中.(jdk1.7后字符串常量池转移到了堆中)

常量池应该由常量池计数器来标志常量池表的大小.

3.4.1 常量池计数器

常量池计数器用于表示常量池的大小

以上文中的例子来说,常量池的大小为62

但是我们看反编译后的文件里,只有61个常量项,这是因为第0项被空出来了,这个空的第0项用于表示没有引用任何常量项的含义.即当某种特殊情况下需要表示不引用任何一个常量池项时,需要使用0.

因此常量池的大小其实可以被定义为 常量池计数器-1

3.4.2 常量池表

常量池表里就是字面量和符号引用(jdk7之后,常量池里其实还加入了新的类型来支持java的动态语言.)

而在字节码中通常使用第一个字节作为标识数据类型的标志,这个字节被称为tag byte,而常用的数据类型的标志如下表:

数据类型标志描述
CONSTANT_utf8_info1utf-8编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类/接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_FieldRef_info9字段的符号引用
CONSTANT_MethodRef_info10类中方法的符号引用
CONSTANT_InterfacteMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的符号引用
CONSTANT_MethodHandle_info15表示方法句柄 jdk1.7后为支持动态语言特性新增
CONSTANT_MethodType_info16表示方法类型 jdk1.7后为支持动态语言特性新增
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点 jdk1.7后为支持动态语言特性新增

字节码通过 1 3 4 ... 这种标志位表示字面量的类型.

常量池表可以看作一个数组,里面存放了各种字面量和符合引用,他们的类型就是上表中的数据类型,在字节码中用标志来表示具体的类型.

3.4.2.1 字面量

字面量的含义就是文本字符串和用final表示的常量

例如:

String str = "hello world";
final int NUM = 10;

3.4.2.2 符号引用

理解: 当内存地址还没分配的时候,我们的引用关系要如何表示呢? 就是通过符号引用表示引用关系

符号引用主要包括三种情况

  1. 类/接口的全限定名
  2. 字段的名称和描述符
  1. 方法的名称和描述符
3.4.2.2.1 全限定名

就是类/接口的全类名,但是将分隔符.换成/,并以;结束用于区别多个全限定名

3.4.2.2.2 名称

字段/方法的简单名称,比如方法 public void test(){} 的名称就为test

3.4.2.2.2 描述符

描述符用于描述字段的类型,方法的参数列表(数量,类型及顺序)和返回值

描述符将基本数据类型及Void类型都用大写字母表示, 而对象类型用大写字母L表示

具体对应关系如下表:

标识符含义
B基本数据类型byte
C基本数据类型char
D基本数据类型doucle
F基本数据类型float
I基本数据类型int
J基本数据类型long
S基本数据类型short
Z基本数据类型boolean
Vvoid类型
L对象类型
[数组类型 一个[代表一维数组

举例:

package com.zy.study15;

/**
 * @author zy
 * @version 1.0.0
 * @ClassName ClassDescribeTest.java
 * @Description TODO
 * @createTime 2022年02月13日 10:59:00
 */
public class ClassDescribeTest {

    public static void main(String[] args) {
        Object[] objects = new Object[10];
        System.out.println(objects);
    }

}

可以看到输出的内容前面部分就是符号引用 [代表数组,L代表对象类型 然后是全限定名 最后是内存地址

符号引用和直接引用的理解:

符号引用: class文件还未加载到内存中,为了表示引用关系而出现的一种引用类型,由一组符号组成,此时符号引用引用的对象并不一定加载到了内存中

直接引用: 直接引用顾名思义就是引用直接指向了内存中目标对象的引用

而我们栈帧中的动态链接部分就是将符号引用转化为直接引用的

常量池表可以看作Class文件的底层资源基础,基本上其他的结构都会调用常量池表中的信息.

3.5 访问标志 access_flag

访问标志用来确定class文件的访问权限和属性

标志码如下:

标志属性码值含义
ACC_PUBLIC0x0001表示public;可以从其包外部访问。
ACC_FINAL0x0010表示final;不允许子类。
ACC_SUPER0x0020当被invokespecial指令 调用时,对超类方法进行特殊处理。 每个类都默认为true,即这个标志每个类都有
ACC_INTERFACE0x0200是接口,不是类。
ACC_ABSTRACT0x0400表示abstract;不得实例化。
ACC_SYNTHETIC0x1000表示由编译器生成;源代码中不存在。
ACC_ANNOTATION0x2000声明为注解类型。
ACC_ENUM0x4000声明为一种enum类型。

\

访问标志的值由上述表格中的码值组合而成.

比如 21的访问标志的意义就是 20 + 1 即 ACC_SUPER+ ACC_PUBLIC

\

官网说明:

An interface is distinguished by the ACC_INTERFACE flag being set. If the ACC_INTERFACE flag is not set, this class file defines a class, not an interface.

If the ACC_INTERFACE flag is set, the ACC_ABSTRACT flag must also be set, and the ACC_FINAL, ACC_SUPER, and ACC_ENUM flags set must not be set.

If the ACC_INTERFACE flag is not set, any of the other flags in Table 4.1-A may be set except ACC_ANNOTATION. However, such a class file must not have both its ACC_FINAL and ACC_ABSTRACT flags set (JLS §8.1.1.2).

The ACC_SUPER flag indicates which of two alternative semantics is to be expressed by the invokespecial instruction (§invokespecial) if it appears in this class or interface. Compilers to the instruction set of the Java Virtual Machine should set the ACC_SUPER flag. In Java SE 8 and above, the Java Virtual Machine considers the ACC_SUPER flag to be set in every class file, regardless of the actual value of the flag in the class file and the version of the class file.

The ACC_SUPER flag exists for backward compatibility with code compiled by older compilers for the Java programming language. In JDK releases prior to 1.0.2, the compiler generated access_flags in which the flag now representing ACC_SUPER had no assigned meaning, and Oracle's Java Virtual Machine implementation ignored the flag if it was set.

The ACC_SYNTHETIC flag indicates that this class or interface was generated by a compiler and does not appear in source code.

An annotation type must have its ACC_ANNOTATION flag set. If the ACC_ANNOTATION flag is set, the ACC_INTERFACE flag must also be set.

The ACC_ENUM flag indicates that this class or its superclass is declared as an enumerated type.

All bits of the access_flags item not assigned in Table 4.1-A are reserved for future use. They should be set to zero in generated class files and should be ignored by Java Virtual Machine implementations.

翻译记录如下:

ACC_INTERFACE接口通过设置的标志来区分。如果未设置ACC_INTERFACE,则此 class文件定义一个类,而不是一个接口。

如果设置了ACC_INTERFACE,那同时应该设置ACC_ABSTRACT标志,并且不能设置ACC_FINAL、ACC_SUPER和ACC_ENUM标志。

如果未设置ACC_INTERFACE,则可以设置除了表 4.1-AACC_ANNOTATION的任何其他标志 但是,此类class文件不得同时设置其ACC_FINAL和ACC_ABSTRACT标志。

Java 虚拟机指令集的编译器应设置ACC_SUPER 标志。在 Java SE 8 及更高版本中,Java 虚拟机认为 ACC_SUPER要在每个class文件中设置标志,该ACC_SUPER标志的存在是为了向后兼容由 Java 编程语言的旧编译器编译的代码。在 JDK 1.0.2 之前的版本中,生成的编译器access_flags现在表示的标志 ACC_SUPER没有指定的含义,如果设置了标志,Oracle 的 Java 虚拟机实现会忽略该标志。

ACC_SYNTHETIC标志表示此类或接口是由编译器生成的,而不是有源代码生成的。

ACC_ANNOTATION标志表示注解类型。如果设置了ACC_ANNOTATION 则必须设置ACC_INTERFACE标志,因为java中的注解都是接口

该ACC_ENUM标志指示该类或其超类被声明为枚举类型。

3.6 当前类/父类/接口索引 this_class/super_class/interface

这部分主要是表示当前class文件的当前类名,父类类名,以及当前类实现的接口集合信息(多接口实现)

这三类其实还是引用常量池的常量符号引用,也说明常量池的重要性

3.7 字段表

字段指的是类/接口中的变量,即类变量,不包含局部变量(方法内部的变量)

字段表中的字段属性还是引用常量池中的基础信息.

字段表中包含类变量和实例变量(即对象创建后的字段)

字段表中主要描述字段的标识符,访问修饰符(访问权限),类变量还是实例变量,是否是常量等属性

\

注: 字段表中不存放父类字段的信息,只存放当前类的字段信息. 特殊情况: 内部类 内部类中可能存在访问外部类的字段(此字段不是由源代码生成)

另外: 字段表中的规范: 只要两个字段的标识不完全一样都是合法的,即字段表允许两个字段的名字是相同的 但是java语言规范里是不允许同名字段的.

3.7.1 字段的结构

字段表中是字段的集合

字段结构如下:

  1. 字段访问标识
  2. 字段名索引
  1. 字段描述符索引
  2. 属性信息

\

3.7.1.1 字段访问标识

与访问标志类似的,字段访问标识就是字段的访问权限,同样该标志可以使用访问标识的表

如上,增加public修饰符,则访问标志为0x0001

3.7.1.2 字段名索引

指向常量池引用.

可以看到就是一个字符串s

\

3.7.1.3 字段描述符索引

字段描述符索引主要描述字段的数据类型

描述符具体内容可以参考上文中符号引用部分描述符.

我们可以看到描述符的具体内容其实就是一个String类型 L代表对象 java/lang/string为全限定名.

3.7.1.4 属性信息表

主要记录字段的属性信息.比如初始化值,一些额外信息等

举例 初始化值: 初始化值只有常量属性才会记录.

我们将字段s改为常量,那么就会出一个属性ConstantValue记录该字段的初始化值

而属性也有自己的结构

  1. 属性名索引: 指向常量池,其实也就是ConstantValue这个字符串
  2. 属性值长度: 常量的属性值长度恒为2
  1. 属性值索引: 指向常量池 我们这里就是一个空字符串,指向cp_info #2的常量.

3.8 方法表

方法表与字段表类似,只不过是用来描述方法信息的

方法表中会存在一些生成的方法,比如类初始化方法cinit和实例初始化方法init

由于java中方法允许重载,所以可能存在同名方法,但是java规范要求方法的参数不能一样, 但是对于字节码的方法表来说,只要求方法的特征签名不完全一致即可,即允许同名方法的参数一致,只要返回值不一致即可.

添加三个方法如下

看下字节码:

可以看到一个方法就对应方法表中的一项.

3.8.1 方法结构

方法的结构也与字段类似:

  1. 方法访问标识
  2. 方法名索引
  1. 方法描述符
  2. 属性表信息

3.8.1.1 方法访问标识

与字段访问标识类似,同样与访问标志的码表相同

3.8.1.2 方法名索引

方法名同样指向常量池

方法名就是method

3.8.1.3 方法描述符索引

描述符主要描述方法的参数和返回值

同样指向了常量池,当前method方法没有形参,返回值是Integer类型

3.8.1.4 属性表信息

方法的属性表信息主要是方法内部代码生成的字节码指令

docs.oracle.com/javase/spec…

属性表的类型有很多,我们可以参考官网的文档如上

比如其中一种属性,也是我们方法中用的 即 Code

code的结构如下:

Code_attribute {
    u2 attribute_name_index; //属性名索引
    u4 attribute_length; // 属性长度
    u2 max_stack; // 操作数栈最大长度
    u2 max_locals; // 局部变量表长度
    u4 code_length; // 字节码表长度(计数器)
    u1 code[code_length]; // 字节码表
    u2 exception_table_length; // 异常表长度(技术器)
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];// 异常表
    u2 attributes_count; // 属性长度
    attribute_info attributes[attributes_count]; //属性
}

其中字节码长度为5的解释:

字节码指令可以看到只有三个:

但是第二个字节码指令 invokestatic 后又一个引用 #4 这个引用要占用两个字节,所以长度为5

属性表中还包含属性表.以Code为例,Code中仍然包含了LineNumberTable和LocalVariableTable两个属性表,如上图所示

在jvm的属性表中,也可以看到对应的描述:

attributes[]

Each value of the attributes table must be an attribute_info structure (§4.7).

A Code attribute can have any number of optional attributes associated with it.

The attributes defined by this specification as appearing in the attributes table of a Code attribute are listed in Table 4.7-C.

The rules concerning attributes defined to appear in the attributes table of a Code attribute are given in §4.7.

The rules concerning non-predefined attributes in the attributes table of a Code attribute are given in §4.7.1.

3.9 属性表

这里的属性表主要是class的属性信息,在上面其实已经描述了字段/方法的属性信息,而class文件也有自己的属性信息.

class文件的属性信息主要是用于jvm的验证和允许,包括class文件的源文件名称,注解等辅助信息.

属性表的具体描述与方法/字段中的属性表一致,只是这里的属性表是对Class文件的属性描述

3.9.1 SourceFile

属性表里以SourceFile为例,说明属性表具体信息,总共属性表有23种属性

docs.oracle.com/javase/spec…

SourceFile属性的结构如下

SourceFile_attribute {
    u2 属性名索引;
    u4 属性长度;
    u2 源文件索引;
}

其实这个属性就是用来标识源文件名称的,源文件索引指向常量池中的某个字符串

如下:

4. 总结

Class文件作为java的基石,是java做到跨平台的核心.

可以理解为Java虚拟机其实就是面向Class文件的,而其他任何语言只要能够编译成符合规范的Class文件,那么都可以在jvm上运行,从这点上来看Class文件起到一个非常关键的作用.

从另外一个角度看,了解Class文件中的结构,内容及指令,对我们深入理解java中的某些源码的执行过程非常有利,比如可以通过Class文件中的指令来观察具体的代码执行过程,从而了解一些不清除的知识点.