首页 > 代码库 > 深入了解Java虚拟机(3)类文件结构
深入了解Java虚拟机(3)类文件结构
虚拟机执行子系统
一、类文件结构
1.魔数和class版本
1.magic-魔数:0xCAFEBABE;4字节
2.minor_version:次版本,丶之后的数字;2字节
3.major_version:主版本,丶之前的数字;2字节
2.常量池
1.constant_pool_count:常量池常量数量(= 此值 - 1):2字节
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。
2.constant_pool:常量,第一位为类型位,之后的就是按照各自常量的定义:n字节
3.访问标识符
1.access_flags:访问标识
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
如:0x0001 0x0020说明是一个公共的类
4.类索引、父类索引、接口索引:Class文件中由这三项数据来确定这个类的继承关系
1.this_class:类索引:2字节
类索引用于确定这个类的全限定名
2.super_class:父类索引:2字节
父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0
3.interfaces:接口索引:2字节数组
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中
接口索引开头为数量:2字节
查找:
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
5.字段表集合
可以包括的信息有:
字段的作用域(public、private、protected修饰 符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰 符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。
字段名、字段数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
1.access_flags:字段访问标识:u2
2.name_index:字段简单名称:u2
引用常量池常量
3.descriptor_index:方法描叙符:u2
描叙字段:字段类型
描叙方法:(参数列表)描叙符
引用常量池常量
6.方法表集合
7.class、字段、方法等的属性表
预定义的有21种,每种都有自己的结构
1.Code属性
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称
attribute_length指示了属性值的长,所以属性值的长度固定为整个属性表长度减去6个字节。
max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(StackFrame)中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。
code_length和code用来存储Java源程序编译后生成的字节码指令。
code:字节码
exception:异常表
如果当字节码在第start_pc行[1]到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引)
则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理
//Java源码 public int inc(){ int x; try{ x=1; return x; }catch(Exception e){ x=2; return x; }finally{ x=3; } }/ /编译后的ByteCode字节码及异常表 public int inc(); Code: Stack=1,Locals=5,Args_size=1 0:iconst_1//try块中的x=1 1:istore_1 2:iload_1//保存x到returnValue中,此时x=1 3:istore 4 5:iconst_3//finaly块中的x=3 6:istore_1 7:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回 9:ireturn 10:astore_2//给catch中定义的Exception e赋值,存储在Slot 2中 11:iconst_2//catch块中的x=2 12:istore_1 13:iload_1//保存x到returnValue中,此时x=2 14:istore 4 16:iconst_3//finaly块中的x=3 17:istore_1 18:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回 20:ireturn 21:astore_3//如果出现了不属于java.lang.Exception及其子类的异常才会走到这里 22:iconst_3//finaly块中的x=3 23:istore_1 24:aload_3//将异常放置到栈顶,并抛出 25:athrow Exception table: from to target type 0 5 10 Class java/lang/Exception 0 5 21 any 10 16 21 any
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制
2.Exception属性
方法的异常
number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常
exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型
3.LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系它并不是运行时必需的属性,但默认会生成到Class文件之中
可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性
对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点
4.LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性
但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值
5.SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。
在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名
6.ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。
只有被static关键字修饰的变量(类变量)才可以使用这项属性。
对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;
而对于类变量,则有两种方式可以选择:
如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化
如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性
8.Deprecated及Synthetic属性
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@deprecated注释进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,
9.StackMapTable属性
字节码验证
10.Signature属性
泛型被擦出后,获取泛型信息
11.BootstrapMethods属性
BootstrapMethods属性在JDK 1.7发布后增加到了Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。
8.字节码指令简介
1.加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(见第2章关于内存区域的介绍)之间来回传输,这类指令包括如下内容。
将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
扩充局部变量表的访问索引的指令:wide。
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。
3.类型转换指令
Java虚拟机直接支持(即转换时无需显式的转换指令)以下数值类型的宽化类型转换(Widening Numeric Conversions,即小范围类型向大范围类型的安全转换):
int类型到long、float或者double类型。
long类型到float、double类型。
float类型到double类型。
处理窄化类型转换(Narrowing Numeric Conversions)
这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
4.对象访问与创建
创建类实例的指令: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。
5.操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,
包括:
将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
将栈最顶端的两个数值互换:swap
6.控制转移指令
条件分支: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。
7.方法调用和返回
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。
invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,
invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用
8.异常处理指令
在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现
除了用throw语句显式抛出异常情况之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。
例如,在前面介绍的整数运算中,当除数为零时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。
而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经不用了),而是采用异常表来完成的。
9.同步指令
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的
Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义
void onlyMe(Foo f){ synchronized(f){ doSomething(); } } // 指令码 Method void onlyMe(Foo) 0 aload_1//将对象f入栈 1 dup//复制栈顶元素(即f的引用) 2 astore_2//将栈顶元素存储到局部变量表Slot 2中 3 monitorenter//以栈顶元素(即f)作为锁,开始同步 4 aload_0//将局部变量Slot 0(即this指针)的元素入栈 5 invokevirtual#5//调用doSomething()方法 8 aload_2//将局部变量Slow 2的元素(即f)入栈 9 monitorexit//退出同步 10 goto 18//方法正常结束,跳转到18返回 13 astore_3//从这步开始是异常路径,见下面异常表的Taget 13 14 aload_2//将局部变量Slow 2的元素(即f)入栈 15 monitorexit//退出同步 16 aload_3//将局部变量Slow 3的元素(即异常对象)入栈 17 athrow//把异常对象重新抛出给onlyMe()方法的调用者 18 return//方法正常返回 Exception table: FromTo Target Type 4 10 13 any 13 16 13 any
深入了解Java虚拟机(3)类文件结构