Java类文件结构

为了节省空间,类文件中没有任何分隔符,各个数据项都是一个挨着一个紧凑排列的,所以其中无论是顺序还是数量等都是严格规定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。下面我们先看一下类文件的整体结构:

 

 

其中常量、接口、字段、方法和属性在其中按各自的结构紧密排列,个数由其前面的数量字段决定。同时类文件中最小单位为1个字节,超过一个字节的数据以大端方式存储。

魔数

魔数是用来确定文件的类型是否是class文件,因为只靠文件扩展名来确定文件类型并不可靠。

这个魔数占文件的开始4个字节,为CA FE BA BE。(注意:这里的字面代表的是十六进制数,而不是ASCII码)

版本号

接下来的4个字节为class文件版本号,其中前两个字节表示的是次版本号,后两个字节表示的是主版本号(从45开始)。

虚拟机可以向下兼容运行class文件,但不能运行高于其版本的class文件。

常量池

由于常量池中的常量数量是不确定的,所以在常量池的入口需要有两个字节用来代表常量池容量计数值(常量池索引从1开始)。

一共有14种常量类型,有着各自对应的结构,但开始的一个字节同样都是表示标志位,用来区分不同的类型。

下面为14种常量的具体类型和对应的标志位:

 

 

每种类型的结构如下(其中u1表示1个字节,u2表示2个字节,其他同理)

 

 

 

 

读取常量池的时候首先读取标志位,判断常量类型,就可以知道对应的结构,获取对应的信息了。

查看命令:

javap -verbose TestClass.class

访问标志

常量池之后的两个字节代表访问标志,即这个class是类还是接口,是否为public等的信息。不同的含义有不同的标志值(没有用到的标志位一律为0。),具体信息如下:

 

 

类索引

类索引占两个字节,分别指向常量池中的CONSTANT_Class_info类型的常量,这个类型的常量结构见常量池中的图表,其中包含一个指向全限定名常量项的索引。

父类索引

因为java只允许单继承,所以只有一个父类,具体内容同上-类索引。

接口索引

接口索引开始两个字节用来表示接口的数量,之后的每两个字节表示一个接口索引,用法同类索引与父类索引。

字段

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

字段域的开始两个字节表示字段数量,之后为紧密排列的字段结构体数据,其结构如下:

 

其中的字段和方法的描述符,对于字段来说用来描述字段的数据类型;而对于方法来说,描述的就是方法的参数列表(包括数量、类型以及顺序)和返回值,这个描述顺序也是固定的,必须是参数列表在前,返回值在后,参数列表必须放在一组小括号内。同时为了节省空间,各种数据类型都使用规定的一个字母来表示,具体如下:

 

对象使用L加上对象的全限定名来表示,而数组则是在每一个维度前添加一个"["来描述。

属性表在之后进行介绍。

方法

class文件中对方法的描述与以前对字段的描述几乎采用了完全一致的方式,唯一的区别就是访问类型不完全一致。

属性

java7中预定义了21项属性,具体内容限于篇幅不再列出。

对于每个属性的结构,没有特别严格的要求,并且可以自定义属性信息,jvm运行时会忽略不认识的属性。

符合规范的属性表基本结构如下:

 

 

 

其中前两个字节为指向常量池中的CONSTANT_Utf8_info类型的属性名称,之后4个字节表示属性值所占用的位数,最后就是具体属性了。

 其中有一个比较重要的名称为「Code」的属性为方法的代码,即字节码指令。

 

Code属性表结构如下:

 

 

 

字节码指令

由于 Java 虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。

  • 除了 long 和 double 类型外,每个变量都占局部变量区中的一个变量槽(slot),而 long 及 double 会占用两个连续的变量槽。
  • 大多数对于 boolean、byte、short 和 char 类型数据的操作,都使用相应的 int 类型作为运算类型。

加载和存储指令

1、将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
2、将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
3、将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
4、扩充局部变量表的访问索引的指令:wide。

运算指令

1、运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
2、算术指令分为两种:整型运算的指令和浮点型运算的指令。
3、无论是哪种算术指令,都使用 Java 虚拟机的数据类型,由于没有直接支持 byte、short、char 和 boolean 类型的算术指令,使用操作 int 类型的指令代替。

加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

类型转换指令

1、类型转换指令可以将两种不同的数值类型进行相互转换。
2、这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

宽化类型转换

int 类型到 long、float 或者 double 类型。
long 类型到 float、double 类型。
float 类型到 double 类型。
i2l、f2b、l2f、l2d、f2d。

窄化类型转换
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。

还有:
对象创建与访问指令:new、newarray、anewarray、multianewarray
操作数栈管理指令:pop、dup、dup2_x1、swap
控制转移指令:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、goto
方法调用和返回指令:invokevirtual、invokeinterface、invokespecial、invokestatic 、invokedynamic

 

可以用工具查看

javap -c -verbose TestJava >a

vi a

 

参考链接:

https://www.cnblogs.com/zawier/p/6659311.html

https://www.jianshu.com/p/c4a727827896

《深入理解Java虚拟机》

 

posted @ 2021-01-08 17:08  diameter  阅读(171)  评论(0编辑  收藏  举报