类的结构

1. Class类文件结构

类文件结构

1.1 与类或接口的关系

对应关系

任何1个Class文件都对应着唯一1个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

1.2 结构

结构

1.2.1 格式

格式

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

1.2.2 结构

结构

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF8编码构成的字符串值。

表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以"_info"结尾。用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

1.3 内容

内容

1.魔数

魔数

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

Class文件的魔数值为:0xCAFEBABE(咖啡宝贝?)。

2.版本

版本

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK1.01.1使用了45.045.3的版本号),高版本的JDK能向下兼容``以前版本的Class文件,但不能运行``以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

3. class文件的常量池

常量池
class文件的常量池是干什么用的?

可以理解为Class文件中的资源仓库。

常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符
为什么变量和方法名不能太长?

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

可以使用javap工具的-verbose参数来输出class文件字节码内容。

4. 访问标志

紧接着常量池之后的2个字节是:访问标志(access_flags)。
用于:识别一些类或者接口层次的访问信息。

具体的标志位以及标志的含义见下表:

访问标志

access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。

5. 类索引、父类索引与接口索引集合

访问标志之后是:类索引、父类索引与接口索引集合。
用于:确定这个类的继承关系。

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。

类、父类索引和接口索引集合

父类索引:由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。

接口索引集合:这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

6. 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。

字段表集合

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

重名

在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。

7. 方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。

方法表集合
方法里的Java代码存放位置

方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的属性里面。

与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器"<clinit>"方法和实例构造器"<init>"方法。

重载

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。

特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。(面试题:方法返回值不同能否重载?答案是Java语言层面不能,而字节码层面能)

方法特征签名:
在《Java虚拟机规范(第2版)》的"§4.4.4Signatures"章节及《Java语言规范(第3版)》的"§8.4.2MethodSignature"章节中都分别定义了字节码层面和Java代码层面的方法特征签名。

  • Java代码的方法特征签名只包括了方法名称、参数顺序及参数类型。
  • 字节码的特征签名还包括方法返回值以及受查异常表。

8. 属性表集合

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

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,在《Java虚拟机规范(JavaSE7)》版中,预定义了21项虚拟机实现应当能识别的属性。

8.1 code 属性
8.2 Exception 属性
8.3 LineNumberTable 属性
8.4 LocalVariableTable 属性
8.5 SourceFile 属性
8.6 ConstantValue 属性
8.7 InnerClasses 属性
8.8 Deprecated及Synthetic属性
8.9 StackMapTable 属性
8.10 Signature 属性
8.11 BootstrapMethods 属性

2. 字节码指令

Java虚拟机的指令由1个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

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

字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了Java虚拟机操作码的长度为1个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。

2.1 字节码与数据类型

字节码按用途分为9类。

加载和存储指令

用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。

  • 加载:把数据从局部变量表加载到操作数栈。
  • 存储:把数据从操作数栈存储到局部变量表。

运算指令

用于对2个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。

大体上算术指令可以分为2种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。

  • 加法指令: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。

类型转换指令

将两种不同的数值类型进行相互转换。

这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理本节开篇所提到的字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。

对象创建与访问指令

JVM对类实例和数组的创建与操作使用了不同的字节码指令。

对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下:

  • 创建类实例的指令:new。
  • 创建数组的指令:newarray、anewarray、multianewarray。
  • 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic。
  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
  • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
  • 取数组长度的指令:arraylength。
  • 检查类实例类型的指令:instanceof、checkcast。

操作数栈管理指令

JVM提供了用于直接操作操作数栈的指令,包括:

  • 将操作数栈的栈顶1个或2个元素出栈:pop、pop2。
  • 复制栈顶1个或2个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
  • 将栈最顶端的2个数值互换:swap。

控制转移指令

可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。
从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
  • 复合条件分支:tableswitch、lookupswitch。
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret。

方法调用和返回指令

方法调用指令

与数据类型无关。列举5条:

  • invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
  • invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic指令用于调用类方法(static方法)。
  • invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法返回指令

根据返回值的类型区分的,包括

  • ireturn(当返回值是boolean、byte、char、short和int类型时使用)、
  • lreturn、
  • freturn、
  • dreturn、
  • areturn、
  • 另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

在Java程序中显式抛出异常的操作(throw语句)由athrow指令来实现,除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用异常表来完成。

同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时 自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit2条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机2者共同协作支持。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容