JVM Class类文件结构

news2024/10/3 6:44:15

国庆节快乐            2024年10月2日17:49:22 



目录

前言

magic 数

文件版本

使用JClassLib观察class文件

一般信息

接口        

常量池

字段

方法 

常量池计数器

常量池

类型

 CONSTANT_Methodref_info

CONSTANT_Class_info

类型结构总表

访问标志 

类索引, 父类索引与接口索引

字段表

方法表

属性表 


作者@NICEFF_KING, 文中描述有错误的地方请在评论区不吝教诲

前言

        对于Java的各种版本来说, 一般都遵从这个特点, 高版本的JDK是可以编译低版本的java源代码的, 因此在CLass文件各项细节, 几乎没有随着版本有太大的改变, 甚至没有做出任何修改, 后续的jdk版本来说, 更新只是在原有的结构上进行扩充, 熙增新增.

        前面我们讲解, 一个java源代码文件, 可以被编译器编译为JVM可以识别的字节码文件, 这个字节码文件是一个二进制文件(二进制流), 各个数据项是严格按照某种特定的顺序排列在文件之后, 中间没有加任何间隔符.

 Class文件是以8个字节为基础的二进制文件, 其文件格式采用类似于C语言结构体的类似的数据结构来存储数据, Class文件中存在两种数据类型

  • 无符号数

        无符号数对应的是基本数据类型, 无符号数通常使用u1, u2, u4, u8(u后面的数字代表的是n个字节, 例如u1就是一个字节的无符号数, u既是unsigned) 来表示无符号数. 

        表则由多个无符号数, 或者其他表作为其构成的复合型数据结构, 为了便于区分表和无符号数, 所有的表的命名都习惯性的加上 _info后缀.  整个Class文件就可以看做是一个表.

        下图是Class文件的格式: 

        下面我们来看看各个数据项的特殊含义, 在开始之前, 你应该写上一个类, 然后将其编译, 然后使用16进制编辑器查看class文件, 如下是一段代码, 以及它编译后的字节码文件: 

public class Test {
    private int x;
    public static void main(String[] args) {

    }
    public int inc() {
        return x + 1;
    }
}

(使用IDEA插件查看class文件)

下载插件: 

对编译好的class文件右击, 然后选择打开于:  

选择刚才下载的插件:

        字节码:

  • 二进制格式 

  • 16进制格式:

magic 数

        首先对于上图的16进制的文件, 不难看出开头的四个字节(2^4可以描述一位16进制的数, 因此两位16进制数需要两个2^4的二进制, 一个字节是8个bit位, 2^4为四个bit位因此2个16进制的数需要1个字节)被标记为天蓝色. 

使用HXD软件查看的字节码文件

        其内容为:"CAFEBABE", 这个也不难理解, 其实你可能已经猜出来了这个是干嘛用的, 它的唯一作用就是检查这个文件是否是一个可以被JVM识别接受的Class文件, 参考其他的文件格式, 有很多也是使用模式来判断身份标识, 例如GIF等文件格式.  不使用扩展名的原因是扩展名可以随意修改, 为了安全起见, 没有采用扩展名的方式. 

        你能找出开头不为:cafebabe的class文件吗.



文件版本

         紧接着magic的四个字节是次版本号(minor_version)和主版本号(major_version), 先来看看这四个字节的16进制数是多少: 

        其中地址偏移量(offset) 为0x04 ~ 0x05的是次版本号, 0x06~0x07的是主版本号, 可以看到次版本号为0, 主版本号为0x0034, 对应的10进制就是52, 这个52到底是什么意思, 为什么次版本号为0呢? 

        首先需要知道的是java的版本号是从45开始的, JDK1.1之后每一个大版本发布都将主版本号+1, 高版本的JDK可以兼容低版本的Class文件, 但是不能兼容高于当前JDK版本的JDK编译生成的Class文件, 即使文件格式没有发生任何改变, 如果版本号高于当前的JDK版本, 虚拟机会需要拒绝此文件(来自java虚拟机规范)

        例如JDK1.1支持的版本号是45.0~45.65535, 但是无法执行版本号为4.6以及以上的Class文件. (上图中我自己的版本号为52, 刚好对应的是JDK1.8版本). 

        下图是对应版本号对应的JDK版本: 

        关于次版本号为0, 存在一些历史因素, 其中在JDK1.1支持的版本是45.0~45.65535, 但是从1.2之后次版本号均没有被使用, 都是为0, 到了JDK12以及后期, JDK本身集成了非常多的功能, 其中可能不乏一些不稳定的新特性, 这些新特性在还没有全面的测试使用和分析数据的情况下, 是不能直接进入商业应用阶段的, 因此为了区分, 如果CLass文件中使用了该JDK版本未正式列入特性清单中的功能, 那么就会将次版本号的值改为: 65535(二进制全1), 因此你可能会在偏移量为0x04和0x05这两个偏移(相对) 看见 FFFF(HEX)的值.



        为了更好的理解常量池是怎么设计的, 我们先使用一些工具来查看字节码的结构

使用JClassLib观察class文件

        首先我们需要下载JclassLib: 

GitHub - ingokegel/jclasslibjclasslib bytecode editor is a tool that visualizes all aspects of compiled Java class files and the contained bytecode. - ingokegel/jclasslibicon-default.png?t=O83Ahttps://github.com/ingokegel/jclasslib        jclasslib(现在简称jcl) 字节码编辑器是一个可以分析java的Class文件结构的工具, 它将Class文件解析成我们可以看懂的结构特征. 目前已经支持到了JDK21版本. 

        下载之后, 进行安装, 然后我们编写如下代码: 

public class Test implements Runnable{
    private int x;
    private static final String a = "abc";

    public int inc() {
        return x + 1;
    }

    @Override
    public void run() {
        
    }
}

        我们使用javac来编译当前的代码: 

javac Test.java

         当前编译的JDK: 

         然后将编译成的Test.class使用jclassLib打开: 

         可以看到jclassLib解析出了6个模块分别是: 

  • 一般信息
  • 常量池
  • 接口
  • 字段
  • 方法
  • 属性

一般信息

         一般信息是类的统计信息, 例如当前javaclass字节码文件中的主次版本号, 常量池中常量项的个数, 访问标志(是否是public), 父类的全限定类名, Test类的名称的常量, 还有当前这个类的实现的接口的数量, 字段数量, 自身的属性等. 

        首先主次版本号中, 次版本号为0 , 说明当前的JDK版本中没有集成哪些不稳定的需要公测的特性. 主版本号为52, 正好对应着JDK1.8版本, 然后常量池中常量计数器的数值(常量项的个数)

        访问标志是两个字节存储的, 也就是u2, 例如0x000021, 它的二进制表示为: 

00000000 00000000 00000000 00100001, 其中第0位表示公共性,其他位可以指示是否是抽象等等. 后面细说.

        然后就是文本索引, 其它的值为cp info #3, 不难猜出其实cp就是类似于linux中的cp指令, 意思就是它引用了#3的常量项(常量池中的), 我们在jclasslib'中点击它, 可以看见它跳转到了如下页面: 

         可以看到它来自这个名为[03]CONSTANT_Class_info的常量项, 并且这个常量项引用了另外一个#24的常量项, 我们继续点击这个#24, 跳转如下: 

        我们注意到这个页面有两个常量池项的类型: 

  • CONSTANT_String_info : 用于表示一个字符串常量
  • CONSTANT_Utf8_info 

        CONSTANT_String_info更接近于Java语法中数据类型的东西, 它包含一个指向CONSTANT_Utf8_info 常量类型的引用, 可以用作字符串的符号引用,在编译时用于表示字符串常量, 而CONSTANT_Utf8_info 专门用于存储UTF-8编码的字符串数据, 也就是真是存储数值的常量项, 这是一个通用的条目类型,可以被其他常量类型引用,例如CONSTANT_Utf8_info 和CONSTANT_String_info, CONSTANT_Utf8_info本身不提供上下文,只是存储字符串的原始内容

        总结来说, CONSTANT_String_info是一个引用, 用于在常量池中指定一个具体的常量, CONSTANT_Utf8_info则是世界存储该字符串的内容的条目

        这里提一嘴, CONSTANT_Class_info直接引用CONSTANT_Utf8_info, 而没有通过CONSTANT_String_info简介去引用CONSTANT_Utf8_info常量的原因是, 类名信息室不需要区分数据类型的, 它只需要表示可读的文本即可, 但是例如你在内存中定义了一个常量String a = "abc", 此时你引用这个a, 就需要引用一个CONSTANT_String_info来表示它是一个String类型的常量, 然后再引用其真实的值. 对于类信息, 例如类名, 它使用Class_info表名其类型, 然后再引用这个CONSTANT_Utf8_info类型的常量. 

        父类引用为#04号常量, 从图中可以简略看出来它是继承自Object.

        我们查看这个引用, 信息如下 : 

         可以看到这个#04的引用是一个Class_info类型的, 说明它描述的是类的信息(毕竟是继承, 继承是表达类与类之间关系的一种方式).  这个Class_info引用了另外一个类型(#27), 通过上面的讲述, 其实你也能猜到引用的是一个CONSTANT_Utf8_info类型的常量如下: 

         它的字面量是一个全限定类名: java/lang/Object. 

接口        

还有一个接口数, 可以看见接口数量为1 : 

        但是貌似没有任何引用. 虽然在一般信息里面不能直接看到, 但是可以从接口栏目查看, 如下: 

        可以看到它引用了 #5 这个常量, 如下: 

         可见, 接口信息也被归属到 类信息中, 并且引用了另外一个CONSTANT_Utf8_info的字面量. 

常量池

        从上面的分析不难看出, 常量池中主要存储的是CONSTANT_Utf8_info的类型的字面量, 还存放着CONSTANT_Class_info的类信息, 除此之外还有CONSTANT_String_info的类型的信息. 具体还有哪些就不一一讲解, 大致总结如下(随着jdk版本的变更或多或少为了支持新特性会增加一些其他类型的信息) : 

        从上图不难看出每种常量池的类型都有不同的标志位, 从1开始往后. 字节码指令中, 往往就是存在着一些对常量池中数据的引用, 如下: 

        这种通过编号引用到常量池中数据的过程成为符号引用. 

字段

如下: 

        我们可以看出来字段拥有着这几类的描述信息: 

  • 字段名 : 名称
  • 描述符 : 描述符号, 可以理解为类型
  • 访问标志: 权限, 是public 还是Private等

        我们首先看名字, 它(字段a)引用了第八项常量, 如下(字段的名称直接引用的是字面量) , 以此来说明这个字段的命名为"a": 

         其次它还引用了第九项常量, 如下: 

         可以看出来描述也是一个CONSTANT_Utf8_info的字面量, 它描述的是类型的信息.  除此之外它的访问标志为

         除此之外, 除了这些描述性的信息(字段名, 类型, 访问标志), 他还有自己的值, 我们双击这个常量a, 就可以返现它有一个CONSTANT的常量值: 

        具体信息如下: 

        其中特有信息引用的是第11项常量值, 如下: 

         它是一个String类型的CONSTANT引用, 因此表面此数据类型是String类型的字面量, 然后引用了第二十九项常量(Class_info类型), 其值为"abc".

         其次, 一般信息中它还引用了一个10号常量, 如下: 

        可以发现 , 里面的字面量为ConstantValue, 这个 ConstantValue有什么用呢? 我不是已经通过一个访问标志来查看它的类型了吗, 而且还有访问标志符号来判定它是否是一个静态常量, 为什么还需要一个一般信息的constantValue来描述它? 

        ConstantValue属性是专门用于存储静态字段(特别是static final字段)的常量值的, ConstantValue属性是专门用于存储静态字段(特别是static final字段)的常量值的. 这个属性会指向常量池中的一个CONSTANT_Utf8_info(对于字符串)或者CONSTANT_Integer_info(对于整数)等,从而指定静态字段的值. ConstantValue属性的存在是为了在类加载阶段能够直接获取到这些静态常量的值,而不需要在运行时通过方法调用等方式去计算或获取.

        在进行类加载的时候, 仅仅只是知道这些常量在常量池中的位置是不够的, 还需要知道哪些字段是静态常量, 并且在类加载的时候, 就加载上正确的值, 这个时候, 就需要看ConstantValue的脸色了, ConstantValue是类文件中字段表的一个可选的属性, 通过解析类文件中的字段表,查找具有ConstantValue属性的字段,并读取该属性所指向的常量值来识别和加载静态常量.

方法 

        需要解析的东西还不少 ... ... 

         回顾一下这个class文件的源代码: 

public class Test implements Runnable{
    private int x;
    private static final String a = "abc";

    public int inc() {
        return x + 1;
    }

    @Override
    public void run() {
        
    }
}

         通过对比源码可知, 0号方法为init, 我们暂时猜测它是构造方法, inc为我们自己定义的方法, run是Runnable的实现方法, 我们首先来看自己定义的inc方法, 如下: 

        同样的拥有两样信息: 

  • 方法名称, 描述方法的名称, 引用了第19个常量, 该常量是一个utf8_info的数据, 值为"inc", 正式我们的方法名
  • 描述符 : 描述该方法的参数和返回值, 该例子中它引用了第20个常量, [20] 常量是一个utf8_info类型的数据, 是一个字面量. 

        双击这个inc就可以发现, 它里面的方法体如下: 

        我们首先看这个Code, 如下: 

         同样拥有一般信息和特有信息(包括方法体的字节码, 异常表,和杂项), 首先是一般信息中, 引用了[14] 号常量, 它是一个utf8_info类型的字面量, 值为Code, 然后字节码为: 

0 aload_0
1 getfield #2 <Test.x : I>
4 iconst_1
5 iadd
6 ireturn

对此字节码指令的说明: 

  • aload_0(操作码:26,局部变量索引:0)

        这条指令从局部变量表的第0个位置加载一个引用类型的值到操作数栈顶。在Java方法中,局部变量表的前几个位置通常用于存储方法的参数和this引用(对于非静态方法). 因此,aload_0通常用于加载当前对象的引用(即this)

  • getfield #2 <Test.x : I>(操作码:179,字段引用索引:2)

        这条指令从操作数栈顶取出一个对象引用(由aload_0加载的),并根据常量池中的索引2找到对应的字段描述符(这里是<Test.x : I>),然后从该对象中获取名为x的字段的值,并将该值(类型为int,由I表示)压入操作数栈顶。这里假设Test是一个类,x是Test类的一个int类型的实例字段。

  • iconst_1(操作码:8)

        这条指令将整型常量1压入操作数栈顶 

  • iadd(操作码:96)

        这条指令从操作数栈顶弹出两个整型值(第二个弹出的是操作数,第一个弹出的是被加数),将它们相加,然后将结果压回操作数栈顶. 在这里,它将getfield指令获取的x的值与iconst_1指令压入的1相加 

  • ireturn(操作码:172)

        这条指令从操作数栈顶弹出一个整型值,并将其作为方法的返回值。这意味着方法的执行将结束,并且该整型值将被返回给方法的调用者 

杂项: 

 至于说下面的这两项: 

        在Java虚拟机(JVM)中,LineNumberTable和LocalVariableTable都是类文件中方法属性表(attribute_info)的一部分,它们提供了关于方法的附加信息,这些信息对于调试和代码分析非常有用 .

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它不是运行时必需的属性,但默认会生成到Class文件中,可以使用javac编译器的选项来取消或要求生成这项信息。

  • 作用:它帮助开发者在调试时,将字节码指令映射回Java源码中的具体行号,从而更容易地定位和理解代码的执行流程。
  • 结构LineNumberTable属性包含一个表,表中每一项都包含一个源码行号和对应的字节码偏移量。

LocalVariableTable

        localVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。它同样不是运行时必需的属性,默认也不会生成到Class文件之中,但可以使用javac编译器的选项来要求生成这项信息。

  • 作用:它帮助开发者在调试时,了解局部变量在方法执行过程中的值和作用域,从而更容易地跟踪和理解代码的执行状态。
  • 结构LocalVariableTable属性包含一个表,表中每一项都包含变量的名称、类型、作用域起始和结束位置(以字节码偏移量表示)等信息。

其他的一些都是class文件的一些属性, 待读者自行研究.  

常量池计数器

        紧接着版本号的就是常量池(红框), 它是Class文件结构中与其他项目关联最多的数据, 通常也是一个CLass文件占用最多的部分, 但是由于常量池的长度不固定, 因此需要有一个标识来记录其常量池的容量的计数值: 

        名为constant_pool_count, 大小为2个字节, 是一个无符号数. 能表示的范围为0~65535.  我之前的截图文件中的constant_pool_count 的数值如下: 

        1A(HEX)即 26(DEC), 这就代表常量池中存在着25项常量. 但是与其他的结构不同的是, 这个值的计数是从1开始, 就例如你数数量, 从来都是从1开始数起. 

        这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示, 你暂时只需要记住这两个字节是表示常量池常量项个数的标识即可. 

常量池

类型

        通过我们上面的jclassLib观察的结果, 发现常量池中的很多种类型, 我们已经熟悉了: 

  • CONSTANT_Class_info , 类信息
  • CONSTANT_Utf8_info , 字面量
  • CONSTANT_String_info , 数据类型

        说完常量池计数器, 紧接着的就是常量池.  常量池中主要存放两大类: 

  • 字面量 : 比较接近java语言中的常量的概念(CONSTANT_Utf8_info)
  • 符号引用 , 主要包括下面几类常量: 
    • 被模块导出或者开放的包
    • 类和接口的全限定名 (CONSTANT_Class_info)
    • 字段的名称和描述符
    • 方法的名称和描述符
    • 方法句柄和方法类型
    • 动态调用点和动态常量

        具体符号引用是什么, 我们需要先理解一下, java的编译原理. 

        在java的Class文件中, 是不会存储文件中各个字段, 方法等最终在内存中的布局信息, 这些字段方法的符号引用不经过java虚拟机在运行期间的转换, 是无法真正得到内存中的入口地址的. 也就无法被虚拟机使用, 

        常量池中的每一项都是一个表, 每个表最初具有11种各不相同的表结构数据, 后来为了更好的支持动态语言调用, 额外增加了4中动态语言相关的常量 ... 

        后续增加到17种, 如下: 

        每一种类型都有自己的标志位来表示自己是哪种类型的, 为了表示它, 每一个类型的最前面都会有一个u1类型的标志, 来表示这个标志位. 

        为了演示, 我们编写如下代码: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

        然后使用HxD查看其字节码文件如下: 

Test.class

 我们回顾一下之前的讲解: 

  • 00 ~ 03 是 java字节码文件的标识(魔数)
  • 04~05 是次版本号
  • 06 ~ 07 是主版本号 34(hex) 对应的10进制是52, 正好对应着JDK1.8
  • 08 ~ 09 对应着常量池中常量项的个数.  在上图的Test.class中对应的16进制是16, 转换成10进制就是22, 也就是说常量池中存在着22个常量项, 通过jclasslib查看如下: 
使用jclasslib观察Test.class

        虽然是22个, 但是这里只显示了21个, 也就是说索引的范围为1~21, 那还少了一个, 这是因为常量池的容器计数是从1开始的, 第0项有着特殊的含义, 也就是将其他常量项的值设置为索引0来表示它不索引任何一个值.

        我们回过头来看看这个常量池中的第一个常量的标志位:

 CONSTANT_Methodref_info

        可以看到第一个常量是(地址偏移为0x0000000A) 0x0A, 也就是标志位值为10, 对应的类型是CONSTANT_Methodref_info, 其实我们也可以在jclassLib中查看: 

         CONSTANT_Methodref_info 说明它是类中方法的引用, 它引用了类名的描述和方法名以及其描述符, 类名引用的是CONSTANT_Class_info类型的类名(此类名引用了CONSTANT_Utf8_info类型的字面量), 名字和描述符引用了CONSTANT_NameAndType_info类型的符号引用, 这个符号引用分别引用了名字的符号引用和符号的引用: 

        描述符描述的就是返回类型和参数, 是一个utf8字面量.  名字也是如此. 

        它的结构如下:

        除了tag(标志位), 下面两个字节是u2类型, 来表示这个方法的类描述符(CONSTANT_Class_info), 紧接着这个u2的也是一个u2类型, 指向名称以及类型描述符(CONSTANT_NameAndType_info)

         可以看到本例子中的第一个常量就是一个CONSTANT_Methodref_info 类型, 第一个index表示了[04]位的常量, 第二个index位0x12, 标识了[18]位常量, 如下: 

        在字节码层面上, 我们对上图的字节码文件的结构进行解析, 一直解析到第一个常量项:  如下

         可以看到, 第一个常量的类型的地址偏移从0x0000000A~0x0000000E, 也就是: 

  • 0A:   为一个u1类型, 表示常量类型的标志位, 这里是0A表示CONSTANT_Methodref_info
  • 0B ~ 0C: 为一个u2类型, 表示 指向声明此方法的类描述符的索引
  • 0D ~ 0E: 为一个u2类型, 表示方法名称和类型描述符

        然后下一个常量开始的地址偏移就是0x0000000F, 其值为9, 表示字段的引用符号: 

         然后以此类推, 就可以查出所有的常量池常量所在的位置

CONSTANT_Class_info

        这个其实就是类的信息, 也使用常量来存储.  它的存储结构如下: 

        第一个u1表示标志类型, 其值与上述的表对应(0x07), name_index 是常量池的索引值, 它使用2个字节来指向常量池中的CONTANT_Class_info类型的常量

        还有很多类型, 这里就不一一列举例子. 

类型结构总表



        避免遗忘回顾一下这张图: 

访问标志 

        可以看出来, 在常量池之后的内容就是一个u2字节的访问标志(access_flags), 用于标识一些类或者接口层次的访问信息, 包括这个Class是类还是接口 , 是否是public类型, 是否是abstract等, 如果是类的话, 又是否是被声明为final等等之类的, 具体的含义如下:

        注意是16进制 ... 

共有16个可以使用, 但只标识了9个

         访问标志位占用2个字节, 也就是16个bit位, 也就是可以使用16位来表示其中的各种含义, 现在不同的标志位对应的含义已经标明, 我们只需要找出访问标志位, 然后解析其值, 并与其上图进行分析即可.

        我们当前使用的代码: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

        对应的class字节码文件(16进制) 

        由于我们再常量池那一章节中使用的是一个普通的java类, 不是接口, 注解, 也不是枚举, 或者模块, 被public修饰但是没有被final修饰, 并且使用JDK1.8标识, java虚拟机规范要求, 如果没有被使用到的一律需要置为0 

        从上述的代码中可以看出Test类是一个普通的java类, 因此 它应该将public标志位置为真: 

         但是我们发现0x0020也被激活, 这是为什么? 从上图中可以看出, 0x0020(ACC_SUPER)是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真

        其余项目都应该置为假, 因此就形成了0x0021的值. 

类索引, 父类索引与接口索引

         access_flags下面的四个字节就是this索引和super索引了, 他们分别占用两个字节, 也就是分别为u2类型的数据, 紧接着access_flags的是this_class, 也就是类索引. 除了this_class和super_class, 接下来还有两个两个字节的u2表示接口相关的信息

        他们四个来确定Class文件中类的集成关系, 接下来分别讲述其真正的意义: 

  • this_class: 用于确定类的全限定类名, 指向一个CONSTANT_Class_info的常量类型
  • super_class: 用于确定父类的全限定类名, 指向一个CONSTANT_Class_info的常量类型

        我们还是拿这个代码为案例: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

         其字节码文件中的类继承相关的字节码偏移量如下: 

         java语言不支持多继承, 但是支持多接口实现, 因此父类索引只能存在一个, 除了Object之外, 所有的Java类都有父类, 因此除了Object其他所有的类的父索引都不为0, 接口索引数据描述实现了哪些接口, 并且这些接口将按照implements的顺序从左到右以此排列在字节码文件中. 

        我们分析其值, 可知: 

  • this_class : 0x0003

         可以看到它引用了一个Utf8_info的字面量, 值为Test

  • super_class: 0x0004

        可以看到 其引用了一个Utf8_info的字面量, 值为java/lang/Object, 也就是Object类的全限定类名. 

  • interfaces_count: 这个是接口的入口, 是一个计数器, 用来表示接口的数量, 你可以将其类比常量池中常量的排列前面总是要加一个constant_pool_count. 

        其值为0, 说明没有接口实现,  这里有个细节, 因为接口计数器值为0 那么它下面的表示接口细节的u2类型的接口索引表也就不会占用任何字节. 




字段表

        首先字段表用于描述接口或者中声明的变量, Java语言中的字段包括了类变量和实例变量, 不包括方法内部局部变量. 

        既然是java的字段, 就应该有以下的描述

  • 访问限定修饰符(public, private 等)
  • 是否是类变量(static)
  • 可变性(final)
  • 可见性(volatile, 强制主内存读写)
  • 字段类型
  • 字段名

        从上图中可以看出, 字段表是紧接着interfaces接口索引集合后面的 (请注意interfaces_count为0的时候, interfaces是不占用任何字节的).  使用了n个字节来表示, 首先需要一个u2类型来描述字段的个数,  然后这两个字节后面就是真正的字段的相关信息

        我们使用如下代码: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

        字节码还是如下, 并且我已经标出来了字段相关信息的部分: 

        从图中可以看出来fields_count的值为1表示有一个字段, 并且这个字段刚好就应该是我们上面定义的m字段. 

        这个字段后面就是具体的字段表(这个依然可以跟常量池比较, 他们前面都有一个记录数量的标识), 然后每个字段里面的符号引用或者字面量都是大小都是固定的(除了info), 如下: 

字段表

         第一个access_flags是不是很眼熟, 这个跟类的访问标识符很像, 但是他们的值有不同的含义, 如下: 

字段表的access_flags

        显然我们再编写代码的时候, ACC_PUBLIC, PRIVATE, PROTECTED这三个是不能同时选择的, 只能选择一个,  同时VOLATILE和FINAL之间也只能选择一个, 我们根据上述的字段表, 可以看到, access_flags后面的两个字节是name_index, 见名知意, 其实就是名称的索引, descriptor_index就是一种类似于描述符的

        他们都是对常量池项目的引用, 我们可以查看jclassLib中的演示: 

 该字段的值如下: 

  • name_index: 0x0005, 指向索引值为5的常量
  • descriptor_index: 0x0006, 指向常量值为6的常量

         索引值为5的常量为CONSTANT_Utf8_info, 内容为m: 

        索引值为6的同样为 CONSTANT_Utf8_info, 值为I (大写的i, 至于为什么为i, 后面会讲解), 表示该字段的类型为int类型. 

        我们在使用的时候, 导入包的时候, 都是使用"import com.mybatis.cn.*"之类的, 可以看出这里面都是使用的全限定的包名, 但是对于JVM来说, 类的全限定名仅仅只是把里面的"."号替换成为了"/" 而已, 例如我们的Object类 : 

        对于类名和方法名, 字段名这些不需要参数类型, 就只是人能看懂的符号即可, 也就是常见的CONSTANT_Utf8_info类型, 例如上述代码中的inc()方法, 它的方法名(inc)表示如下: 

         但是描述符可能就要复杂一点, 因为描述符需要描述方法和字段的返回值, 数据类型等等之类的, 就基本数据的类型而言(byte、char、double、float、int、long、short、boolean), 以及void类型, 都是用其数据类型命名的第一个字母的大写表示,

如下: 

描述符表示字符的含义

        这里的void只是为了统一让读者了解而列出来的, 在java虚拟机中为VoidDescriptor.  数组类型需要在前面加上"[", 例如int[] 就被描述为"[I". 

        因此下图中(类字段m) : 

        它的描述符为一个引用了Utf8_info类型的, 值为大写i(I)的引用, 此处的大写的i就是表示的基本数据类型int : 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

         对于方法inc的描述, 可以看到其描述符的值为: 

        描述方法的时候, 按照如下顺序来描述: 

  • 参数列表 , 严格按照编码的顺序放在(). 例如方法int func(int[] arrInt, char ch)的描述符就为([IC)I
  • 返回值, 紧跟在()后面, 例如方法char func(), 描述符为()C
字段表

         字段表的固定描述到了descriptor_index就算结束了, 后面的attributes_count和attributes用于存储一些额外的属性信息, 本例中, attributes_count为0, 也就是没有额外的信息存储. 

        那什么时候需要使用到这个, 如果你将m字段声明为static final, 那么可能就需要额外存储一段ConstantValue的属性


方法表

        紧接着字段表的就是方法表, 第一个跟表相关的仍然是count数值, 也就是methods_count, 表示方法的个数. 

        方法表和字段表几乎采用了一摸一样的描述方法. 方法表的结构如下: 

方法表结构

        在access_flags中, 因为方法的访问标志中, 不存在volatile等关键词, 因此在访问标志的描述上,, 存在些许差异: 

方法访问标志表

         唯一需要谈谈的就是, 方法里面的代码去哪了? 

java方法里面的code经过Javac编译器, 编译成字节码指令, 然后存放在方法属性表集合中一个名为code的属性之中去了 : 

         以如下代码为例子: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

        字节码的方法表如下: 

        methods_count的值为2, 代表有两个方法, java基础好的肯定知道, 虽然只写了一个inc方法, 但是还有一个方法肯定是跟构造方法之类的有关, 没错他就是<init>方法(编译器添加的实例构造器), 第一个方法的访问标志位为0x0001,  对比{方法访问标志表}得知, 表示name_index为7: 

        可知它为init的方法名称. 

         描述符(0x0008) 不再赘述, 自行研究: 对应常量为 -- ()V.

        init方法的属性计数器为1, 因此表示此方法的属性表集合有1项属性,属性名称的索引值为0x0009: 

         查看常量池, 值为code, 说明此属性是描述的方法的字节码. 



属性表 

        属性表紧跟着方法表之后, 开头仍然是一个计数器, 其次才是表

        属性表用来存放什么信息? 

Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息, 我们之前所说的方法的code就是存储在此

         例如我们之前写的代码: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

         使用JClassLib来观察其方法inc的方法表如下: 

         JDK12规范中的属性KV如下: 

 

         属性表的结构如下 : 

  • attribute_name_index自然是引用的一个CONSTANT_Utf8_info类型的字面量, 
  • attribute_length来说该属性所占用的位数
  • info来表示即为值 

        接下来我们来聊聊方法表中属性中存储的code属性. 

        经过编译器编译之后, 方法体被编译成字节码指令序列, 被存储在code属性中, 它的结构如下: 

         我们逐项解析: 

  • attribute_name_index自然就是引用的Utf8_Info(CONSTANT_Utf8_info), 表示属性名称的字面量. 此处为"Code"
  • attribute_length指示了属性值的长度
  • max_stack 表示操作数栈的最大深度(可控制的值)
  • max_locals 表示局部变量表所需的空间单位是变量槽. 变量槽是虚拟机分配内存的基本单位, 对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放
  • code_length代表字节码长度的长度,
  • code用于存放字节码指令, 每一个字节码指令的大小为u1, 个数为code_length. 

         继续使用如下代码作为案例分析: 

public class Test{
    private int m;
    public int inc() {
        return m + 1;
    }
}

        字节码: 

        通过上几节的分析, 我们可以找到属性code所在位置: 

         我们使用javap反编译这个文件: 

C:\Program Files\Java\jdk-17\bin>javap -verbose "you\path\Test.class"
Classfile /C:/TestData/ThreadTest/out/production/ThreadTest/Test.class
  Last modified 2024年9月30日; size 338 bytes
  SHA-256 checksum 03b1362989e1f14e5cc4311d6fbca80c2caf91724b4d5ee8eb27a94bfffaa76d
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // Test
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // Test.m:I
   #3 = Class              #20            // Test
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LTest;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               Test.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               Test
  #21 = Utf8               java/lang/Object
{
  public Test();
    descriptor: ()V
    flags: (0x0001) 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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   LTest;
}
SourceFile: "Test.java"

        下面是<init>方法的字节码指令: 

  public Test();
    descriptor: ()V
    flags: (0x0001) 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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

        可以看到args_size为1, 也就是参数为1, 但是构造方法明明没有任何参数, 同样的, 里面的locals为什么为1? 这是因为在类中的实例方法 无论有无参数, 都会有一个this参数指向自身. 

        仅仅是在编译的时候, 把this关键字作为转变为一个普通的方法参数. 因此在该方法的局部变量表中, 至少会存在一个指向当前实例的局部变量(占用局部变量表中第一个变量槽, 只对实例方法有效)




参考资料

  • 深入理解JAVA虚拟机 3版
  • oracle 官网 JVM规范: 

Java SE Specificationsicon-default.png?t=O83Ahttps://docs.oracle.com/javase/specs/index.html

  • Java虚拟机规范  Java SE 8版

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2185754.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信息安全工程师(30)认证概述

前言 认证&#xff0c;作为一种信用保证形式&#xff0c;是通过一系列的程序和标准来确认某人或某物的身份、资格、性能或质量的过程。其重要性不言而喻&#xff0c;是国家规范经济、促进发展的重要手段&#xff0c;也是政府保障产品、生态和人民生命财产安全的关键措施&#…

C语言 | Leetcode C语言题解之第452题用最少数量的箭引爆气球

题目&#xff1a; 题解&#xff1a; int cmp(void* _a, void* _b) {int *a *(int**)_a, *b *(int**)_b;return a[1] < b[1] ? -1 : 1; }int findMinArrowShots(int** points, int pointsSize, int* pointsColSize) {if (!pointsSize) {return 0;}qsort(points, pointsSi…

深度剖析音频剪辑免费工具的特色与优势

是热爱生活的伙伴或者想要记录美好声音的普通用户&#xff0c;都可能会需要对音频进行剪辑处理。而幸运的是&#xff0c;现在有许多优秀的音频剪辑软件提供了免费版本&#xff0c;让我们能够轻松地施展音频剪辑的魔法。接下来&#xff0c;就让我们一同深入了解这些音频剪辑免费…

【Docker】docker的存储

介绍 docker存储主要是涉及到3个方面&#xff1a; 第一个是容器启动时需要的镜像 镜像文件都是基于图层存储驱动来实现的&#xff0c;镜像图层都是只读层&#xff0c; 第二个是&#xff1a; 容器读写层&#xff0c; 容器启动后&#xff0c;docker会基于容器镜像的读层&…

VScode 自定义代码配色方案

vscode是一款高度自定义配置的编辑器, 我们来看看如何使用它自定义配色吧 首先自定义代码配色是什么呢? 看看我的代码界面 简而言之, 就是给你的代码的不同语义(类名, 函数名, 关键字, 变量)等设置不同的颜色, 使得代码的可读性变强. 其实很多主题已经给出了定制好的配色方案…

「C++系列」预处理器

【人工智能教程】&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站&#xff1a;【人工智能教程】 文章目录 一、预处理器1. 宏定义&#xff08;Macro Definition&#xff09;2…

【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000013 案例 EXCEL筛选与高级筛选。使用的软件&#…

【C语言】数组(下)

6、二维数组的创建 6.1二维数组的概念 通过数组&#xff08;上&#xff09;介绍&#xff0c;我们学习了一维数组&#xff0c;数组的元素都是内置类型的&#xff0c;如果我们把一维数组作为数组的元素&#xff0c;这时就是二维数组&#xff0c;以此类推&#xff0c;如果把二维…

基于单片机跑步机控制系统设计

** 文章目录 前言概要功能设计设计思路 软件设计效果图 程序文章目录 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对…

【嵌入式裸机开发】智能家居入门3(MQTT服务器、MQTT协议、微信小程序、STM32)

前面已经写了两篇博客关于智能家居的&#xff0c;服务器全都是使用ONENET中国移动&#xff0c;他最大的优点就是作为数据收发的中转站是免费的。本篇使用专门适配MQTT协议的MQTT服务器&#xff0c;有公用的&#xff0c;也可以自己搭建 前言一、项目总览二、总体流程分析1、了解…

海外合规|新加坡推出智慧国2.0计划 设新网络安全与保障机构

智慧国2.0计划&#xff1a;政府将成立新机构杜绝网络伤害和援助受害者。政府将成立新机构&#xff0c;并制定新法令&#xff0c;以杜绝网络伤害行为和为受害者提供更多援助与保障。新加坡总理兼财政部长黄循财星期二&#xff08;10月1日&#xff09;在推介晚宴上&#xff0c;宣…

基于单片机的花色可调跑马灯设计

**基于单片机频率花色可调跑马灯 文章目录 前言概要设计思路 软件设计效果图 程序文章目录 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/A…

51c自动驾驶~合集1

我自己的原文哦~ https://blog.51cto.com/whaosoft/11466109 #HTCL 超过所有视觉方案&#xff01;HTCL&#xff1a;分层时间上下文问鼎OCC 本文是对ECCV2024接受的文章 HTCL: 的介绍&#xff0c;HTCL在SemanticKITTI基准测试中超过了所有基于相机的方法&#xff0c;甚至在和…

在pycharm中使用PySpark 出现Java gateway process exited before sending its port number.

# 原因是没有下载Java&#xff08;jdk&#xff09; 程序出现下面错误&#xff1a; 解决办法&#xff1a; 1、2、3、先点击“” &#xff0c;添加这一行&#xff0c;点击确定即可。再次之前先判断你电脑上没有jdk&#xff0c;有的话&#xff0c;直接添加&#xff0c;也可以手动…

国庆节快乐前端(HTML+CSS+JavaScript+BootStrap.min.css)

一、效果展示 二、制作缘由 最近&#xff0c;到了国庆节&#xff0c;自己呆在学校当守校人&#xff0c;太无聊了&#xff0c;顺便做一个小demo帮祖国目前庆生&#xff01;&#xff01;&#xff01; 三、项目目录结构 四、准备工作 (1)新建好对应的文件目录 为了方便&#xff…

【超详细】Python、JDK、vscode安装

Python 下载 首先去Python官网下载安装程序Python官网&#xff0c;鼠标悬浮到Download后选择推荐的Python版本(笔者为Windows系统故选择Windows版本安装程序) 之后点击打开文件&#xff0c;或者点击文件的图标打开下载的目录&#xff0c;打开下载好的安装程序 安装 首先勾…

测试-----BUG篇

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 软件测试的生命周期bug的概念描述bugbug的级别bug的生命周期 软件测试的生命周期 软件测试贯穿与软件的整个生命周期&#xff0c;它的具体流程是: 1.需求分析 2.测…

【初阶数据结构】排序——归并排序

目录 前言归并排序归并排序的非递归写法计数排序排序总结 前言 对于常见的排序算法有以下几种&#xff1a; 前面我们已经学习了&#xff1a; 【初阶数据结构】排序——插入排序【初阶数据结构】排序——选择排序【初阶数据结构】排序——交换排序 下面这节我们来看最后一个…

基于SSM的O20兼职系统的设计与实现(源码+定制+文档)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

P1010 [NOIP1998 普及组] 幂次方 Python题解

[NOIP1998 普及组] 幂次方 题目描述 任何一个正整数都可以用 2 2 2 的幂次方表示。例如 137 2 7 2 3 2 0 1372^7 2^3 2^0 137272320。 同时约定次方用括号来表示&#xff0c;即 a b a^b ab 可表示为 a ( b ) a(b) a(b)。 由此可知&#xff0c; 137 137 137 可表示…