Java虚拟机翻译.class文件的理解?

讲解了Java类和对象在内存中的表示机制,Java对象是根据Java类创建的,表示一个Java类实例;Java类是根据Class文件创建的,Class文件被类加载器加载、链接、初始化后就变成Hotspot中的Klass。注意这里的Class文件是指符合Class文件格式规范的字节流,并不特指磁盘文件形式的以.class结尾的class文件,如类加载器也可读取网络字节流形式的Class文件。Class文件格式是JVM自己定义的用于表示Java类的二进制字节流规范,与操作系统本身无关,该文件格式正是Java代码一次编译,跨平台运行的关键,其他的与底层操作系统强耦合的编译执行的语言如C/C++需要在每个操作系统上都编译一遍才能正常执行,因为不同操作系统基本都有特定的汇编器和链接器,支持的二进制文件格式也可能不同。那么类的字段,方法,继承的父类,实现的接口等这些信息在Class文件中是如何组织和表示的了?方法对应的字节码是是如何运行的?

每个class文件都有一个ClassFile结构,称为Class文件格式,如下图:


magic: 魔数,用于标识当前Class文件的文件格式,JVM可据此判断该文件是否可以被解析,目前固定为0xCAFEBABE

constant_pool:常量池,是一种表结构,包含class文件结构和子结构中引用的所有字符串常量,类或者接口名,字段名和其他常量,其有效索引范围是1- (constant_pool_count-1)。其中类和接口名采用全限定形式,即在整个JVM中的绝对名称,如java.lang.Object,方法名,字段名、局部变量名和形参名都采用非限定名,即在源代码文件中使用相对名称,如属性名name。

access_flags:用于表示某个类或者接口的访问权限和属性

this_class:类索引,该值必须是对常量池中某个常量的一个有效索引值,该索引处的成员必须是一个CONSTANT_Class_info类型的结构体,表示这个class文件所定义的类和接口

super_class:父类索引,同this_class,该值必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,如果该值为0,则只能表示java.lang.Object类,因为该类是唯一一个没有父类的类。

interfaces_count:接口计数器,表示当前类或者接口的直接超接口的数量

interfaces:接口表,是一个表结构,每个成员同this_class,必须是对常量池中CONSTANT_Class_info类型常量的一个有效索引值,其有效索引范围为0~interfaces_count,接口表中成员的顺序与源代码中给定的接口顺序是一致的,interfaces[0]表示源代码中最左边的接口。

fields_count:字段计数器,当前class文件所有字段的数量

fields:字段表,是一个表结构,表中每个成员必须是filed_info数据结构,用于表示当前类或者接口的某个字段的完整描述,不包含从父类或者父接口继承的字段

methods_count:方法计数器,表示当前类方法表的成员个数

methods:方法表,是一个表结构,表中每个成员必须是method_info数据结构,用于表示当前类或者接口的某个方法的完整描述,包含当前类或者接口定义的所有方法,如实例方法、类方法、实例初始化方法等,不包含从父类或者父接口继承的方法

attributes:属性表,是一个表结构,表中每个成员必须是attribute_info数据结构,这里的属性是对class文件本身,方法或者字段的补充描述,如SourceFile属性用于表示class文件的源代码文件名。

描述符有两种,字段描述符和方法描述符,本质就是一个基于特定规则的字符串,其中字段描述符用来表示类,实例和局部变量的类型,具体如下:


如I表示一个int变量,Ljava.lang.Object;表示一个Object实例,[[I表示一个二维int数组实例。方法描述符包含一个或者多个参数描述符合一个返回值描述符,参数描述符和返回值描述符都是上面的字段描述符,再加一个特殊的V,表示该方法不返回任何值。如方法Object m(int i, double d, Thread t)

Java虚拟机指令不依赖类,接口,类实例或数组的运行时内存布局,而是依赖依赖常量池表中的符号信息,常量池表中所有项都有如下通用格式:


其中tag作为类型标记,用于确定后面的info的格式,tag是一个字节,info是两个或者多个字节,取决于tag的值,如下图:


其中access_flags表示字段的访问权限和属性,是个由标识构成的掩码,所谓标识就是某个特定的二进制位,取值为1表示开启,0表示关闭,各标识开启后的含义如下:



access_flags表示方法的访问权限和基本属性,同filed_info,也是一个由标识构成的掩码,各标识开启的含义如下:


其中attribute_name_index是常量池中一个类型为CONSTANT_Utf8_info的有效索引,表示该属性的属性名,attribute_length表示后面的info信息的字节长度,这个长度不包括attribute_name_index和attribute_length的6字节。Java8预定义了23种属性,用户在编译源代码文件时可以添加新的属性,只要JVM实现能够正确识别该属性即可,注意用户自定义的属性不能使用这些预定义属性的属性名,各属性的作用如下:

Code:位于method_info的属性表中,表示该方法的虚拟机指令及辅助信息,method_info中有且仅有一个Code属性,其结构如下:

其中max_stack表示当前方法操作数栈的最大深度;max_locals表示此方法引用局部变量表中的局部变量的个数,包含传递方法入参的局部变量;code_length表示后面的code数组的字节长度;code数组表示当前方法的虚拟机指令的数据;exception_table_length表示后面的exception_table数组的长度;exception_table中表示此方法的捕获的各异常的异常处理逻辑,每个成员对应一个异常类型,每个成员包含4个属性,start_pc, end_pc表示try/catch的代码范围,具体来说是起止代码对应的虚拟机指令在code数组中的索引,handler_pc是异常处理逻辑的代码的虚拟机指令在code数组中的索引,catch_type是常量池中一个类型为CONSTANT_Class_info的有效索引,表示捕获的异常类型。

StackMapTable:位于Code属性的属性表中,最多只能包含一个,用于虚拟机的类型检查验证阶段,验证某个局部变量的类型与操作数栈顶所需的核查类型是否一致。

Exceptions:位于method_info的属性表,表示该方法可能抛出的受检异常的异常类型。

InnerClasses: 位于ClassFile的属性表中,表示该类定义的内部类信息,如果有内部类,则有且仅有一个InnerClasses属性

Synthetic:位于ClassFile,method_info或者filed_info结构的属性表中,表示该成员没有在源文件中出现,如编译器自动添加的默认构造方法。

SourceFile: 位于ClassFile的属性表中,表示该class文件对应的源代码文件的文件名

LineNumberTable:位于Code的属性表中,表示虚拟机指令同源文件代码行的对应关系,注意LineNumberTable与源文件的代码行没有一一对应关系,可能多个LineNumberTable属性对应同一个代码行,且LineNumberTable的属性顺序是任意的。

LocalVariableTable:位于Code的属性表中,表示方法的局部变量,每个局部变量最多对应一个LocalVariableTable属性,Code中的多个LocalVariableTable属性的顺序是任意的。每个局部变量通过5个属性表示,start_pc和length表示该局部变量的作用域范围,start_pc是Code数组的索引,name_index属性表示局部变量的变量名,descriptor_index表示该变量的字段描述符,index表示该变量在局部变量表中的索引

RuntimeVisibleAnnotations:位于ClassFile,method_info或者filed_info结构的属性表中,最多只能含有一个,表示加在此成员声明上面的运行时可见的注解,注解用annotation结构表示,保存了注解的多个键值对属性。

MethodParameters:位于method_info结构的属性表中,用来记录方法的形参的个数,形参名,形参是否final等。

其中最重要的是 line_number_table 数组,该数组元素包含如下 两个成员变量:

  • line_number:为 start_pc 对应源文件代码的行号。需要注意的是,多个 line_number_table 元素可以指向同一行代码,因为一行 Java 代码很可能被编译成多条指令。

LocalVariableTable 属性用于 描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中, 可以在 javac 中分别使用 -g:none 或者 -g:vars 选项生成该信息。

需要注意的是,每个非 static 函数都会自动创建一个叫做 this 的本地变量,代表当前是在哪个对象上调用此函数。
并且,this 对象是位于局部变量数组第1个位置(即 Slot = 0),它的作用范围是贯穿整个函数的。

说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。

JVM 在调用一个函数的时候,会创建一个局部变量数组(即 LocalVariableTable),而 Slot 则表示当前变量在数组中的位置。

C/C++的方法会被编译成特定于CPU架构的汇编指令,然后交由CPU逐一执行,因为汇编指令与CPU架构是强绑定的,所以C/C++程序在执行前需要在不同CPU架构的机器上编译一遍。Java为了实现一处编译,跨平台运行的目标,在汇编指令之上引入了一个独立于平台的中间层,虚拟机指令,由Java虚拟机规范提供指令标准定义,由Java虚拟机厂商提供指令实现,不同平台的Java虚拟机都遵循相同的指令集规范,从而实现跨平台运行目标。一个方法对应的一组虚拟机指令称为这个方法的字节码(byte

一条虚拟机指令由一个指定要完成的操作的操作码和表示待操作值的的零或者多个操作数构成,所谓操作数就是该指令的入参。操作数可能在编译器产生并嵌入到字节码指令中,即在内存中操作数紧挨着字节码指令,也可能在运行期通过计算得出保存在操作数栈中,这种情形下虚拟机指令的操作数的个数为0,直接从操作数栈中读取操作数,通常是栈顶的N个元素。所谓操作数栈就是保存虚拟机指令执行结果的一个后进先出的栈,由Java虚拟机维护,其功能类似于汇编指令执行过程中使用的进程栈或者内核栈。

 Java虚拟机规范定义指令时使用了一张指令格式表,包含助记符,操作码,指令格式,操作数栈,功能描述,链接时异常,运行时异常,注意共8项,class文件中的虚拟机指令就是数字形式的操作码,助记符是对该数字的含义的简单描述,指令格式描述该指令后面的操作数,操作数栈描述该指令执行时操作数栈的状态和执行完成操作数栈的状态,功能描述项会详细描述该指令在各种场景下的处理逻辑,链接时异常项描述在链接环节执行该指令校验时应该抛出的异常类型,运行时异常项描述了运行时该指令执行过程中应该抛出的异常类型。

以无操作数的aload指令和有一个操作数的iload指令为例说明:


图中的50是该指令的十进制的操作码,0x32是十六进制的;图中的箭头表示栈顶,aload指令执行时操作数栈的栈顶的两个元素必须是arrayref和index,其中arrayref表示一个组件类型为reference的数组,即对象数组,是待读取的目标数组,index表示待读取数组元素的索引,arrayref和index的类型校验在链接环节的类型检查时完成。

iload的操作数index表示当前栈帧中的局部变量的索引,iload指令将该索引处的int值压入到操作数栈栈顶,index的值在编译期确定并随iload指令一起写入到class文件中。
在class文件中,某个Java方法的虚拟机指令保存在表示该方法的method_info结构的Code属性的code项中,code项是一个byte数组,code_length项描述了该数组的长度。Java8定义的虚拟机指令总共有204个,其中3个是Java虚拟机内部使用的保留指令,因此指令操作码用一个字节即可表示,每个指令后面的字节按照规范表示该指令的操作数,操作数的类型和字节数在规范中都有定义(指令格式表中格式项每一行都表示一个字节),Java方法对应的一组指令按照指令执行的先后顺序依次保存,code[0]就是第一条执行的指令。解析code数组包含的虚拟机指令时,先从code[0]读取第一条指令,判断其是否有操作数及其占用的字节数,如果有则读取指定字节数的操作数,如果没有或者操作数读取完毕则下一个字节是下一条虚拟机指令,依次往下循环即可,在class文件校验环节会检查code数组是否合法完整。

在《Hotspot Klass模型——Java类内存表示机制》中讲到方法的字节码实际保存在ConstMethod对象的后面,可以通过HSDB具体的字节码及其内存数据,测试代码:

其中line表示行号,bci表示该字节码在code数组中的索引。再用Inspect,根据方法的地址找到该方法的ConstMethod对象的地址,如下图:


接着用CLHSDB的mem命令查看ConstMethod对象的内存,不过先得执行printas命令查看该对象的内存大小,从而确定该方法字节码在内存中的起始位置,如下图:

该对象的大小是48字节,即6个字段,从第7个字宽开始即是字节码的内存起始位置。字节码的内存大小是37字节,即5个字宽,用mem命令查看这5个字宽的内存数据,如下图:

Intel按照小端的方式存储数据,因为mem命令打印内存数据时做了一道转换,所以实际的内存数据与上图打印的结果是相反的,以第一行0xbc 06 3d 06 3c 05 3b 04为例,实际存储和读取的顺序是从04开始的,从右往左,一直到bc,然后从下一行的末尾的0a开始。将内存数据与上面的虚拟机指令的操作码逐一比对,结果如下:

istore_0 3b 将栈顶的int变量放入局部变量表索引为0的位置,即变量a的初始化
istore_1 3c 将栈顶的int变量放入局部变量表索引为1的位置,即变量a2的初始化
dup 59 复制栈顶的操作数并压入栈顶,此时栈顶的操作数是newarray创建的int数组的引用,dup执行完成栈顶头2个元素 就是该引用
iload_0 1a 将局部变量表中索引为0的变量压入栈顶,即变量a,执行完的状态如下图
iastore 4f 读取栈顶的三个操作数,将指定value插入到指定数组的指定索引处,然后三个操作数弹出,即变量a插入int数组b 索引0处
dup 59 此时栈顶还是int数组b的引用,执行完后栈顶头两个元素都是该引用
iastore 4f 同上,将变量a3插入int数组b索引1处,此时栈顶的数据是int数组b的引用
astore_3 4e 将栈顶的引用数据保存到局部变量表索引为3的地方,前面0,1,2分别是变量a,a2,a3
aload_3 2d 从局部变量表加载索引为3的引用变量到栈顶,即int数组b的引用重新放入栈顶
iconst_4 07 将int变量4压入栈顶,此时操作数栈的状态如下图。
iastore 4f 读取栈顶的三个元素,将指定数组的指定索引处的值修改为目标值,即int数组b索引为2的元素修改为4
iconst_2 05 将int变量2压入栈顶,即待读取的数组索引
iaload 2e 读取栈顶的头两个元素,读取指定数组的指定索引的元素并加载到栈顶,此时操作数栈只有一个该元素
return b1 返回到调用该方法的指令处

 上节中我们使用HSDB查看方法字节码,除此之外,Java还提供了解析class文件的javap命令,javap会将其中的二进制数据转换成方便阅读的文本数据,可以借此具体了解Class文件格式各部分的构成。不过javap对class文件有要求,如果需要输出完整的信息,在javac编译源代码文件时需要加上-g选项,类似于gcc的-g选项,此选项会加上额外的源码的相关信息。

如下图,注意-c并不是反汇编,只是查看class文件中的方法字节码,-v是输出最完整全面的信息

代码中使用了Junit包,为了解决使用javac编译找不到Junit相关类的问题,可利用ideal完成该类的编译,在Java Compile里面加上-g选项即可,如下图:


//最后的//的内容是对该属性的翻译 //stack表示操作数栈的最大深度,locals表示本地变量的个数,args_size表示参数个数 //虚拟机指令对应的代码行号 //本地变量表,this是默认传入方法的参数 //将异常重新抛出使用 //此方法可能抛出的异常类型 //运行时可见的注解,#60是注解的类名,#61是属性名,c#62是属性值

网盘是个好东东,可以对话和传文件


一般人认为,PHP虽然的解释执行(完全不编译),而JAVA是半编译(编译后代码非CPU执行执行),看起来JAVA的效率要高。

但是由于JAVA虚拟机的消耗太大,实际效果远不如PHP好。

本回答被提问者和网友采纳


不能这么说,java项目运行是有一个打包过程,页面像jsp这样的直接在打包过程中编译成.CLASS文件,这个过程是不影响执行效率的。真正要比较的是java虚拟机执行的这一过程。至于php我不太了解所以不作回答,你如果是因为java执行jsp文件繁琐就觉得效率慢是错误的

下载百度知道APP,抢鲜体验

使用百度知道APP,立即抢鲜体验。你的手机镜头里或许有别人想知道的答案。

我要回帖

更多关于 怎么向虚拟机里面导入文件 的文章

 

随机推荐