• Class文件结构


    Java之所以能实现“Write Once, Run Anywhere”,是因为不同平台的虚拟机都统一使用一种程序存储格式——字节码。Java虚拟机不和包括Java在内的任何语言绑定,它只于“Class”文件这种特定的二进制文件格式所关联。

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间无任何分隔符。

    明确两个概念:无符号数和表

    无符号数属于基本的数据类型,以u1、u2、u4来分别代表1个字节、2个字节和4个字节的无符号数。

    表是由多个无符号数或者其他表作为数据项构成的复合数据结构,整个Class文件本质上就是一张表。

    Class文件格式
    类型 名称 数量 描述
    u4 magic 1 魔数
    u2 minor_version 1 次版本号
    u2 major_version  1 主版本号
    u2 constant_pool_count 1 常量池容量
    cp_info constant_pool costant_pool_count-1 常量池
    u2 access_flags 1 访问标志
    u2 this_class 1 当前类常量索引
    u2 super_class 1 超类常量索引
    u2 interfaces_count 1 接口数量
    u2 interfaces interfaces_count 接口常量索引
    u2 fields_count 1 字段数量
    field_info fields fields_count 字段信息
    u2 methods_count 1 方法数量
    method_info methods methods_count 方法信息
    u2 attributes_count 1 属性数量
    attribute_info attributes attributes_count 属性信息

    解释Class文件格式之前,先编写一个简单的java类

    1 package com.yyl.Test;
    2 public class Test{
    3     private int i = 2;
    4     
    5     public int getResult(){
    6         return i + 2;
    7     }
    8 }

    编译成class文件后,用winhex软件打开class字节码

    使用javap命令帮助分析

    1、魔数(magic)

    每个Class文件头4个字节称为魔数,它的唯一作用是确定这个文件能否为一个能为虚拟机接收的Class文件,基于安全考虑,使用魔数而不是扩展名来进行身份识别。从16进制字节码中看出前4个字节为CAFEBABE(咖啡宝贝?)。

    2、次/主版本号(minor_version/major_version)

    紧接着魔数的4个字节分别是次版本号和主版本号,java的版本号是从45开始,高版本的JDK能向下兼容以前版本的Class文件,虚拟机拒绝执行超过其版本号的Class文件。从16进制字节码中看出次版本号0x0000,主版本号0x0032。

    3、常量池容量、常量池(constant_pool_count、constant_pool)

    常量池主要存放两大类常量:

    a.字面值:接近java语言层面的常量概念,如文本字符串、final常量值等。

    b.符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

    由于常量池中常量的数量是不固定的,所以在常量池入口需要设置一项u2类型数据,代表常量池容量计数值。其计数不是从0开始,而是从1开始。

    如图,常量池容量值为0x0013,即十进制19,因此容量为19-1=18个。查看javap命令输出的常量表中也可以看出Constant pool总共有18个常量。

    设计者把第0项空出来目的在于在特定情况下需要表达“不引用任何一个常量池项目”的含义。

    常量池项目结构第一项均为u1类型的tag,该标志代表常量池项目的类型,而其他结构各异。

    下面只列出部分结构:

    常量池中常量项结构
    常量 项目 类型 描述
    CONSTANT_Utf8_info tag u1 值为1
    length u2 占用字节数
    bytes u1 长度为length的UTF-8编码的字符串
    CONSTANT_Integer_info tag u1 值为3
    bytes u4 按照高位在前存储的int值
    CONSTANT_Class_info teg u1 值为7
    index u2 指向全限定名常量项的索引
    CONSTANT_String_info  tag u1 值为8
    index u2 指向字符串字面值的索引
    CONSTANT_Fieldref_info tag u1 值为9
    index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
    index u2 指向字段描述符CONSTANT_NameAndType的索引项
    CONSTANT_Methodref_info tag u1 值为10
    index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
    index u2 指向方法描述符CONSTANT_NameAndType_info的索引项
    CONSTANT_NameAndType_info tag u1 值为12
    index u2 指向该字段或方法名称常量项的索引
    index u2 指向该字段或方法描述符常量项的索引

    接着分析字节码:

    如图可以看出第一个常量项tag为0x0A,即十进制10,类型为CONSTANT_Methodref_info,接着的u2类型0x0004指向类的索引,即指向java/lang/Object类,后面的0x000F指向方法描述符,即指向方法名为<init>返回值为()V的描述符。所以整个常量项就表示如图中的“结果”。

    其他常量项类似上面方法,就不一一阐述。

    另外,由于Class文件中方法、字段等都需要引用CONSTAN_Utf8_info型常量来描述名称,所以该类型最大长度也就是java中方法、字段名的最大长度(u2类型表达的最大值为65535),所以如果定义了超过64KB英文字符的变量或方法名,将无法编译。

    4、访问标志(access_flags)

    常量池结束后,紧接着的两个字节表示访问标志,用于识别类或接口层次的访问信息,包括这个Class是类还是接口,是否定义为public类型、abstract类型等等。

    访问标志
    标志名称 标志值 含义
    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 标识这是一个枚举

    Test类被public关键字修饰,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,因此其access_flags值应为0x0001|0x0020=0x0021。

    5、类索引、父类索引与接口索引集合(this_class、super_class、interfaces)

    类索引和父类索引都是一个u2类型的数据,接口索引集合是一组u2类型的数据的集合。它们各自指向一个类型为CONSTANT_Class_info的类描述符常量。

     

    6、字段表集合(field_info)

    字段表用于描述接口或类中声明的变量,字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

    字段表结构
    类型 名称 数量
    u2 access_flags 1
    u2 name_index 1
    u2 descriptor_index 1
    u2 attributes_count 1
    attribute_info attributes attributes_count

    字段修饰符放在access_flags项目中,如下表

    字段访问标志
    标志名称 标志值 含义
    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

    跟随access_flags的标志是两项索引值:name_index和descriptor_index,他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。

    区分三个概念:全限定名、简单名称、描述符

    a.全限定名,如java/lang/Object,仅仅将类全名中的“.”替换成“/”。

    b.简单名称是指没有类型和参数修饰的方法或者字段名称,如类中getResult()方法和i字段的简单名称分别为“getResult”和“i”。

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

    描述符标识字符含义
    标识字符 含义 标识字符 含义
    B 基本类型byte J 基本类型long
    C 基本类型char S 基本类型short
    D 基本类型double Z 基本类型boolean
    F 基本类型float V 特殊类型void
    I 基本类型int             L 对象类型,如Ljava/lang/Object

    对于数组类型,每一唯独使用一个前置的“[”字符描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String”。

    当描述符描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如int getResult()方法的描述符为“()I”。

    字段表最后的属性表结构可用于存储一些额外的信息。

    7、方法表集合(method_info)

    方法表的结构跟字段表结构一样,依次包括access_flags、name_index、descriptor_index、attributes,而访问标志则有所区别。

    方法访问标志
    标志名称 标志值 含义
    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 字段是否由编译器自动产生

    方法定义可以由访问标志、名称索引、描述符表达清楚,而方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。

    8、属性表集合(attribute_info)

    在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

    下文只介绍了其中一些属性:

    Code属性表结构
    类型 名称 数量 描述
    u2 attribute_name_index 1 常量值固定为Code,代表该属性名称
    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_table_length 异常属性表
    u2 attributes_count 1 属性集合中属性个数
    attribute_info attributes attributes_count 属性信息

    其中max_locals代表局部变量的存储空间,单位为Slot(虚拟机为局部变量分配内存所使用的最小单位)。对于byte、char、float、int、short等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数,包括实例方法中的隐藏参数“this”、显式异常处理器的参数、方法体中定义的局部变量都需要使用局部变量表来存放。max_locals并不是简单将所有局部变量所占Slot之和作为其值,java编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算max_locals的大小。

    字节码中每个u1类型的单字节代表一个指令。意义请自行查找虚拟机字节码指令表。

    异常表结构
    类型 名称 数量 类型 名称 数量
    u2 start_pc 1 u2 handler_pc 1
    u2 end_pc 1 u2 catch_type 1

    这些字段的含义为:如果当字节码在第start_pc行(相对于方法体开始的偏移量)到第end_pc行(不包括)之间出现类型为catch_type或其子类的异常,则转到handler_pc行继续处理。当catch_type为0时,代表任意异常情况都需要转向handler_pc行处处理。

    LineNumberTable属性结构
    类型 名称 数量
    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

    该属性用于描述java源码行号与字节码行号(偏移量)之间的对应关系,line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是java源码行号。

    后面是另一个方法的字节码,就不再赘述。

  • 相关阅读:
    可视化工具Grafana:简介及安装
    数据采集工具Telegraf:简介及安装
    怒怼某些自媒体培训机构,吃相不要太难看了!!!
    时序数据库InfluxDB:简介及安装
    jmeter(二十五)linux环境运行jmeter并生成报告
    Linux:CentOS7.4新建用户并授权
    服务端监控工具:Nmon使用方法
    Locust:简介和基本用法
    Quant Finance Master’s Guide 2020
    数据科学入门前需要知道的10件事
  • 原文地址:https://www.cnblogs.com/honghuzidelaoren/p/3622021.html
Copyright © 2020-2023  润新知