Java的 Class(类)文件结构详解

  了解Class文件的结构组成,对于我们后续的JVM以及Java原理深入学习是很有帮助的,因为Class文件帮我们默默的做了很多事,比如、为什么对象方法中可以直接使用this变量?!本文将带领大家,一步步,从开头到结尾,逐字逐句分析、了解、深入Class文件组成和结构!
  本文类容比较多,不建议直接看,建议收藏,并跟着我的案例一起做!如果10个人中能有3个人跟着学完本文(敲完代码),那么本人的目的就算达到了。

1 Class文件与无关性

  Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode,Class文件语法)是构成平台无关性的基石,也是实现语言无关性的基石。
在这里插入图片描述

2 Class文件的结构

2.1 Class文件结构概述

  任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。Class文件是一组以8位字节为基础单位的二进制流。
  Java 虚拟机规范规定 Class 文件格式采用一种类似与 C 语言结构体的伪结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数和表

  1. 无符号数属于基本的数据类型,以 u1、u2、u4、u8来分别代表 1 个字节、2 个字节、4 个字节和 8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码结构构成的字符串值。对于字符串,则使用u1数组进行表示。
  2. 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以「_info」结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件就是一张ClassFile 表,它由下表中所示的数据项构成。

根据 Java 虚拟机规范,一个Class文件由单个 ClassFile 结构组成:

ClassFile {
  u4  magic;   //Class 文件的标志,魔术
  u2  minor_version;  //Class 的附版本号
  u2  major_version;  //Class 的主版本号
  u2  constant_pool_count;  //常量池表项的数量
  cp_info  constant_pool[constant_pool_count-1];  //常量池表项,索引为1~constant_pool_count-1
  u2  access_flags;  //Class 的访问标志(类访问修饰符)
  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文件字节码结构组织示意图:
在这里插入图片描述
  下面来一个一个具体介绍

2.2 魔数

u4  magic;  //Class 文件的标志

  每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。之所以使用魔数而不是文件后缀名来进行识别主要是基于安全性的考虑,因为文件后缀名是可以随意更改的(当然魔术也可以改,只不过比起改后缀名来说更复杂)。
  Class 文件的魔数值固定为「0xCAFEBABE」。Java一直以咖啡为代言,CAFEBABE可以认为是 Cafe Babe,读音上和Cafe Baby很近。所以这个也许就是代表Cafe Baby的意思。

2.2.1 案例

  Java源码

public class ClassFile {
    public static final String J = "2222222";

    private int k;

    public int getK() {
        return k;
    }

    public void setK(int k) throws Exception {

        try {
            this.k = k;
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } finally {
        }
    }

    public static void main(String[] args) {

    }
}

  运行后,会出现 Class文件,拿到Class文件,使用notepad++编辑器打开–点击插件–HEX-Editor(没有该插件的自行下载)–view in HEX,即可以16进制形式查看Clsaa文件。
  当然也可以直接使用HEX-Editor软件打开:hex-editor
  我们先看前四个字节:
在这里插入图片描述
  我们可以看到魔数在首位,并且正是0xcafebabe

2.3 补充:各种名以及描述符

2.3.1 全限定名和非限定名

  Class文件中的类和接口,都是使用全限定名,又被称作Class的二进制名称。例如“com/ikang/JVM/classfile”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。
  非限定名又被称作简单名称,Class文件中的方法、字段、局部变量、形参名称,都是使用简单名称,没有类型和参数修饰,例如这个类中的getK()方法和k字段的简单名称分别是“getK”和“m”。
  非限定名不得包含ASCII字符. ; [ / ,此外方法名称除了特殊方法名称和方法之外,它们不能包含ASCII字符<或>,字段名称或接口方法名称可以是或,但是没有方法调用指令可以引用,只有invokespecial指令可以引用。

2.3.2 描述符

  描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值类型。

2.3.2.1 字段描述符

  根据描述符规则基本数据类型(byte、char、doubIc、float, int、loog、shon, boolean)以及代表无返回伯的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名来表示,数组则用[
  字段描述符的类型含义表

字段描述符 类型 含义
B byte 基本类型byte
C char 基本类型char
D double 基本类型double
F float 基本类型float
I int 基本类型int
J long 基本类型long
LClassName; reference 对象类型,例如Ljava/lang/Object;
S short 基本类型short
Z boolean 基本类型boolean
[ reference 数组类型

  对象类型的实例变量的字段描述符是L+类的二进制名称的内部形式。
  对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”

2.3.2.2 方法描述符

  它基于描述符标识字符含义表所示的字符串的类型表示方法, 同时对方法签名的表示做了一些规定。它将函数的参数类型写在一对小括号中, 并在括号右侧给出方法的返回值。
  比如, 若有如下方法:

Object m(int i, double d, Thread t) {… }

  则它的方法描述符为:

(IDLjava/lang/Thread;)Ljava/lang/Object;

  可以看到, 方法的参数统一列在一对小括号中, “I”表示int , “D”表示double,“Ljava/lang/Thread;”表示Thread对象。小括号右侧的Ljava/lang/Object;表示方法的返同值为Object对象类型。

2.4 Class文件版本

u2  minor_version;  //Class 的附版本号
u2  major_version;  //Class 的主版本号

  紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 两个字节是附版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。高版本的 JDK 能够向下兼容低版本的 Class 文件,但虚拟机会拒绝执行超过其版本号的 Class 文件(低版本不能向上打开高版本文件)。

JDK主版本号 Class主版本号 16进制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

2.4.1 案例

  使用上面的案例,向后取四个字节:
在这里插入图片描述
  我们发现主版本号为0x0034,转换为十进制为52,可知属于JDK1.8

2.5 常量池

  u2  constant_pool_count;  //常量池表项的数量
  cp_info  constant_pool[constant_pool_count-1];  //常量池表项,索引为1~constant_pool_count-1

  紧接着主次版本号之后的是常量池的常量数量constant_pool_count,但是常量池的常量实际数量是 constant_pool_count-1(常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表“不引用任何一个常量池项;其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。”)。
  之后就是常量池的实际内容constant_pool,每一项以类型、长度、内容或者、类型、内容的格式依次排列存放。

  常量池是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它还是 Class 文件中第一个出现的表类型数据项目。
  java虚拟机指令并不依赖类、接口、类实例或者数组的运行时布局。相反,指令依靠常量池中的符号信息,常量池是整个Class文件的基石。
  Class常量池主要存放两大常量:字面量和符号引用

  1. 字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
  2. 符号引用则属于编译原理方面的概念。包括下面三类常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符

  补充:由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译

2.5.1 常量池表项目类型

  在JDK1.8中有14种常量池项目类型,每一种项目都有特定的表结构,这14种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
  常量池tag类型表:

常量类型 标志(tag) 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

2.5.2 常量池表结构

  每种常量类型均有自己的表结构,非常繁琐:
  常量池项目结构表:

常量 描述 项目 类型 项目描述
CONSTANT_Utf8_info UTF-8编码的字符串 tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes[length] u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info 整型字面量 tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info 浮点型字面量 tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Long_info 长整型字面量 tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info 双精度浮点型字面量 tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info 类或接口的符号引用 tag u1 值为7
name_index u2 指向全限定名常量项的索引
CONSTANT_String_info 字符串类型字面量 tag u1 值为8
string_index u2 指向字符串字面量的索引
CONSTANT_Fieldref_info 字段的符号引用 tag u1 值为9
class_index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
name_and_type_index u2 指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_info 类中方法的符号引用 tag u1 值为10
class_index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
name_and_type_index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_info 接口中方法的符号引用 tag u1 值为11
class_index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
name_and_type_index u2 指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_info 字段或方法的部分符号引用 tag u1 值为12
name_index u2 指向该字段或方法名称常量项的索引
descriptor_index u2 指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_info 表示方法句柄 tag u1 值为15
reference_kind u1 值必须在1~9范围,它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效索引
CONSTANT_MethodType_info 标识方法类型 tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_InvokeDynamic_info 表示一个动态方法调用点 tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

  当然,所有的常量池条目都有如下的通用结构如下:

cp_info {
  u1   tag;
  u1   info[];
}

2.5.3 案例

2.5.3.1 constant_pool_count

  继续向后走,到了constant_pool_count取两个字节
在这里插入图片描述
  可以看到常量池的表项目数量0x2f,转换为十进制为47,那么常量池表项为47-1=46项。因为常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表不引用任何一个常量池项;但是其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。

2.5.3.2 第一项常量

  继续向后走,到了constant_pool了,常量类型的通用第一项均是u1长度的tag,那么向后取一个字节,即取第一个常量的tag:
在这里插入图片描述
  该值为0x0a,转换为10进制就是tag=10,查找上面的常量池项目类型表,可知第一个常量属于CONSTANT_Methodref_info,表示类中方法的符号引用,在常量池结构表中查找它的结构,如下:
在这里插入图片描述
  由于后两个字段都是指向常量池的索引,因此完整结构为:
在这里插入图片描述
  继续向后走4个字节:
在这里插入图片描述
  class_index=0x0006,表示指向声明方法的类描述符CONSTANT_Class_info的索引项由常量池第6个常量字符串指定。
  name_and_type_index=0x0025,表示指向名称及类型描述符CONSTANT_NameAndType的索引项由常量池第37个常量字符串指定。

2.5.3.3 第二项常量

  第一项常量结束,继续向下查找第二个常量项的tag。
在这里插入图片描述
  该tag为0x09,转换为十进制就是9,查找上面的项目类型表,可知第二个常量项属于CONSTANT_Fieldref_info,表示字段的符号引用,在结构表中查找它的结构,如下:
在这里插入图片描述
  可知后面两个u2类型项目同样都是指向常量池的索引,向后取四个字节:
在这里插入图片描述
  class_index=0x0005,表示指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项由常量池第5个常量字符串指定。
  name_and_type_index=0x0026,表示指向字段描述符CONSTANT_NameAndType的索引项由常量池第38个常量字符串指定。

2.5.3.4 第三项常量

  第二项常量结束,继续向下查找第三个常量项的tag:
在这里插入图片描述
  该tag为0x07,转换为十进制就是7,查找上面的项目类型表,可知第三个常量项属于CONSTANT_Class_info,表示类或接口的符号引用,在结构表中查找它的结构,如下:
在这里插入图片描述
  可知后面一个u2类型项目是指向常量池的索引,向后取两个字节:
在这里插入图片描述
  name_index=0x0027,表示该类的全限定类名由常量池第39个常量字符串指定。

2.5.3.5 第四项常量

  第三项常量结束,继续向下查找第四项常量的tag:
在这里插入图片描述
  该tag值为0x0a,转换而为10进制就是tag=10,查找上面的项目类型表,可知第四个常量属于CONSTANT_Methodref_info,表示类中方法的符号引用,在结构表中查找它的结构,如下:
在这里插入图片描述
  可知后面两个u2类型项目都是索引,向后取四个字节:

在这里插入图片描述
  class_index=0x0003,表示指向声明方法的类描述符CONSTANT_Class_info的索引项由常量池第3个常量字符串指定。回过头看我们上面找到的的第三个常量,刚好是CONSTANT_Class_info类型,说明我们到目前所有查找是正确的。
  name_and_type_index=0x0028,表示指向名称及类型描述符CONSTANT_NameAndType的索引项由常量池第40个常量字符串指定。

  第四项常量结束,目前找到了4个,还剩下42个,这种方法看起来很麻烦,还容易出错。我们可以使用javap工具加上-v参数帮我们分析字节码文件。

2.5.4 javap命令帮助分析Class文件

  javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
  我们可以直接使用idea自带的控制台
  找到class文件,右键——open in Terminal
在这里插入图片描述
  输入 javap -v ClassFile.class 指令,即可输出Class文件信息。
在这里插入图片描述
信息如下:

Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class
  Last modified 2020-4-4; size 960 bytes
  MD5 checksum a7fc5ccb0b193f1d32e2658e68fed475
  Compiled from "ClassFile.java"
public class com.ikang.JVM.classfile.ClassFile
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#37         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#38         // com/ikang/JVM/classfile/ClassFile.k:I
   #3 = Class              #39            // java/lang/IllegalStateException
   #4 = Methodref          #3.#40         // java/lang/IllegalStateException.printStackTrace:()V
   #5 = Class              #41            // com/ikang/JVM/classfile/ClassFile
   #6 = Class              #42            // java/lang/Object
   #7 = Utf8               J
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               ConstantValue
  #10 = String             #43            // 2222222
  #11 = Utf8               k
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/ikang/JVM/classfile/ClassFile;
  #20 = Utf8               getK
  #21 = Utf8               ()I
  #22 = Utf8               setK
  #23 = Utf8               (I)V
  #24 = Utf8               e
  #25 = Utf8               Ljava/lang/IllegalStateException;
  #26 = Utf8               StackMapTable
  #27 = Class              #39            // java/lang/IllegalStateException
  #28 = Class              #44            // java/lang/Throwable
  #29 = Utf8               Exceptions
  #30 = Class              #45            // java/lang/Exception
  #31 = Utf8               main
  #32 = Utf8               ([Ljava/lang/String;)V
  #33 = Utf8               args
  #34 = Utf8               [Ljava/lang/String;
  #35 = Utf8               SourceFile
  #36 = Utf8               ClassFile.java
  #37 = NameAndType        #13:#14        // "<init>":()V
  #38 = NameAndType        #11:#12        // k:I
  #39 = Utf8               java/lang/IllegalStateException
  #40 = NameAndType        #46:#14        // printStackTrace:()V
  #41 = Utf8               com/ikang/JVM/classfile/ClassFile
  #42 = Utf8               java/lang/Object
  #43 = Utf8               2222222
  #44 = Utf8               java/lang/Throwable
  #45 = Utf8               java/lang/Exception
  #46 = Utf8               printStackTrace
{
  public static final java.lang.String J;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String 2222222

  public com.ikang.JVM.classfile.ClassFile();
    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 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ikang/JVM/classfile/ClassFile;

  public int getK();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field k:I
         4: ireturn
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ikang/JVM/classfile/ClassFile;

  public void setK(int) throws java.lang.Exception;
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field k:I
         5: goto          19
         8: astore_2
         9: aload_2
        10: invokevirtual #4                  // Method java/lang/IllegalStateException.printStackTrace:()V
        13: goto          19
        16: astore_3
        17: aload_3
        18: athrow
        19: return
      Exception table:
         from    to  target type
             0     5     8   Class java/lang/IllegalStateException
             0     5    16   any
             8    13    16   any
      LineNumberTable:
        line 16: 0
        line 20: 5
        line 17: 8
        line 18: 9
        line 20: 13
        line 19: 16
        line 21: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       4     2     e   Ljava/lang/IllegalStateException;
            0      20     0  this   Lcom/ikang/JVM/classfile/ClassFile;
            0      20     1     k   I
      StackMapTable: number_of_entries = 3
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/IllegalStateException ]
        frame_type = 71 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 2 /* same */
    Exceptions:
      throws java.lang.Exception

  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: return
      LineNumberTable:
        line 25: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
}
SourceFile: "ClassFile.java"

截取其中的常量池部分,也就是Constant pool部分:

Constant pool:
   #1 = Methodref          #6.#37         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#38         // com/ikang/JVM/classfile/ClassFile.k:I
   #3 = Class              #39            // java/lang/IllegalStateException
   #4 = Methodref          #3.#40         // java/lang/IllegalStateException.printStackTrace:()V
   #5 = Class              #41            // com/ikang/JVM/classfile/ClassFile
   #6 = Class              #42            // java/lang/Object
   #7 = Utf8               J
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               ConstantValue
  #10 = String             #43            // 2222222
  #11 = Utf8               k
  #12 = Utf8               I
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/ikang/JVM/classfile/ClassFile;
  #20 = Utf8               getK
  #21 = Utf8               ()I
  #22 = Utf8               setK
  #23 = Utf8               (I)V
  #24 = Utf8               e
  #25 = Utf8               Ljava/lang/IllegalStateException;
  #26 = Utf8               StackMapTable
  #27 = Class              #39            // java/lang/IllegalStateException
  #28 = Class              #44            // java/lang/Throwable
  #29 = Utf8               Exceptions
  #30 = Class              #45            // java/lang/Exception
  #31 = Utf8               main
  #32 = Utf8               ([Ljava/lang/String;)V
  #33 = Utf8               args
  #34 = Utf8               [Ljava/lang/String;
  #35 = Utf8               SourceFile
  #36 = Utf8               ClassFile.java
  #37 = NameAndType        #13:#14        // "<init>":()V
  #38 = NameAndType        #11:#12        // k:I
  #39 = Utf8               java/lang/IllegalStateException
  #40 = NameAndType        #46:#14        // printStackTrace:()V
  #41 = Utf8               com/ikang/JVM/classfile/ClassFile
  #42 = Utf8               java/lang/Object
  #43 = Utf8               2222222
  #44 = Utf8               java/lang/Throwable
  #45 = Utf8               java/lang/Exception
  #46 = Utf8               printStackTrace
{

  我们发现,这样能更加直观的查看Class的信息,左边表示第一个常量,右边是指向的常量索引或者具体的常量值,我们分析前四项:
在这里插入图片描述
  从上面的数据可以看出,我们在前面把前四项的常量都计算了出来,并且与我们的就算结果一致。后面的分析我们会继续和javap的结果做对比。
  查看右边是具体的常量值,会发现其中有一部分常量是咱们没见过的,比如"< init > " : ( ) V 、LineNumberTable、LocalVariableTable等常量。实际上它们会被后面的字段表、方法表、属性表所使用到,他们用来描述无法使用固定字节表达的内容,比如描述方法的返回值、有几个参数、每个参数类型等。因为Java中的 “类” 是无穷无尽的, 无法通过简单的无符号字节来描述一个方法用到了什么类, 因此在描述方法的这些信息时, 需要引用常量表中的符号引用进行表达。

2.6 Class访问标记(access_flag)

  u2  access_flags;  //Class的访问标记(类访问修饰符)

  在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问权限和属性,例如:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
  类访问和属性修饰符标志表

表示名称 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final类型,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDB1.2之后发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真。
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志为真,其它类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

2.6.1 案例

  我们使用javap的帮助,可以快速找到常量池的每一位常量项占用的字节,这样就能快速找到常量池后面的访问标志。通过javap可知,最后一项常量是printStackTrace,那么我们可以轻易找到他所在的字节,如下图:
在这里插入图片描述
  然后尝试计算access_flag的值,该测试类为public,因此ACC_PUBLIC为真,加上使用的是JDK1.8编译,那么ACC_SUPER一定为真,其他标志则为假。综合起来access_flag应该为:0x0001+0x0020=0x0021。
  下一项u2就是access_flag,向后取两个字节来进行验证:
在这里插入图片描述
  发现确实是0x0021,说明咱们计算正确

2.7 当前类索引、父类索引、接口索引集合

  u2  this_class;  //表示当前类的引用
  u2  super_class;  //表示父类的引用
  u2  interfaces_count;  //接口数量
  u2  interfaces[interfaces_count];  //接口索引集合

  类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。他们各自指向常量池中一个CONSTANT_Class_info类型的类描述符常量,通过CONSTANT_Class_info的索引可以定位到内部的CONSTANT_utf8_info常量中的全限定类名字符串。
  接口索引数组用来描述这个类实现了哪些接口,这些被实现的接口将按implents(如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。如果该类没有实现任何接口,则interfaces_count为0,并且后面的interfaces集合不占用字节

2.7.1 案例

2.7.1.1 当前类和父类索引

  继续向后四个字节:
在这里插入图片描述
  我们找到这两个u2类型的引用,分别是0x0005和0x0006,他们的十进制值为5和6,表示指向常量池中的第5和6个常量项
  使用javap,查看查看常量池中的第5和6个常量验证:
在这里插入图片描述
  确实是CONSTANT_Class_info类型,继续查看CONSTANT_Class_info的索引指向的第41和42个常量:
在这里插入图片描述
  确实是CONSTANT_utf8_info字符串类型,后面就能找到本类和父类的全限定类名了,说明我们计算正确。

2.7.1.2 接口个数、索引集合

  向后走两位,查看实现的接口个数interfaces_count信息:
在这里插入图片描述
  interfaces_count接口计数为0x0000,即表示不继承接口,那么后面的interfaces集合就字段不占用字节了。
  到此,当前类索引、父类索引、接口索引集合,结束。

2.8 字段表集合(field_info)

u2  fields_count;  //此类的字段表中的字段数量
field_info  fields[fields_count];  //一个类会可以有多个字段,字段表

  字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。不会列出从父类或者父接口继承来的字段,但有可能列出原本Java代码没有的字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  field info(字段表) 的结构如下:

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

access_flags: 字段的作用域(public,private,protected修饰符),是实例变量还是类变量(static修饰符),可否被序列化(transient修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 只有一个
name_index:对常量池的引用,CONSTANT_utf8_info,表示的字段的名称;只有一个
descriptor_index: 对常量池的引用,CONSTANT_utf8_info,表示字段和方法的描述符;只有一个
attributes_count: 一个字段还会拥有一些额外的属性,表示attributes_count 存放属性的个数;只有一个
attributes[attributes_count]: 存放具体属性具体内容集合。有attributes_count个

  在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
  额外的属性表示一个字段还可能拥有一些属性, 用于存储更多的额外信息, 比如初始化值、一些注释信息等。属性个数存放在attributes_count 中, 属性具体内容存放于attributes数组。常量数据比如“public static finall”类型的字段,就有一个ConstantValue的额外属性。

  字段访问和属性标识表:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段为public
ACC_PRIVATE 0x0002 字段为private
ACC_PROTECTED 0x0004 字段为protected
ACC_STATIC 0x0008 字段为static
ACC_FINAL 0x0010 字段为final
ACC_VOLATILE 0x0040 字段为volatile
ACC_TRANSIENT 0x0080 字段为transient
ACC_SYNTHETIC 0x1000 字段由编译器自动产生的
ACC_ENUM 0x4000 字段是enum

attribute属性结构(在属性表部分会深入讲解):

以常量属性为例,常量属性的结构为:
ConstantValue_attribute {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 constantvalue_index;
}

  attribute_name_index 为2 字节整数, 指向常量池的CONSTANT_Utf8_info类型, 并且这个字符串为“ ConstantValue" 。该属性是所有类型的字段都有的。
  attribute_length由4个字节组成, 表示这个属性的剩余长度为多少。对常量属性而言, 这个值恒定为2,表示从Ox00000002 之后的两个字节为属性的全部内容。该属性是所有类型的字段都有的。
  constantvalue_ index 表示属性值,但值并不直接出现在属性中,而是指向常量池引用,即存放在常量池中,不同类型的属性会和不同的常量池类型匹配。

2.8.1 案例

2.8.1.1 fields_count

  向后走两个字节,查看fields_count:
在这里插入图片描述
  0x0002,即有两个字段,可以猜测是常量J和普通对象变量k

2.8.1.2 第一个字段

  下面来看第一个字段,先取前八个字节:
在这里插入图片描述
  八个字节,两个一组,表示字段作用域、对常量池的引用–字段的名称、对常量池的引用–字段和方法的描述符、额外属性。
  access_flags表示字段作用域为0x19,查询字段访问标志表,可知,该字段作用域为:ACC_PUBLIC+ACC_STATIC+ACC_FINAL,即public static final ,可以猜到该字段就是类中的“J”字段。
  name_index表示对常量池的引用(字段的名称),值分别0x0007,即第七个常量
  descriptor_index表示对常量池的引用(字段和方法的描述符),0x0008,即第八个常量。
  我们在 javap命令下的常量池中查找:
在这里插入图片描述
  看到值为“J”,刚好是我们类中常量的字段的名称。字段和方法的描述符为Ljava/lang/String;,即String类型,同样符合我们的预期。
  attributes_count表示额外属性个数,值为0x0001,即1个。那么后面的第五个属性就是描述该属性的结构,由于是常量属性,格式会占用8个字节。
  attributes描述额外属性的具体内容集合,该例子中只有一个数据,如下8字节:
在这里插入图片描述
  前两个字节为attribute_name_index,指向常量池,0x09即第九个常量,常量字段值固定为“ConstantValue”,我们通过javap验证:
在这里插入图片描述
  发现确实如此。
  后4个字节为attribute_length**, 表示这个属性的剩余长度为多少,常量固定为2,从上面可以看到确实是2;表示从Ox00000002 之后的两个字节为属性的全部内容。
  最后取两个字节表示constantvalue_ index,即属性值引用,同样指向常量池,0x0a即第10个常量,我们通过javap验证:
在这里插入图片描述
  第10项表示该常量是String类型,并且具体值又指向了第43个常量,我们找到它:
在这里插入图片描述
  这里就是存的具体值了,确实为“2222222”,和代码中一致。

2.8.1.3 第二个字段

  下面来看第二个字段,同样先看前四个属性:
在这里插入图片描述
  access_flag访问标志为 0x0002,查询字段access表,可知该字段为ACC_PRIVATE,即private。
  name_index字段名引用为0x000b,即指向常量池第11个字段:
在这里插入图片描述
  我们看到字段名确实是k。
  descriptor_index字段描述符引用为0x000c,即指向常量池第12个字段:
在这里插入图片描述
  可以看到,由于是基本类型,并且是int,因此只有一个I,符合预测。
  attributes_count表示额外的属性,这里是0x0000,即没有额外属性。那么后面的额外属性集合自然不占用字节了。
  到此字段表分析结束。

2.9 方法表集合(method_info)

  u2  methods_count;  //此类的方法表中的方法数量
  method_info  methods[methods_count];  //一个类可以有个多个方法,方法表

  Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标记、名称索引、描述符索引、属性表集合几项。而方法中的具体代码则是存放在属性表中了。类似于字段表,子类不会记录父类未重写的方法,同上编译器可能自己加方法,比如< init > 、< cinit >。
method_info(方法表的) 结构如下:

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

  access_flag表示访问标记,因为volatile修饰符和transient修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized、native、abstract等关键字修饰方法,所以也就多了这些关键字对应的标志。access_flag 取值表如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICTFP 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

  name_index 表示方法的名称, 它是一个指向常量池的索引。
  descriptor_ index为方法描述符, 它也是指向常量池的索引, 是一个字符串, 用以表示方法的签名(参数、返回值等)。关于方法描述符,在最开始已经介绍了。
  和字段表类似, 方法也可以附带若干个属性, 用于描述一些额外信息, 比如方法字节码等, attributes_count 表示该方法中属性的数量, 紧接着, 就是attributes_count个属性的描述。
  attribute_info通用格式为:

attribute_info {
  u2   attribute_name_index;
  u4   attribute_length;
  u1   info[attribute_length];
}

  其中, attribute_name_ index 表示当前attribute的名称, attribute_length为当前attribute的剩余长度, 紧接着就是attribute_length个字节的byte 数组。常见的 attribute在属性表会有详细介绍。

2.9.1 案例

2.9.1.1 methods_count

  接着上面的案例,先看methods_count
在这里插入图片描述
  这说明有四个方法,那么肯定有些方法是编译时自动生成的

2.9.1.2 第一个方法

  然后,看第一个方法的method_info的内部前四个字段:
在这里插入图片描述
  access_flag为0x0001,查找方的access_flag 取值表可知,该方法使用了ACC_PUBLIC,即pubilc方法。然后没有了修饰符。
  name_index为0x000d,即指向常量池第13个 常量:
在这里插入图片描述
  可以看到该方法名字叫 < init >
  descriptor_ index为0x000e,即指向常量池第14个常量:
在这里插入图片描述
  可以看到该方法描述符号为 ()V
  attributes_count 值为0x0001,表示具有一个额外属性,那么可以继续向下找。

  向后六个字节,查看attribute_info
在这里插入图片描述
attribute_name_index为0x000f,即指向常量池第15个常量:
在这里插入图片描述
  说明该方法具有名为code的属性!
  attribute_length为0x0000002f,即长度为47,这就很长了,并且不同的属性具有自己的格式,因此具体的分析,我们在属性表集合中介绍!

2.10 属性表集合

  u2  attributes_count;  //此类的属性表中的属性数量
  attribute_info  attributes[attributes_count];  //属性表集合

  属性表用于class文件格式中的ClassFile,field_info,method_info和Code_attribute结构,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
  对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度去说明属性值所占用的位数即可。
  属性表通用结构

attribute_info {
  u2   attribute_name_index;
  u4   attribute_length;
  u1   info[attribute_length];
}

  对于任意属性, attribute_name_index必须是对当前class文件的常量池的有效16位无符号索引。 常量池在该索引处的成员必须是CONSTANT_Utf8_info 结构, 用以表示当前属性的名字。 attribute_length项的值给出了跟随其后的信息字节的 长度, 这个长度不包括attribute_name_index 和 attribute_length项的6个字节。

2.10.1 预定义属性

  《java虚拟机规范 JavaSE8》中预定义23项虚拟机实现应当能识别的属性:

属性 可用位置 含义
SourceFile ClassFile 记录源文件名称
InnerClasses ClassFile 内部类列表
EnclosingMethod ClassFile 仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法
SourceDebugExtension ClassFile JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。
BootstrapMethods ClassFile JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符
ConstantValue field_info final关键字定义的常量值
Code method_info Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令)
Exceptions method_info 方法声明的异常
RuntimeVisibleAnnotations ClassFile, field_info, method_info JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。
RuntimeInvisibleAnnotations ClassFile, field_info, method_info JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。
RuntimeVisibleParameterAnnotations method_info JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。
RuntimeInvisibleParameterAnnotations method_info JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。
AnnotationDefault method_info JDK1.5中新增的属性,用于记录注解类元素的默认值
MethodParameters method_info 52.0
Synthetic ClassFile, field_info, method_info 标识方法或字段为编译器自动产生的
Deprecated ClassFile, field_info, method_info 被声明为deprecated的方法和字段
Signature ClassFile, field_info, method_info JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
RuntimeVisibleAnnotations ClassFile, field_info, method_info JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。
RuntimeInvisibleAnnotations ClassFile, field_info, method_info JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。
LineNumberTable Code Java源码的行号与字节码指令的对应关系
LocalVariableTable Code 方法的局部变量描述
LocalVariableTypeTable Code JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
StackMapTable Code JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
MethodParameters method_info JDK1.8中新加的属性,用于标识方法参数的名称和访问标志。
RuntimeVisibleTypeAnnotations ClassFile, field_info, method_info, Code JDK1.8中新加的属性,在运行时可见的注释,用于泛型类型,指令等。
RuntimeInvisibleTypeAnnotations ClassFile, field_info, method_info, Code JDK1.8中新加的属性,在编译时可见的注释,用于泛型类型,指令等。

  这里主讲一些常见的属性。

2.10.2 Code属性——方法体

  Java方法体里面的代码经过Javac编译之后,最终变为字节码指令存储在Code属性内,Code属性出现在在method_info结构的attributes表中,但在接口或抽象类中就不存在Code属性(JDK1.8可以出现了)。一个方法中的Code属性值有一个。
  在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
  在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于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];
}

  Code属性的第一个字段attribute_name_index指定了该属性的名称,它是一个指向常量池的索引, 指向的类型为CONSTANT_Utf8_info, 对于Code 属性来说,该值恒为“Code"。
  attribute_length指定了Code属性的长度,该长度不包括前6个字节,即表示剩余长度。
  在方法执行过程中,操作数栈可能不停地变化,在整个执行过程中,操作数栈存在一个最大深度,该深度由max_stack表示。
  在方法执行过程中,局部变量表也可能会不断变化。在整个执行过程中局部变量表的最值由max_locals表示, 它们都是2字节的无符号整数。也包括调用此方法时用于传递参数的局部变量。
  在max_locals 之后,就是作为方法的最重要部分—字节码。它由code_length和code[code_length]两部分组成, code_length 表示字节码的字节数,为4字节无符号整数,必须大于0;code[code length]为byte数组,即存放的代码的实际字节内容本身。
  在字节码之后,存放该方法的异常处理表。异常处理表告诉一个方法该如何处理字节码中可能抛出的异常。异常处理表亦由两部分组成:表项数量和内容。
  exception_table_length表示异常表的表项数量,可以为0;
  exception_table[exception_table_length]结构为异常表的数据。
  异常表中每一个数据由4部分组成,分别是start_pc、end_pc、handler_pc和catch_type。这4项表示从方法字节码的start_pc偏移量开始(包括)到end_pc 偏移量为止(不包括)的这段代码中,如果遇到了catch_type所指定的异常, 那么代码就跳转到handler_pc的位置执行,handler_pc即一个异常处理器的起点。
  在这4项中, start_pc、end_pc和handlerpc 都是字节码的编译量, 也就是在code[code_length]中的位置, 而catch_type为指向常量池的索引,它指向一个CONSTANT_Class_info 类,表示需要处理的异常类型。如果catch_type值为0,那么将会在所有异常抛出时都调用这个异常处理器,这被用于实现finally语句。
  至此, Code属性的主体部分已经介绍完毕, 但是Code属性中还可能包含更多信息, 比如行号表、局部变量表等。这些信息都以attribute属性的形式内嵌在Code属性中, 即除了字段、方法和类文件可以内嵌属性外,属性本身也可以内嵌其他属性。attributes_count表示Code属性的属性数量,attributes表示Code属性包含的属性。

2.10.3 LineNumberTable属性——记录行号

  位于Code属性中,描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。主要是如果抛出异常时,编译器会显示行号,比如调试程序时展示的行号,就是这个属性的作用。
  Code属性表中,LineNumberTable可以属性可以按照任意顺序出现。
  在Code属性 attributes表中,可以有不止一个LineNumberTable属性对应于源文件中的同一行。也就是说,多个LineNumberTable属性可以合起来表示源文件中的某行代码,属性与源文件的代码行之间不必有一一对应的关系。

  LineNumberTable属性的格式如下:

LineNumberTable_ attribute {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 line_number_table_length;
  {   u2   start_pc;
      u2   line_number;
  }line_number_table[line_number_table_length];
}

  其中, attribute_name_index为指向常量池的索引, 在LineNumberTable 属性中, 该值为"LineNumberTable", attribute_length为4 字节无符号整数, 表示属性的长度(不含前6个字节),line_number_table_length 表明了表项有多少条记录。
  line_number_table为表的实际内容,它包含line_number_table_length 个<start_pc, line_number>元组,其中,start_pc为字节码偏移量,必须是code数组的一个索引, line_number 为对应的源码的行号。

2.10.4 LocalVariableTable属性——保存局部变量和参数

  描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。用处在于当别人使用这个方法是能够显示出方法定义的参数名。
  它也不是运行时必需的属性,但默认会生成到Class文件之中。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失。
  LocalVariableTable属性结构如下:

LocalVariableTable_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   local_variable_table_length;
  {   u2   start_pc;
      u2   length;
      u2   name_index;
      u2   descriptor_index;
      u2   index;
  } local_variable_table[local_variable_table_length];
}

  其中, attribute_name_index为当前属性的名字, 它是指向常量池的索引。对局部变量表而言, 该值为“ LocalVariableTable", attribute_length 为属性的长度,local_variable_table_length为局部变量表表项条目。
  局部变量表的每一条记录由以下几个部分组成:

  1. start_pc、length:start_pc表示当前局部变量的开始位置,从0开始,length表示范围覆盖长度,两者结合就是这个局部变量在字节码中的作用域范围。
  2. name_ index: 局部变量的名称, 这是一个指向常量池的索引,为CONSTANT_Utf8_info类型。
  3. descriptor_index: 局部变量的类型描述, 指向常量池的索引。使用和字段描述符一样的方式描述局部变量,为CONSTANT_Utf8_info类型。
  4. index,局部变量在当前帧栈的局部变量表中的槽位(solt)。当变量数据类型是64位时(long 和double), 它们会占据局部变量表中的两个槽位,位置为index和index+1。

  在JDK 1.5引入泛型后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦出掉,描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable。

2.10.5 StackMapTable属性——加快字节码校验

  JDK 1.6 以后的类文件,件, 每个方法的Code 属性还可能含有一个StackMapTable 的属性结构。这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器,加快字节码校验。
  StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。
  该属性不包含运行时所需的信息,仅仅作为Class文件的类型检验。
  StackMapTable属性结构如下:

StackMapTable_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   number_of_entries;
  stack_map_frame entries[number_of_entries];
}

  其中,attribute_name——index为常量池索引, 恒为“ Stack.MapTable", attribute_length为该属性的长度, number_of_entries为栈映射帧的数量, 最后的stack_map_frame entries则为具体的内容,每一项为一个stack_map_ frame结构。

2.10.6 Exceptions属性——列举异常

  列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。Exceptions与Code 属性中的异常表不同。Exceptions 属性表示一个方法可能抛出的异常,通常是由方法的throws 关键字指定的。而Code 属性中的异常表,则是异常处理机制,由try-catch语句生成。
  Exceptions属性结构如下:

Exceptions_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   number_of_exceptions;
  u2   exception_index_table[number_of_exceptions];
}

  Exceptions 属性表中, attribute_name_index 指定了属性的名称, 它为指向常量池的索引,恒为“Exceptions",attribute_lengt表示属性长度, number_of_exceptions表示表项数量即可能抛出的异常个数,最后exception_index_table项罗列了所有的异常,每一项为指向常量池的索引,对应的常量为CONSTANT_Class_info,表示为一个异常类型。

2.10.7 SourseFile属性——记录来源

  SourseFile属性ClassFile,记录生成这个Class文件的源码文件名称,抛出异常时能够显示错误代码所属的文件名。
  SourseFile属性结构如下:

SourceFile_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   sourcefile_index;
}

  SourseFile属性表中, attribute_name_index 指定了属性的名称, 它为指向常量池的索引,恒为“SourseFile", attribute_length表示属性长度,对SourseFile属性来说恒为2,sourcefile_index表示源代码文件名, 它是为指向常量池的索引,对应的常量为CONSTANT_Uft8_info类型。

2.10.8 InnerClass属性——记录内部类

  InnerClass属性属性ClassFile,用于记录内部类与宿主类之间的关联。
  InnerClass属性结构如下:

InnerClasses_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   number_of_classes;
  {   u2   inner_class_info_index;
      u2   outer_class_info_index;
      u2   inner_name_index;
      u2   inner_class_access_flags;
  } classes[number_of_classes];
}

  其中, attribute_name_index表示属性名称,为指向常量池的索引,这里恒为“TnnerClasses ” 。attribute_ length 为属性长度,number_of_classes 表示内部类的个数。classes[number_ of_ classes]为描述内部类的表格,每一条内部类记录包含4个字段,其中,inner_class_ info_ index为指向常量池的指针, 它指向一个CONSTANT_ Class_info, 表示内部类的类型。outer_class_ info_index表示外部类类型,也是常量池的索引。inner_name_ index 表示内部类的名称, 指向常量池中的CONSTANT_ Utf8_info项。最后的inner_class_access_flags为内部类的访问标识符, 用于指示static、public等属性。
  内部内标识符表如下:

标记名 含义
ACC_PUBLIC 0x0001 public
ACC_PRIVATE 0x0002 私有类
ACC_PROTECTED 0x0004 受保护的类
ACC_STATIC 0x0008 静态内部类
ACC_FINAL 0x0010 fmal类
ACC_INTERFACE 0x0200 接口
ACC_ABSTRACT 0x0400 抽象类
ACC_SYNTHETIC 0x1000 编译器产生的,而非代码产生的类
ACC_ANNOTATION 0x2000 注释
ACC_ENUM 0x4000 枚举

2.10.9 ConstantValue属性——static(常量)标识

  ConstantValue属性位于filed_info属性中,通知虚拟机自动为静态变量赋值,只有被static字符修饰的变量(类变量)才可以有这项属性。如果非静态字段拥有了这一属性,也该属性会被虚拟机所忽略。
  对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。但是不同虚拟机有不同的实现。
  目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,即编译的时候;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化,即类加载的时候。
  ConstantValue属性结构如下:

ConstantValue_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   constantvalue_index;
}

  其中, attribute_name_index表示属性名称,为指向常量池的索引,这里恒为“ConstantValue_attribute ” 。attribute_length表示后面还有几个字节,这里固定为2。constantvalue_index代表了常量池中一个字面常量的引用,根据字段类型不同,字面量可以是CONSTANT_Long_info、 CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_lnteger_info、CONSTANT_String_info常量中的一种。

2.10.11 Signature属性

  一个可选的定长属性,可以出现于ClassFile, field_info, method_info结构的属性表中。
  任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。
  Signature属性结构如下:

Signature_attribute {
  u2   attribute_name_index; //指向常量池的名称引用,固定为“Signature”
  u4   attribute_length; // 固定为2
  u2   signature_index; //指向常量池的类签名、方法类型前面、字段类型签名引用
}

2.10.12 Deprecated属性——废弃

  标志类型的布尔属性,Deprecated表示类、方法、字段不再推荐使用,使用注解表示为@deprecated

2.10.13 Synthetic属性——编译器添加

  标志类型的布尔属性,Synthetic属性用在ClassFile, field_info, method_info中,表示此字段或方法或类是由编译器自行添加的。

对于其他属性,可以查看官方文档:The Java® Virtual Machine Specification——Java SE 8 Edition

2.10.14 测试

2.10.14.1 Code属性

  我们接着上面的Class文件向下看,我们知道< init >方法中有一个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];
}

首先看前两个属性(其实在方法表处已经分析出来了):
在这里插入图片描述
  attribute_name_index为0x000f,即指向常量池第15个常量。
在这里插入图片描述
  该属性名为Code!
  attribute_length 表示Code属性剩余长度 ,0x0000002f,即47个字节。

  接着看后两个属性:
在这里插入图片描述
  max_stack为1,max_locals也为1,即方法内部没调用其他方法。

  接着看后两个属性,即字节码部分:
在这里插入图片描述
  code_length 表示字节码的长度,这里是5, code[code length]为byte数组, 为字节码内容本身,长度为5个字节:
在这里插入图片描述
  继续向下,该方法实际上是用于对象创建的方法,实际上是没有异常处理表的,因此长度自然为0,下面的数组部分分字节码为0,如下图:
在这里插入图片描述
  exception_table_length为0x0000,即0,那么就不会出现
exception_table
,继续向下两个字节,表示Code属性的属性数量,即attributes_count:
在这里插入图片描述
  attributes_count值为0x0002,即有Code属性内部有两个属性,下面我们来看看是哪两个属性!

2.10.14.2 Code内部第一个属性

  老样子,向后走六个字节:
在这里插入图片描述
  Code属性内部的第一个属性的attribute_name_index为0x0010,即第16个常量;
  Code属性内部的第一个属性的attribute_length为0x00000006,即后接6字节;
  通过查找javap的常量池可知,第16个常量属性就是LineNumberTable属性。
在这里插入图片描述
我们知道LineNumberTable属性结构为:

LineNumberTable_ attribute {
  u2 attribute_name_index;
  u4 attribute_length;
  u2 line_number_table_length;
  {   u2   start_pc;
      u2   line_number;
  }line_number_table[line_number_table_length];
}

  继续,向下走六个字节,查看属性后面部分:
在这里插入图片描述
  line_number_table_length为0x0001,即1,即line_number_table表的实际内容为1个数据。下面进入这个数据内部:
  start_pc为0x0000,即字节码偏移量为0。
  line_number为0x0004,即对应的行号为4(反编译Class就会出现无参构造器,对应行号就是4)。
  **到此,Code属性内部的第一个属性LineNumberTable结束。

2.10.14.3 Code内部第二个属性

老样子,第二个属性先向后取六位字节:
在这里插入图片描述
  Code属性内部的第二个属性的attribute_name_index为0x0011,即第17个常量;
  Code属性内部的第二个属性的attribute_length为0x0000000c,即后接12字节;
  通过查找javap的常量池可知,第17个常量属性就是LocalVariableTable属性。
在这里插入图片描述
  我们知道LocalVariableTable属性结构为:

LocalVariableTable_attribute {
  u2   attribute_name_index;
  u4   attribute_length;
  u2   local_variable_table_length;
  {   u2   start_pc;
      u2   length;
      u2   name_index;
      u2   descriptor_index;
      u2   index;
  } local_variable_table[local_variable_table_length];
}

  向后再取两个字节:
在这里插入图片描述
  local_variable_table_length为0x0001,即1,即局部变量表表项条目为1条,后面一条数据,刚好十个占据字节,进入local_variable_table的数据:
在这里插入图片描述
  start_pc为0x0000,即0,局部变量开始位置为0,即占用0位solt,实际上这个局部变量就是this(在运行时栈结构部分会有讲解)。
  length为0x0005,即范围覆盖长度为5。
  name_index为0x0012,即第18个常量。
在这里插入图片描述
  可以看到这个局部变量名叫this,这个变量是虚拟机为我们加到实例方法中的,可以直接使用,但是对于静态方法,却没有这个局部变量。
  descriptor_index为0x0013,即第19个常量。
在这里插入图片描述
  表示局部变量的类型描述,可以看到,该this变量的类型就是该类的类型。
  index为0x0000,即0,即this变量,所占用的solt槽位是0。
  到此,Code的两个额外属性寻找完毕,刚好47个字节,该< init >方法的额外属性Code寻找完毕,该方法寻找完毕!
在这里插入图片描述

2.10.14.4 验证

  下面来验证我们上面的分析:
  我们的javap -v ClassFile.class 指令实际上也编译出了方法表,我们找到init方法:
在这里插入图片描述
  可以看到,我们通过计算class文件得到的信息和javap指令反编译得到的信息是一致的!

  对于后面的三个方法,就是< cinit >、setK、getK,不过是依葫芦画瓢,找到之后,得出结果,然后和javap的结果进行对比,看是不是正确的,在此不再赘述。
  在最后,实际上最后8个字节表示的是SourseFile属性,你们可以自己看看到底是不是!
在这里插入图片描述

3 总结

  到此,我已经带领大家把Class文件从头到尾的大概梳理了一遍,以后遇到更加复杂的类,实际上可以直接使用javap指令查看反解析出的数据,那样更加方便。关于javap指令和字节码指令的查看,这里没有讲解,可以参考官网:Verification of class Files,后面我也会处相应的中文教程。我们知道了Class文件的结构组成,对于我们后续的JVM深入学习是很有帮助的,比如编译器的学习(这篇文章只是讲解编译之后的数据结构),我们以后可以自己实现一个Java编译器!

如果有什么不懂或者需要交流,各位可以留言。另外,希望收藏、关注一下,我将不间断更新Java各种教程!

发布了58 篇原创文章 · 获赞 106 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/105310047