zoukankan      html  css  js  c++  java
  • JVM学习笔记(三):类文件结构

    代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 

    实现语言无关性的基础是虚拟机字节码存储格式Java虚拟机不和包括Java在内的任何语言绑定,只与"Class文件"这种特定的二进制文件所关联,Class文件中包含了Java虚拟机指令集合符号表以及若干其它辅助信息。Java虚拟机作为一个通用的、机器无关的执行平台,任何其他语言都可以将其作为语言的产品交付媒介。

    Java虚拟机提供的语言无关性:

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

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

    Class文件格式:

    图来自:Java类文件结构详解 

    更具体而言:

    下面以一个简单的Java代码为例进行说明:

    package chapter02;
    
    public class ClassTest {
        
        private int m;
        
        public int inc(){
            return m + 1;
        }
            
    }

    生成的Class文件如下所示(可用十六进制编辑器WinHex打开.class文件,WinHexagon也可在这里下载):

    下面具体来分析Class文件中各项数据的含义

    1. 魔数

    每个Class文件的头4个字节称为魔数,用于验证该文件是否为一个能够被虚拟机接受的Class文件。Java的Class文件魔数固定为:0xCAFEBABE

    2. Class文件的版本

     魔数后4个字节,第5个和第6个字节是次版本号,第7个和第8个字节是主版本号。这里次版本号为:0x0000,主版本号为0x0034,转换为十进制为:52,即使用的JDK版本为1.8

    3. 常量池

    (1). 常量池容量计数值
    计数从1开始,目的是满足某些常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池”的含义。
    这里常量池容量为0x0016,即十进制的22,代表常量池中有21项常量,索引值范围为1~21

    (2). 常量池
    常量池中主要存放两大类常量:字面量和符号引用
    字面量接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
    符号引用属于编译原理方面概念,主要包含以下三类常量: 1) 类和接口的权限定名;2) 字段的名称和描述符; 3) 方法的名称和描述符
    常量池中每一项常量都是一个表,这些表都具有如下通用格式:

    表开始的第一位是一个标志位(tag),后面info[]项的内容tag的类型所决定。
    下面进行具体分析:
    常量1:

    07 //标志位,表示这个常量属于CONSTANT_Class_info类型。CONSTANT_Class_info类型表示这个常量是一个类或接口的符号引用
    00 02 //索引值,指向常量池中的第二项常量。

    常量2(即常量1指向的第二项常量):

    01 //标志位,表示这个常量属于CONSTANT_Utf8_info类型。CONSTANT_Utf8_info类型表示这个常量是一个UTF-8编码的字符串
    00 13 //length值,表示这个UTF-8编码的字符串占用的字节数,它后面紧跟着长度为“length值”的连续数据是一个使用UTF-8缩略编码表示的字符串。0x0013的十进制为19,也就是长19字节
    63 68 61 70 74 65 72 30 32 2F 43 6C 61 73 54 65 73 74//长度为19个字节,表示chapter02/ClassTest

    CONSTANT_Class_info类型的表和CONSTANT_Utf8_info类型的表的具体结构:

    到此为止我们分析了ClassTest.class常量池中21个常量中的两个,其余19个常量也可通过类似的方法计算出来。同时,我们也可以通过javap命令行查看:

    可以看出,图中的第1、2项常量与我们手工计算的结果一致。(javap命令:cd到.class的目录下,执行javap命令)

    4. 访问标志
    在常量池结束之后,紧跟着的两个字节代表访问标志,用于识别一些类或者接口层次的访问信息。

    00 21 //表示该类是一个public类型

    5. 类索引、父类索引、接口索引集合
    类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
    对于接口索引集合,入口第一项为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面的接口索引表不再占用任何字节。

    00 01 //类索引,表示chapter02/ClassTest
    00 03 //父类索引,表示java/lang/Object。(Java中所有的类都有父类,除了java.lang.Object)    
    00 00 //接口索引集合大小,因为该类没有实现任何接口,所以值为0

    查询javap命令计算出来的常量池,找出对应的类和父类的常量:

     

    6. 字段表集合
    字段表用于描述接口或类中声明的变量。字段包括类级别变量以及实例变量,但不包括在方法内部声明的局部变量。包含的信息有:字段的作用域(public 、private、protected修饰符)、类变量还是实例变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读取)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合用标志位来表示。而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

    00 01 //容量计数器fields_count,说明这个类只有一个字段表数据
    00 02 //字段修饰符access_flags,表示private修饰符为真,即该字段修饰符为private
    00 05 //字段名称name_index,从javap命令行计算出的常量池表中,可知第5项常量是一个CONSTANT_Utf8_info类型的字符串,其值为"m"
    00 06 //字段描述符descriptor_index,从javap命令行计算出的常量池表中,可知第6项常量是一个CONSTANT_Utf8_info类型的字符串,其值为"I",代表该字段是一个int类型的值

    可以看出,修饰符是用标志位表示的,而字段名称(name_index)和字段类型(descriptor_index),是从常量池中读取的。

    7. 方法表集合
    方法表的结构同字段表一样,依次包含了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
    至此,可知,方法的定义可通过访问标志、名称索引、描述符索引表达清楚[如:public static void testTenuringThreshold()方法];方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。

    00 02 //计数器容量,代表集合中有两个方法(分别为编译器添加的实例构造器<init>和源代码中的方法inc())
    00 01 //访问标志,表示只有ACC_PUBLIC标志为真,即该方法的修饰符为public
    00 07 //名称索引,通过查询常量池表,可得方法名为"<init>"
    00 08 //描述符索引值,通过查询常量池表,可得对应常量为"()V"
    00 01 //属性表计数器,表示此方法的属性表集合有一项属性
    00 09 //属性名称索引,对应常量为"Code",说明此属性是方法的字节码描述,即Java方法里面的代码

     

    8. 属性表集合
    java class文件内部属性信息,和java语言定义的属性没有关系,纯粹就是给java虚拟机用的。
    对于每一个属性,它的名称需从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,需需要通过一个长度属性去说明属性值所占用的位数即可。

    Code属性:
    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。

    00 01 //max_stack,代表操作数栈深度的最大值,这里操作数栈最大深度为1。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧。
    00 01 //max_locals,代表局部变量表所需的存储空间,这里最大的局部变量数为1。
    00 00 00 05 //code_length,代表字节码长度,这里方法代码的长度为5。code_length和code用来存储Java源程序编译后生成的字节码指令。
    2A B7 00 0A B1 //code,用于存储字节码指令的一系列字节流。
    //具体分析2A B7 00 0A B1
    2A //查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶
    B7 //查表的0xB7对应的指令为invokespecial
    00 0A //这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器"<init>"方法的符合引用
    B1 //查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。

    参考:
    JVM(四)类文件结构解析 
    Java字节码结构解析 
    《深入理解java虚拟机 JVM高级特性与最佳实践》

  • 相关阅读:
    希望走过的路成为未来的基石
    第三次个人作业--用例图设计
    第二次结对作业
    第一次结对作业
    第二次个人编程作业
    第一次个人编程作业(更新至2020.02.07)
    Springboot vue 前后分离 跨域 Activiti6 工作流 集成代码生成器 shiro权限
    springcloud 项目源码 微服务 分布式 Activiti6 工作流 vue.js html 跨域 前后分离
    spring cloud springboot 框架源码 activiti工作流 前后分离 集成代码生成器
    java代码生成器 快速开发平台 二次开发 外包项目利器 springmvc SSM后台框架源码
  • 原文地址:https://www.cnblogs.com/zeroingToOne/p/9122501.html
Copyright © 2011-2022 走看看