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个字节)被标记为天蓝色.
其内容为:"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/jclasslibhttps://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查看其字节码文件如下:
我们回顾一下之前的讲解:
- 00 ~ 03 是 java字节码文件的标识(魔数)
- 04~05 是次版本号
- 06 ~ 07 是主版本号 34(hex) 对应的10进制是52, 正好对应着JDK1.8
- 08 ~ 09 对应着常量池中常量项的个数. 在上图的Test.class中对应的16进制是16, 转换成10进制就是22, 也就是说常量池中存在着22个常量项, 通过jclasslib查看如下:
虽然是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进制 ...
访问标志位占用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是不是很眼熟, 这个跟类的访问标识符很像, 但是他们的值有不同的含义, 如下:
显然我们再编写代码的时候, 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 Specificationshttps://docs.oracle.com/javase/specs/index.html
- Java虚拟机规范 Java SE 8版
相关文章:
JVM Class类文件结构
国庆节快乐 2024年10月2日17:49:22 目录 前言 magic 数 文件版本 使用JClassLib观察class文件 一般信息 接口 常量池 字段 方法 常量池计数器 常量池 类型 CONSTANT_Methodref_info CONSTANT_Class_info 类型结构总表 访问标志 类索引, …...
解决 GitHub 文件大小限制的问题
要解决 GitHub 文件大小限制的问题,可以使用 Git Large File Storage (Git LFS)。以下是设置步骤: 安装 Git LFS: 对于 macOS:brew install git-lfs对于 Windows:从 Git LFS官网 下载并安装。 初始化 Git LFSÿ…...
wordpress源码资源站整站打包32GB数据,含6.7W条资源数据
源码太大了,足足32gb,先分享给大家。新手建立资源站,直接用这个代码部署一下,数据就够用了。辅助简单做下seo,一个新站就OK了。 温馨提示:必须按照顺序安装 代码下载...
金融领域的人工智能——Palmyra-Fin 如何重新定义市场分析
引言 DigiOps与人工智能 正在改变全球各行各业,并带来新的创新和效率水平。人工智能已成为金融领域的强大工具,为市场分析、风险管理和决策带来了新方法。金融市场以复杂性和快速变化而闻名,人工智能处理大量数据并提供清晰、可操作的见解的…...
STL--string类
我们从这篇文章之后就正式开始学习STL的string,字面看起来是不是像C语言里面的字符串之类的处理方法,是的,C里面也是对字符串的一些处理函数,但是C有很多这样的函数,给大家推荐一个网站 ,这个网站是C的官网…...
iptables 的NDAT报错bash: 9000: command not forward
外网主机设置: iptables -t nat -A PREROUTING -d 192.168.3.51 -p tcp --dport 9000 -j DNAT --to-destination 192.168.3.61:22 本地shell连接: PS C:> ssh root192.168.3.51 9000 显示如下操作: PS C:> ssh root192.168.3.51 9000…...
快速了解:MySQL InnoDB和MyISAM的区别
目录 一、序言二、InnoDB和MyISAM对比1、InnoDB特性支持如下2、MyISAM特性支持如下 三、两者核心区别1、事务支持2、锁机制3、索引结构4、缓存机制5、故障恢复6、使用场景 一、序言 在MySQL 8.0中,InnoDB是默认的存储引擎。除了InnoDB,MySQL还支持其它的…...
TI DSP TMS320F280025 Note14:模数转换器ADC原理分析与应用
TMS320F280025 模数转换器ADC原理分析与应用 ` 文章目录 TMS320F280025 模数转换器ADC原理分析与应用逐次比较型ADC和双积分型ADC工作原理逐次比较型 ADC双积分型 ADC280025ADCADC原理分析ADC时钟SOCSOC内部原理ADC触发方式ADC采集(采样和保持)窗口通道寄生电容基准电压发生器模…...
【C++前缀和】2845. 统计趣味子数组的数目|2073
本文涉及的基础知识点 C算法:前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode 2845. 统计趣味子数组的数目 难度分:2073 给你一个下标从 0 开始的整数数组 nums ,以及整数 modulo 和整数 k 。 请你找出并统计数组…...
C++入门基础 (超详解)
文章目录 前言1. C关键字2. C的第一个程序3. 命名空间3.1 namespace的定义3.2 命名空间的嵌套3.3 命名空间使用3.4 查找优先级总结 4. C输入和输出4.1 标准输入输出 (iostream库)4.2 文件输入输出 (fstream库)4.3 字符串流 (sstream库)4.4 C格式化输出4.5 std::endl和\n的区别 …...
docker零基础入门教程
注意 本系列文章已升级、转移至我的自建站点中,本章原文为:Docker入门 目录 注意1.前言2.docker安装3.docker基本使用4.打包docker镜像5.docker进阶 1.前言 如果你长期写C/C代码,那你应该很容易发现C/C开源项目存在的一个严重问题ÿ…...
【Java SE 题库】移除元素(暴力解法)--力扣
🔥博客主页🔥:【 坊钰_CSDN博客 】 欢迎各位点赞👍评论✍收藏⭐ 目录 1. 题目 2. 解法(快慢“指针”) 3. 源码 4. 小结 1. 题目 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素。元素的顺…...
linux文件编程_进程
1. 进程相关概念 面试中关于进程,应该会问的的几个问题: 1.1. 什么是程序,什么是进程,有什么区别? 程序是静态的概念,比如: 磁盘中生成的a.out文件,就叫做:程序进程是…...
java NIO实现UDP通讯
NIO Udp通讯工具类 import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Iterator;impo…...
ffmpeg如何实现视频推流?
FFmpeg是一个强大的多媒体框架,用于处理视频和音频数据。它包括了libavcodec(用于解码和编码)、libavformat(用于格式转换)、libavutil(提供一些辅助工具和函数)、libavfilter(用于音视频过滤)等多个库。 以下这些都是FFmpeg的特性 FFmpeg支持大量的音视频编解码器&…...
【HTML5】html5开篇基础(3)
1.❤️❤️前言~🥳🎉🎉🎉 Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的…...
echarts实现3D柱状图(视觉层面)根据博主改编
https://blog.csdn.net/weixin_57798646/article/details/131067725 这是原贴 在这个基础上我需要实现 一根柱子 代码如下 <!DOCTYPE html> <html lang"en" style"height: 100%"><head><meta charset"utf8"> </hea…...
【一篇文章理解Java中多级缓存的设计与实现】
文章目录 一.什么是多级缓存?1.本地缓存2.远程缓存3.缓存层级4.加载策略 二.适合/不适合的业务场景1.适合的业务场景2.不适合的业务场景 三.Redis与Caffine的对比1. 序列化2. 进程关系 四.各本地缓存性能测试对比报告(官方)五.本地缓存Caffine如何使用1. 引入maven依…...
OpenSource - 开源WAF_SamWaf
文章目录 PreSafeLine VS SamWaf开发初衷软件介绍架构界面主要功能 使用说明下载最新版本快速启动WindowsLinuxDocker 启动访问升级指南自动升级手动升级 在线文档 代码相关代码托管介绍和编译已测试支持的平台测试效果 安全策略问题反馈许可证书贡献代码 Pre Nginx - 集成Mod…...
旅游避坑指南
1.火车站旁白的小摊贩,还有周边的小饭店百分之百是黑店,不仅难吃要死而且巨黑!!! 可以地图上搜索附近的大型商超,例如泰安市的银座商超,里面的东西不仅好吃而且价格透明,还有很多当…...
矩阵系统源码搭建的具体步骤,支持oem,源码搭建
一、前期准备 明确需求 确定矩阵系统的具体用途,例如是用于社交媒体管理、电商营销还是其他领域。梳理所需的功能模块,如多账号管理、内容发布、数据分析等。 技术选型 选择适合的编程语言,如 Python、Java、Node.js 等。确定数据库类型&…...
正则表达式调试工具实战
正则表达式调试工具实战 1、新建工程QWidget工程工程名RegexTool 如果QT不会配置,请参考我的博客,QT配置 Widget.cpp 默认内容如下 2、主界面设计 三行两列,每行采用HBoxLayout作为行布局控件,内部一个Lable控件和一个TextEdit控件,采用VBoxLayout 控件包裹三个HBoxLa…...
SQL:函数以及约束
目录 介绍 函数 字符串函数 数值函数 日期函数 流程函数 约束 总结 介绍 说到函数我们都不陌生,在C,C,java等语言中都有库函数,我们在平时也是经常使用,函数就是一段代码,我们既可以自定义实现,又可以使用库里内置的函数;从来更加简洁方便的完成业务;同样的在SQL中也有…...
在Linux中将设备驱动的地址映射到用户空间
本期主题: MMU的简单介绍,以及如何实现设备地址映射到用户空间 往期链接: Linux内核链表零长度数组的使用inline的作用嵌入式C基础——ARRAY_SIZE使用以及踩坑分析Linux下如何操作寄存器(用户空间、内核空间方法讲解)…...
电脑自带dll修复在哪里,dll丢失的6种解决方法总结
在现代科技日新月异的时代,电脑已经成为我们生活中不可或缺的一部分。然而,在使用电脑的过程中,我们常常会遇到一些常见的问题,其中之一就是dll文件丢失或损坏。当这些dll文件丢失或损坏时,可能会导致某些应用程序无法…...
k8s基于nfs创建storageClass
首先安装nfs #服务端安装 yum install -y nfs-utils rpcbind #客户端安装 yum install -y nfs-utils #启动服务 并设置开启启动 systemctl start rpcbind && systemctl enable rpcbind systemctl start nfs && systemctl enable nfs #创建共享目录 mkdir -p /…...
Chrome无法拖入加载.crx扩展文件(以IDM为例)
问题原因:新版本的Chrome浏览器已不支持加载.crx文件 解决办法:将.crx文件压缩为.zip文件,解压缩后再加载到Chrome中 以IDM的.crx文件作为示例; IDM的.crx文件位于C:\Program Files (x86)\Internet Download Manager; 将IDMGCE…...
数字教学时代:构建高效在线帮助中心的重要性
在数字化教学日益普及的今天,教育领域正经历着前所未有的变革。随着在线课程、虚拟教室、智能学习平台等数字化工具的广泛应用,教育资源的获取方式和学习模式发生了深刻变化。然而,这种变革也带来了新的挑战,其中之一便是如何确保…...
828华为云征文|华为云弹性云服务器FlexusX实例下的Nginx性能测试
本文写的是华为云弹性云服务器FlexusX实例下的Nginx性能测试 目录 一、华为云弹性云服务器FlexusX实例简介二、测试环境三、测试工具四、测试方法五、测试结果 下面是华为云弹性云服务器FlexusX实例下的Nginx性能测试。 一、华为云弹性云服务器FlexusX实例简介 华为云弹性云服…...
知识图谱入门——2:技术体系基本概念:知识表示与建模、知识抽取与挖掘、知识存储与融合、知识推理与检索
知识图谱是通过构建“实体”和“关系”来描述世界的信息网络,它不仅是数据的存储方式,还可以支持推理与查询,帮助系统更好地理解、整合和利用数据。 文章目录 1. 知识表示与建模2. 知识抽取与挖掘3. 知识存储与融合4. 知识推理与检索总结 1.…...
合肥新站开发区管委会网站/武汉seo论坛
使用技术 LR.APP是基于uni-app开发的多端APP/小程序系统,设计理念是解决多端开发问题,使用时,开发者仅需一套代码,即可编译到iOS、Android、H5、小程序等多个平台。 LR.APP封装了跨端兼容的组件和api,如果你不熟悉un…...
家具定制东莞网站建设/seo博客教程
1.介绍Kaldi语音识别工具将HTK比较零碎的各种各样的指令和功能进行整理集合,使用perl脚本调用。同时也加入了深度神经网络的分类器(DNN),本身由原来做HTK开发的人员制作而成,可以说是HTK的升级加强版。kaldi官方网站请见:http://k…...
兴仁企业建站公司/谷歌手机版下载安装
01. ip address show/ip a 检查网卡地址配置02. ping 测试网络连通性03. nmtui 图形界面修改网卡地址信息04. exit 注销05. shutdown 关机命令shutdown -h 5 指定关机时间 (推荐)shutdown -r 5 重启主机时间 (推荐)shutdown -…...
模板网站建设公司/免费奖励自己的网站
8.4.6 用编程方式添加DataTable行 在为DataTable定义了架构之后,也就是设置好了需要的列名以后,就可以可通过将DataRow对象添加到表的Rows集合中来将数据行添加到表中。与添加DataColumn类似,同样可以通过使用DataRow构造函数,或…...
微网站免费建站系统/互联网广告投放代理公司
使用RD Client来远程桌面 可能你会觉得奇怪,team viewer和向日葵之类的难道不香吗?看起来他们两个都是实现了远程桌面的功能,好像没必要特地用Windows自带的RD Client进行内网穿透之后远程桌面。 实际上team viewer之类的在我的使用范围内不…...
做网站制作较好的公司/成都疫情最新消息
一.简介 0. 页面的生命周期。 1. WebForm后台页面类继承于Page类,Page类实现了IHttpHandler接口。 2. 前台页面类继承于后台页面类。 3. 先调用PageLoad方法,再调用Render方法生成html代码。 二. 加密安全 互联网没有绝对的安全,登…...