JVM字节码文件概述

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 字节码文件概述

字节码文件概述


字节码文件的跨平台性

Java语言:跨平台的语言

  • 当Java源代码成功编译字节码后,如果想在不同的平台上面运行,则无需再次编译
  • 这个优势目前来说已经不再吸引人,因为Python、PHP、Ruby、Lisp等有强大的解释器
  • 跨平台已经快成为一门语言的必选特性


Java虚拟机:跨语言的平台

Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件所关联,无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以这Java虚拟机上执行。

1654689914136.png

想要让一个Java程序正确的运行在JVM中,Java源码就必须要被编译为符合JVM规范的字节码。


  • 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件
  • javac是一种能够将Java源码编译为字节码的前端编译器
  • javac编译器这将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码

1654690356587.png1654690356587.png

Java的编译

1654690642709.png

  1. 前端编译


   Java源代码的编译结果是字节码,那么肯定要有一种能够将Java源代码编译为字码,

   承担这个责任的就是一配置在path环境变量中的javac编译器。javac是一种能够

   将Java源码编译为字节码的前端编译器


   优点:


  1. 许多Java语法新特性(泛型、内部类等),是靠前端编译器实现的,而不是依赖虚   拟机。
  2. 编译成的Class文件可以直接给JVM解释器解释执行,省去编译时间,加快启动速度。


缺点:


  1. 对代码运行效率几乎没有任何优化措施。
  2. 解释执行效率较低,所以需要结合下面的JIT编译。


2.后端编译/JIT编译


  1. 通过Java虚拟机(JVM)内置的即时编译器(Just In Time Compiler,JIT编译器);
  2. 在运行时把Class文件字节码编译成本地机器码的过程。


优点:


  1. 通过在运行时收集监控信息,把"热点代码"(Hot Spot Code)编译成与本地平台相关的机器码,并进行各种层次的优化。
  2. 可以大大提高执行效率。


缺点:


  1. 收集监控信息影响程序运行。
  2. 编译过程占用程序运行时间。
  3. 编译机器码占用内存。


3.静态提前编译(AOT)


  程序运行前,直接把Java源码文件编译成本地机器码的过程。

优点:


  1. 编译不占用运行时间,可以做一些较耗时的优化,并可加快程序启动。
  2. 把编译的本地机器码保存磁盘,不占用内存,并可多次使用。


缺点:


  1. 因为Java语言的动态性(如反射)带来了额外的复杂性,影响了静态编译代码的质量,一般静态编译不如JIT编译的质量,这种方式用得比较少。


目前Java体系中主要还是采用前端编译+JIT编译的方式


运作过程:


  1. 首先通过前端编译把符合Java语言规范的程序代码转化为满足JVM规范所要求Class格式。
  2. 然后程序启动时Class格式文件发挥作用,解释执行,省去编译时间,加快启动速度。
  3. 针对Class解释执行效率低的问题,在运行中收集性能监控信息,得知"热点代码"。
  4. JIT逐渐发挥作用,把越来越多的热点代码"编译优化成本地代码,提高执行效率。


透过字节码指令看代码细节

面试题


  1. 类文件结构有几个部分?
  2. 知道字节码吗?字节码都有哪些?Integer x = 5; int y = 5; 比较 x == y 都经历哪些步骤?

1654691246872.png

首先在声明Integer x的时候会调用Integer.valueOf方法,首先看下这个方法

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
}

如果所传入的值是-128~127之间,就只用静态内部类的cache数组,否则就new一个新的对象。


这也表明为什么 Integer x = 128; Integer y = 128; x != y 的原因。


之后可以看到调用了 Integer.intValue 进行自动拆箱操作,使得 x 变成 int 类型进行比较。


虚拟机的基石:Class文件


  • 字节码文件里是什么?源代码经过编译器编译之后便生成一个字节码文件,字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码
  • 什么是字节码指令?Java虚拟机的指令,由一个字节长度的、代表着某种特定操作含义的操作码(opcode),以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。


    比如 aload_1 (操作码) 、aload 4 (操作码 + 操作数),因为aload只有0、1、2、3

    所以如果想继续扩充,就要用到操作数。
1654691318525.png

Class文件结构


  • Class类的本质由于一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件形式存在。Class文件是一组以8位字节为基础单位的二进制流


  • Class文件格式Class的结构不像 XML 等描述语言,由于它没有任何分割符号,所以这其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义、长度多少、先后顺序,都不允许改变。Class文件格式采用一种类似于C语言结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数


  • 无符号数属于基本数据类型,以 u1 、u2 、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表,由于表没有固定长度,所以通常会其前面加上个数说明。
  • Class文件结构概述Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是基本结构和框架是非常稳定的。结构如下:


  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

1654691368130.png

类型 名称 说明 长度 数量
u4 magic 魔数,识别Class文件格式 4个字节 1
u2 minor_version 副版本号(小版本) 2个字节 1
u2 major_version 主版本号(大版本) 2个字节 1
u2 constant_pool_count 常量池计数器 2个字节 1
cp_info constant_pool 常量池表 n个字节 constant_pool_count-1
u2 access_flags 访问标识 2个字节 1
u2 this_class 类索引 2个字节 1
u2 super_class 父类索引 2个字节 1
u2 interfaces_count 接口计数器 2个字节 1
u2 interfaces 接口索引集合 2个字节 interfaces_count
u2 fields_count 字段计数器 2个字节 1
field_info fields 字段表 n个字节 fields_count
u2 methods_count 方法计数器 2个字节 1
method_info methods 方法表 n个字节 methods_count
u2 attributes_count 属性计数器 2个字节 1
attribute_info attributes 属性表 n个字节 attributes_count

字节码文件解析

首先我们创建一个简单的源码:

public class Demo {
    private int num = 1;
    public int add() {
        num = num + 2;
        return num;
    }
}

通过 javac 命令编译后可以看到Class文件内容:

public class Demo {
    private int num = 1;
    public Demo() {
    }
    public int add() {
        this.num += 2;
        return this.num;
    }
}

为什么编译以后增加了无参构造器,以及this关键字,我们一点点分析。


之后我们使用Notepad++,需要安装一个HEX-Editor插件来打开这个Class文件就可以看到下图内容。

1654691437963.png

这里就直接使用 excel 来清晰解释具体内容。

1654691463446.png

魔数:Class文件的标志

  • 每个Class文件开头的4个字节的无符号整数成为魔数(Magic Number)
  • 它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。
  • 魔数值固定为0xCAFEBABE
  • 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出ClassFormatError错误。
  • 使用魔数而不是扩展名来识别主要是基于安全方面考虑,因为文件扩展名是可以随意改变的。


Class文件版本号

  • 紧接着魔数的 4 个字节是 Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。
  • 它们共同构成了Class文件的格式版本号。譬如某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号就是 M.m。
  • 版本号和Java编译器的对应关系表如下:


主版本(十进制) 副版本(十进制) 编译器版本
45 3 1.1
46 0 1.2
47 0 1.3
48 0 1.4
49 0 1.5
50 0 1.6
51 0 1.7
52 0 1.8
53 0 1.9
54 0 1.10
55 0 1.11
  • Java的版本号是从45开始的,JDK 1.1 之后的每个JDK大版本发布主版本号向上加1。
  • 不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行低版本编译器编译的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。


常量池:存放所有常量

  • 常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析有着至关重要的作用。
  • 随着Java虚拟机的不断发展,常量池的内容也日渐丰富。可以说,常量池是整个Class文件的基石。
  • 在版本号之后,紧跟着的就是常量池的数量,以及若干个常量池表项。
  • 常量池中常量的数量是不固定的,所以需要一个u2类型的无符号数,代表着常量池容量计数器(constant_pool_count),与Java语言习惯不一样的是,容量计数器是从1开始而不是0。
  • 常量池表项中,用于存放编译期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。


常量池计数器

  • 由于常量池的数量不固定,所以需要放置两个字节来表示常量池容量计数器。
  • 常量池计数器(u2类型):从1开始,表示常量池中有多少项常量。即constant_pool_count = 1表示常量池中有0个常量池。
  • 我们看刚才举例的Demo:其值为0x0016,转换为十进制也就是22。
    1654691550147.png
    但是实际中只有21项常量,范围是1-21。
这里的常量池把第0项空出来了,为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项"的含义,这种情况可以使用索引0来表示。

常量池表

  • constant_pool是一种表及结构,以1 ~ constant_pool_count - 1 为索引。表明后面有多少个常量项。
  • 常量池主要存放放两大类变量:字面量(Literal)符号引用(Symbolic Reference)
  • 它包含了Class文件结构及其子结构中引用的所有字符串常量、类、或接口名、字段名、和其它常量。常量池中的每一项都具有相同特征。第1个字节作为类型标记,用于确定该项的格式,这个字节成为tag byte (标记字节)。


字面量和符号引用


常量池主要存放放两大类变量:字面量(Literal)符号引用(Symbolic Reference)


常量 具体的常量
字面量 文本字符串
声明为final的常量值
符号引用 类和接口的全限定名
字段和名称的描述符
方法的名称和描述符

全限定名


com/test/Demo这个就是类的全限定名,仅仅是把包名的"."替换成了"/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个";"表示全限定名结束。


简单名称


简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法,和num字段的简单名称分别是add和num。


描述符


描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(boolean,byte,char,short,int,float,long,double)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示:


标志符 含义
B 基本数据类型byte
C 基本数据类型char
D 基本数据类型double
F 基本数据类型float
I 基本数据类型int
J 基本数据类型long
S 基本数据类型short
Z 基本数据类型boolean
V 代表void类型
L 对象类型,比如:Ljava/lang/Object;
[ 数组类型,代表一维数组。比如:double[][][] = [[[D
public static void main(String[] args) {
        Object[] arr = new Object[10];
        System.out.println(arr);//[Ljava.lang.Object;@14ae5a5
        Long[][] longs = new Long[10][10];
        System.out.println(longs);//[[Ljava.lang.Long;@7f31245a
        int[][] ints = new int[10][10];
        System.out.println(ints);//[[I@7f31245a
}

需要注意的是,用描述符来描述方法的时候,先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号"()"内。


虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中


  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来一般不会相同。如果有了直接引用,则说明引用的目标必定存在内存之中。
常量类型和结构
类型 标志(或标识) 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

1654691668529.png


从上面的图中可以看到,虽然每一项的结构都不相同,但是它们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。


访问标记(access_flag)

  • 在常量池后,紧接着访问标记,该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等,详细说明如下:


标志名称 标志值 含义
ACC_PUBLIC 0x0001 标志为public类型
ACC_FINAL 0x0010 标志被声明为final,只有类可以设置
ACC_SUPER 0x0020 标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举
  • 类的访问权限通常为 ACC_ 开头的常量。
  • 每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的,比如如果是public final的类,则标记为 ACC_PUBLIC | ACC_FINAL。
  • 使用ACC_SUPER可以让类更准确的定位到父类的方法super.method(),默认都是设置并使用这个标记。


我们可以看到上面的Demo的字节码对应的访问标记是21,也就是对应表格中的 ACC_PUBLIC 和 ACC_SUPER 加起来就等于21。


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

长度 含义
u2 this_class
u2 super_class
u2 interfaces_count
u2 interfaces[interfaces_count]


  • 类索引用于确定这个类的继承关系。
  • 父类索引用于确定这个类的父类全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了Object 之外,所有的Java类都有父类,因此除了Object之外,所有Java类的父类索引都不为0。
  • 接口索引集合就用来描述这个类实现了拿些接口,这些被实现的接口将按implements语句(如果当前类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。


this_class(类索引)


2 字节无符号整数,指向常量池的索引。它提供了类的全限定名,如com/test/Demo。this_class的值必须是常量池中某项的一个有效索引值。常量池在这个索引出的成员必须为constant_class_info类型结构体,该结构表示这个Class文件所定义的类或者接口。


super_class(父类索引)


  • 2字节无符号整数,指向常量池的索引。它提供了当前类的父类全限定名。如果我们没有继承任何类,其默认继承的是Java/lang/Object类。同时,由于Java不支持多继承,所以其父类只有一个。
  • super_class指向的父类不能是final类型。


interfaces


  • 指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。
  • 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的Constant_Class(指向的是接口,并不是类)。


interfaces_count(接口计数器)


interfaces_count 项的值表示当前类或者接口的直接超接口数量。


interfaces[](接口索引集合)


interfaces[]中每个成员的值必须是对常量池表中某项索引的有效索引值,它的长度为interfaces_count。每个成员interfaces[i]必须为constant_class_info结构,其中 0 <= i < interfaces_count。在interfaces[]中,各成员所表示的接口顺序对应的源代码中给定的接口顺序(从左至右)一样,即 interface[0]对应的是源代码中最左边的接口。


字段表集合

  • 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例变量,但是不包括方法内部、代码块内部声明的局部变量。
  • 字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的。只能引用常量池中的常量来描述。
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。


注意:


  • 字段表集合中不会列出从父类或者实现的接口中继承来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来说,如果两个字段的描述符不一致,那字段名重名也是合法的。


fields_count(字段计数器)


fields_count的值表示当前Class文件fields表的成员个数,用2个字节表示。


fields表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。


fields[](字段表)


  • fields表中的每个成员都必须是一个field_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
  • 一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。


  • 作用域
  • 实例变量还是类变量
  • 是否final
  • 是否volatile
  • 是否序列化 transient 修饰
  • 字段数据类型(基本数据类型,对象,数组)
  • 字段名称
  • 字段表结构
类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 字段名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count

字段表访问标识


我们知道,一个字段可以被各种关键字修饰,比如作用域修饰符、static修饰符、final修饰符、volatile修饰符等等。字段访问标志有如下这些:


标志名称 标志值 含义
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_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

字段名索引


根据字段名索引的值,查询常量池中的指定索引项即可。


描述符索引


描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型及无返回值的void都是用大写符来表示,对象使用字符L+全限定名表示。


属性集合


一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count中,属性具体内容存在attributes数组中。

结构为:


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

对于常量属性而言,attribute_length的值恒为2。

1654691815406.png

常量值索引所指向的 #8 其实就是对应int的值。

1654691838496.png

方法表集合

methods:指向常量池索引集合,它完整描述了每个方法的签名。


  • 在字节码文件中,每一个method_info项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符,方法的返回值类型,以及方法的参数信息等。
  • 如果方法不是抽象的或者不是native的,那么字节码就会体现出来。
  • 一方面,methods表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods表有可能会出现由编译器自动添加的方法,最典型的就是编译器产生的方法信息,比如类或者接口初始化方法<clinit>()和实例初始化方法<init>()


methodds_count(方法计数器)


methods_count的值表示当前class文件methods表的成员个数。使用 2 个字节来表示。

methods表中每个成员都是一个method_info结构。


methods[](方法表)


  • methods表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。如果某个method_info结构的access_flags项既没有设置ACC_NATIVE标志和ACC_ABSTRACT标志,那么该结构中也应该包含实现这个方法所用到的Java虚拟机指令。
  • method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法。
  • 方法表的结构实际和字段表是一致的。如下:
类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 字段名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count

属性表集合

方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该Class文件的源文件的名称,以及任何带有RetentionPolicy.CLASS或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试。


此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。


属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。


属性的通用格式


属性表的结构比较灵活,各种不同的属性只要满足以下结构即可:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u1 info attribute_length 属性表

属性类型

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature 类,方法表,字段表 用于支持泛型情况下的方法签名
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 用于存储额外的调试信息
Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的
LocalVariableTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持
RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault 方法表 用于记录注解类元素的默认值
BootstrapMethods 类文件 用于保存invokeddynamic指令引用的引导方式限定符

Code属性


Code属性就是存放方法体里面的代码,像接口或者抽象方法,没有具体的方法体,因此也就不会有Code属性了。


Code属性表的结构:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存续空间
u4 code_length 1 字节码指令的长度
u1 code code_length 存储字节码指令
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_length 异常表
u2 attributes_count 1 属性集合计数器
attribute_info attributes attributes_count 属性集合

LineNumberTable属性


  • LineNumberTable属性是可选变长属性,位于Code结构的属性表
  • LineNumberTable属性是用来描述Java源码行号字节码行号之间的对应关系。
  • start_pc,即字节码行号;line_number,即Java源代码的行号
  • 在Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现,此外,多个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];  
}
类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 line_number_table_length 1 行号表长度
line_number_info line_number_table line_number_table_length 行号表

SourceFile属性

类型 名称 数量 含义
u2 attribute_name_index 1 属性名索引
u4 attribute_length 1 属性长度
u2 sourcefile_index 1 源码文件索引

其长度总是固定的8个字节。

1654691907178.png

我们看之前的Excel最后一位也可以看到对应的就是源文件名。

相关文章
|
1月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
30 3
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
47 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
4月前
|
Java
jmap 查看jvm内存大小并进行dump文件内存分析
jmap 查看jvm内存大小并进行dump文件内存分析
100 3
|
5月前
|
存储 Java 编译器
JVM系列7-虚拟机字节码执行引擎
JVM系列7-虚拟机字节码执行引擎
32 1
|
6月前
|
Java 索引
【JVM】字节码文件的组成部分
【JVM】字节码文件的组成部分
56 1
|
5月前
|
存储 Java 编译器
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
|
5月前
|
存储 XML 安全
JVM系列5-类文件结构
JVM系列5-类文件结构
32 0
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
14天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
12天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
12 1