深入了解Java虚拟机类文件结构
Posted 寰殇丶天使
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解Java虚拟机类文件结构相关的知识,希望对你有一定的参考价值。
虚拟机执行子系统
一、类文件结构
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属性
对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点
line_number_table:数量为line_number_table_length、类型为line_number_info的集合,
line_number_info:表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号
4.LocalVariableTable属性
LocalVariableTable:描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性
但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值
local_variable_info:代表了一个栈帧与源码中的局部变量的关联
start_pc和length:分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。
name_index和descriptor_index:指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符。
index:这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个
泛型的支持属性
在JDK 1.5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable
这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),
对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉
描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable。
5.SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。
在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名
sourcefile_index:指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名
6.ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。
只有被static关键字修饰的变量(类变量)才可以使用这项属性。
对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;
而对于类变量,则有两种方式可以选择:
如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化
如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化
ConstantValue属性是一个定长属性
attribute_length:数据项值必须固定为2。
constantvalue_index:数据项代表了常量池中一个字面量常量的引用
根据字段类型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性
number_of_classes:代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述
inner_class_info_index和outer_class_info_index:指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。
inner_name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称,如果是匿名内部类,那么这项值为0。
inner_class_access_flags:内部类的访问标志,类似于类的access_flags
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虚拟机类文件结构的主要内容,如果未能解决你的问题,请参考以下文章
《深入理解Java虚拟机》-----第7章 虚拟机类加载机制——Java高级开发必须懂的