JVM Class类文件结构

国庆节快乐            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;}@Overridepublic 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;}@Overridepublic 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.classLast modified 2024年9月30日; size 338 bytesSHA-256 checksum 03b1362989e1f14e5cc4311d6fbca80c2caf91724b4d5ee8eb27a94bfffaa76dCompiled from "Test.java"
public class Testminor version: 0major version: 52flags: (0x0021) ACC_PUBLIC, ACC_SUPERthis_class: #3                          // Testsuper_class: #4                         // java/lang/Objectinterfaces: 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: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   LTest;public int inc();descriptor: ()Iflags: (0x0001) ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: getfield      #2                  // Field m:I4: iconst_15: iadd6: ireturnLineNumberTable:line 4: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       7     0  this   LTest;
}
SourceFile: "Test.java"

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

  public Test();descriptor: ()Vflags: (0x0001) ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       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.rhkb.cn/news/439280.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通信协议感悟

本文结合个人所学&#xff0c;简要讲述SPI&#xff0c;I2C&#xff0c;UART通信的特点&#xff0c;限制。 1.同步通信 UART&#xff0c;SPI&#xff0c;I2C三种串行通讯方式&#xff0c;SPI功能引脚为CS&#xff0c;CLK&#xff0c;MOSI&#xff0c;MISO&#xff1b;I2C功能引…

【api连接ChatGPT的最简单方式】

通过api连接ChatGPT的最简单方式 建立client 其中base_url为代理&#xff0c;若连接官网可省略&#xff1b;配置环境变量 from openai import OpenAI client OpenAI(base_url"https://api.chatanywhere.tech/v1" )或给出api和base_url client OpenAI(api_key&…

冯诺依曼体系结构与操作系统简介

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 冯诺依曼体系结构与操作系统简介 收录于专栏[Linux学习] 本专栏旨在分享学习Linux的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1.…

使用TensorBoard可视化模型

目录 TensorBoard简介 神经网络模型 可视化 轮次-损失曲线 轮次-准确率曲线 轮次-学习率曲线 迭代-评估准确率曲线 迭代-评估损失曲线 TensorBoard简介 TensorBoard是一款出色的交互式的模型可视化工具。安装TensorFlow时,会自动安装TensorBoard。如图: TensorFlow可…

vscode 连接云服务器(ubantu 20.04)

更改服务器系统 如果云服务器上的系统不是ubantu20.04的&#xff0c;可以进行更改&#xff1a; 登录云服务官网&#xff08;这里以阿里云为例&#xff09;点击控制台 点击服务器实例 点击更多操作、重置系统 点击重置为其他镜像、系统镜像&#xff1a;选择你要使用的系统镜像…

解决MySQL报Incorrect datetime value错误

目录 一、前言二、问题分析三、解决方法 一、前言 欢迎大家来到权权的博客~欢迎大家对我的博客进行指导&#xff0c;有什么不对的地方&#xff0c;我会及时改进哦~ 博客主页链接点这里–>&#xff1a;权权的博客主页链接 二、问题分析 这个错误通常出现在尝试将一个不…

随笔(四)——代码优化

文章目录 前言1.原本代码2.新增逻辑3.优化逻辑 前言 原逻辑&#xff1a;后端data数据中返回数组&#xff0c;数组中有两个对象&#xff0c;一个是属性指标&#xff0c;一个是应用指标&#xff0c;根据这两个指标展示不同的多选框 1.原本代码 getIndicatorRange(indexReportLi…

企业级版本管理工具(1)----Git

目录 1.Git是什么 2.Git的安装和使用 在Ubuntu下安装命令如下&#xff1a; 使用git --version查看已安装git的版本&#xff1a; 使用git init初始化仓库&#xff1a; 使用tree .git列出目录&#xff1a; 使用git config命令设置姓名和邮箱&#xff1a; 加入--global选项…

【前端】前端数据转化为后端数据

【前端】前端数据转化为后端数据 写在最前面格式化数组代码解释hasOwnProperty是什么&#xff1f; &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限…

【操作系统】引导(Boot)电脑的奇妙开机过程

&#x1f339;&#x1f60a;&#x1f339;博客主页&#xff1a;【Hello_shuoCSDN博客】 ✨操作系统详见 【操作系统专项】 ✨C语言知识详见&#xff1a;【C语言专项】 目录 什么是操作系统的引导&#xff1f; 操作系统的引导&#xff08;开机过程&#xff09; Windows操作系…

渗透测试入门学习——使用python脚本自动识别图片验证码,OCR技术初体验

写在前面 由于验证码在服务端生成后存储在服务器的session中&#xff0c;而标用于标识用户身份的sessionid存在于用户cookie中 所以本次识别验证码时需要用requests.session()创建会话对象&#xff0c;模拟真实的浏览器行为&#xff0c;保持与服务器的会话才能获取登录时服务…

常用排序算法(下)

目录 2.5 冒泡排序 2.6 快速排序 2.6 1 快速排序思路 详细步骤 2.6 2 快速排序递归实现 2.6 3快速排序非递归&#xff1a; 快排非递归的优势 非递归思路 1. 初始化栈 2. 将整个数组的起始和结束索引入栈 3. 循环处理栈中的子数组边界 4. 单趟排序 5. 处理分区后的子…

【论文速看】DL最新进展20241005-Transformer、目标跟踪、Diffusion Transformer

目录 【Transformer】【目标跟踪】【Diffusion Transformer】 【Transformer】 [NeurlPS 2024] Parameter-Inverted Image Pyramid Networks 机构&#xff1a;清华大学、上海AI Lab、上交、港中文、商汤 论文链接&#xff1a;https://arxiv.org/pdf/2406.04330 代码链接&…

C++ | Leetcode C++题解之第454题四数相加II

题目&#xff1a; 题解&#xff1a; class Solution { public:int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {unordered_map<int, int> countAB;for (int u: A) {for (int v: B) {count…

网络基础 【HTTPS】

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux初窥门径⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a; &#x1f4bb;操作环境&#xff1a; CentOS 7.6 华为云远程服务器 &#x1f339;关注我&#x1faf5;带你学习更多Linux知识…

LabVIEW提高开发效率技巧----调度器设计模式

在LabVIEW开发中&#xff0c;针对多任务并行的需求&#xff0c;使用调度器设计模式&#xff08;Scheduler Pattern&#xff09;可以有效地管理多个任务&#xff0c;确保它们根据优先级或时间间隔合理执行。这种模式在需要多任务并发执行时特别有用&#xff0c;尤其是在实时系统…

软件验证与确认实验一:静态分析

目录 1. 实验目的及要求.................................................................................................... 3 2. 实验软硬件环境.................................................................................................... 3 …

JAVA运用中springBoot获取前端ajax提交参数方式汇总

本篇文章主要讲解springboot获取前端提交的参数信息&#xff0c;后端进行接受的常见方法汇总&#xff0c;通过本篇文章你可以快速掌握对表单和连接参数获取的能力。 作者&#xff1a;任聪聪 日期&#xff1a;2024年10月5日 一、delete、get等url参数获取方式 前台提交&#xf…

数字图像处理:空间域滤波

1.数字图像处理&#xff1a;空间域滤波 1.1 滤波器核&#xff08;相关核&#xff09;与卷积 图像上的邻域计算 线性空间滤波的原理 滤波器核&#xff08;相关核&#xff09;是如何得到的&#xff1f; 空间域的卷积 卷积&#xff1a;滤波器核与window中的对应值相乘后所有…

【Echarts】折线图和柱状图如何从后端动态获取数据?

&#x1f680;个人主页&#xff1a;一颗小谷粒 &#x1f680;所属专栏&#xff1a;Web前端开发 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 1.1 前端数据分析 1.2 数据库表分析 1.3 后端数据处理 1.4 前端接收数据 继上一篇文章&…