当前位置: 首页 > 科技观察

一篇文章为您带来JavaClass的详解

时间:2023-03-20 01:29:26 科技观察

更多信息请访问:OpenHarmony技术社区https://ost.51cto.com基于栈和基于寄存器的指令有什么区别?什么是直接引用和间接引用?类文件从哪里来?Apt和AMS字节码检测?Section1Classfileintroduction1.背景“计算机只知道0和1,所以我们写的程序需要经过编译器翻译必须转换成由0和1组成的二进制格式才能被计算机执行。”十几年过去了,今天的计算机只能识别0和1,但是由于虚拟机以及近十年来大量基于虚拟机的编程语言的出现和发展将我们编写的程序编译成二进制本地机器码(NativeCode)不再是唯一的选择,越来越多的编程语言选择了独立于操作系统和机器指令集的平台中立格式用作程序编译后的存储格式Java语言之所以能够一次编译到处运行,是因为它使用了所有平台都支持的字节码格式第二节类文件结构1.类文件格式描述类文件如下图,我们可以按照这个表的格式来解释一个class文件,用u1、u2、u4、u8分别表示1字节、2字节、4字节、8字节的无符号数。unsignednumbers可以用来描述数字、索引引用、数量值,或者根据UTF-8编码构造一个字符串值。接下来,我们用这一小段示例代码来说明class文件的具体内容。不管java源文件多复杂,都可以这样分析。公共类TestClass{privateintm;publicintinc(){返回m+1;我们用编译器编译上面的代码得到一个TestClass.class文件。通过Windows工具“010Editor”读取此类文件。以下是010Editor上的class二进制内容:0AFEBABE:magicnumber(它的唯一作用是判断这个文件是否是虚拟机可以接受的Class文件)。00000034:Minorversionnumberandmajorversionnumber次版本号为0,主版本号为52(仅jdk1.1~1.8识别)。类的主要版本与jdk版本的关系(部分)。2、常量池0016:常量池的个数为22,索引为1-21。为什么常量池的索引不是从0开始呢?如果某些指向常量池索引值的数据在某些情况下需要表达“不引用任何常量池项”的意思,可以将索引值设置为0来表示。0A00040012(constantindex:1):0A:->10通过查表表示一个Methodref_info。04:找到索引为4的常量->java/lang/Object.12转十进制得到18,这里在常量池中找到18的常量来表示()V。得到结果:java/lang/Object()V.0900030013(constantindex:2):09:->09代表一个Fieldref_info。结束于:com/havefun/javaapitest/TestClass和mi。070014(常数索引:3):最终结果:com/havefun/javaapitest/TestClass。070015(常量索引:4):07表示类信息。15->21是常量->java/lang/Object中的索引。0100016D(常数索引:5):m。01000149(常数索引:6):I.0100063C696E69743E(常数索引:7):010003282956(常数索引:8):()V。010004436F6465(常数索引:9):代码。01000F4C696E654E756D6265725461626C65(常数索引:10):LineNumberTable。0100124C6F63616C5661726961626C655461626C65(常量索引:11):LocalVariableTable01000474686973(常量索引:12):---->this0100234C636F6D2F6861766566756E2F6A61\7661617069746573742F54657374436C6173733B(常数索引:13):Lcom/havefun/Tetra/java;010003696E63(常数索引:14):inc010003282949(常数索引:15):()I01000A536F7572636546696C65(常数索引:16):SourceFile01000E54657374436C6173732E6A617661(常量索引:17):TestClass.java0C00070008(常量索引:18):0C表示对字段或方法的部分引用。07->05->()V以://"":()V结尾。0C00050006(常数索引:19):最后得到://m:I010021636F6D2F6861766566756E2F6A61\7661617069749573742F54657376436C617373(constantindex:20):最后得到:com/havefun/javaapitest/TestClass。0100106A6176612F6C616E672F4F626A656374(常量索引:21):最后得到:java/lang/Object.javap-v生成的内容,通过上面的分析,很容易理解反编译常量池的内容!常量池:#1=Methodref#4.#18//java/lang/Object。"":()V#2=Fieldref#3。#19//com/havefun/javaapitest/TestClass.m:I#3=Class#20//com/havefun/javaapitest/TestClass#4=Class#21//java/lang/Object#5=Utf8m#6=Utf8I#7=Utf8#8=Utf8()V#9=Utf8Code#10=Utf8LineNumberTable#11=Utf8LocalVariableTable#12=Utf8this#13=Utf8Lcom/havefun/javaapitest/TestClass;#14=Utf8inc#15=Utf8()I#16=Utf8SourceFile#17=Utf8TestClass.java#18=NameAndType#7:#8//"":()V#19=NameAndType#5:#6//m:I#20=Utf8com/havefun/javaapitest/TestClass#21=Utf8java/lang/Object**访问标识符、类索引、父类索引和接口索引集合**下图是类文件结构表的一部分,描述了访问标识符。类索引、父类索引和接口集合等。0021:ACC_PUBLIC|ACC_SUPER下面是常量池截取的部分,这里既可以找到类索引,也可以找到父类索引。#1=Methodref#4.#18//java/lang/Object."":()V#2=Fieldref#3.#19//com/havefun/javaapitest/TestClass.m:I#3=Class#20//com/havefun/javaapitest/TestClass#4=Class#21//java/lang/Object0003:类索引->常量池索引30004:父类索引->常量池索引40000:接口Quantity03,字段信息字段表结构如下:字段访问标志:字段表信息:0001:字段号1通过字段表结构读取6个字节:00020005000600000002访问描述符:代表private0005字段名在常量池中的索引:m0006描述符在常量池中的索引:I0000当属性个数为0时,很容易知道这是一个privateint类型的字段m。4.方法表信息继续看class文件后面的内容:0002表示有两个方法。方法表的结构:向后读取方法表的第一个方法:0001:代表公共方法0007:方法名0008:方法签名()V以上小部分可以得到如下信息:publiccom.havefun.javaapitest。测试类();descriptor:()Vflags:ACC_PUBLIC0001:表示属性表有属性属性表结构:00090000002F:表示代码通过常量池09(代码表示Java代码编译成字节码指令),next4个字节表示下一个属性的长度,2F转十进制等于47。Code对应的结构:下一个字节码为:00010001表示操作数栈的最大深度为1;max_locals表示局部变量表所需的存储空间。接下来的4个字节:00000005(表示码长)。向后读取5个字节表示代码:2AB70001B1;。2A:对应指令aload_0。就是将第0个变量槽中引用类型的局部变量压入操作数栈顶。B7:指令是invokespecial。该指令的作用是将栈顶引用类型数据所指向的对象作为方法接收者,调用该对象的实例构造方法、私有方法或其父类的方法。这个方法有一个u2类型的参数,表示具体调用哪个方法,它指向常量池中一个CONSTANT_Methodref_info类型的常量,是这个方法的符号引用。这里0001表示常量池中的常量#1=>(//java/lang/Object."":()V)这是一个构造方法。因为Java默认会在每个方法中插入一个默认参数this,并将其放在变量槽0的位置。以上两条指令可以理解为this=newObject();实例化这个这个。B1:对应命令为返回。说明:这里的一个字节代表一条指令操作,也就是说Java虚拟机最多不会超过256条指令;0000:异常表长度为0。0002:属性列表个数为2。那么上面可以得到如下信息:Code:stack=1,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:return(1)属性表信息000A00000006000100000003:查表对应的常量池中0A:LineNumberTable;LineNumberTable属性用于描述Java源代码行号和字节码行号(字节码偏移量的对应关系)。00000006表示属性长度为6字节;0001表示有一个line_number_table;0000表示字节码的行号,0003表示Java源码的行号。LineNumberTable对应的结构体:那么可以得到如下信息:LineNumberTable:line3:0000B0000000C000100000005000C000D0000:通过常量池得到的0B表示LocalVariableTable。LocalVariableTable的属性结构:local_variable_info结构。属性长度0C转换为十进制为12;0001局部变量表长度为1。00000005:表示start_pc和length属性分别表示这个局部变量生命周期开始时的字节码偏移量及其覆盖范围长度,两者组合为字节码中此局部变量的范围。0C:在常量池查询中,是这个意思;0D:对应这个变量的描述符:lcom/havefun/javaapitest/TestClass。最后一个0000的意思是:index是栈帧局部变量表中变量槽中局部变量的位置。通过以上部分可以得到如下信息:LocalVariableTable:StartLengthSlotNameSignature050thisLcom/havefun/javaapitest/TestClass;5.属性信息SourceFile属性结构。0010:对应常量池的SourceFile00000002:对应的属性长度为2功能:如果不生成该属性,抛出异常时,不会获取错误码所属的文件名显示在堆栈上。11:转为十进制得到17。sourcefile_index数据项是常量池中指向CONSTANT_Utf8_info类型常量的索引,常量值为源代码文件的文件名。通过常量池得知17对应的常量为:TestClass.java。第三节Stack-BasedInstructions简介1.Stack-basedInterpreterExecutionProcess下面以一段代码为例,说明和演示字节码的执行过程。publicintcalc(){inta=100;整数b=200;整数c=300;return(a+b)*c;}编译成字节码指令如下:publicintcalc();descriptor:()Iflags:ACC_PUBLICCode:stack=2,locals=4,args_size=10:bipush100//将100压入操作数栈2:istore_1//弹出操作数栈顶部的整数值并存入1号第一个局部变量槽3中:sipush200//将200压入操作数栈6:istore_2//弹出操作数栈顶的整数值,存入第二个局部变量槽7:sipush300//将300压入操作数栈10:istore_3//弹出操作数栈顶部的整数值,存入第三个局部变量槽11:iload_1//将局部变量槽1中的变量放入theoperandstack12:iload_2//将局部变量槽2的变量放入操作数栈13:iadd//弹出操作数栈的前两个栈顶元素,进行整数加法,然后将结果放回栈中14:iload_3//将局部变量槽3的变量放入操作数栈15:imul//弹出操作数s的前两个栈顶元素tack,执行整数乘法,然后将结果放回栈中16:ireturn//Put结束方法的执行,将操作数栈顶部的整数值返回给方法的调用者。2.基于堆栈和基于寄存器的指令集有什么区别?同样以1+1计算为例。基于栈的指令集如下:iconst_1iconst_1iaaddistore_0基于寄存器的指令集如下:moveax,1addeax,1这两种指令集的优缺点:基于栈的指令集的主要优点是可移植性.基于寄存器的指令将少于基于堆栈的指令,但每条指令会更长。基于堆栈的指令集的主要缺点是理论上执行速度会相对较慢。我对类文件结构分析的个人总结到此结束。通过一个简单的类来探究编译器是如何实现类编写的,我们可以一步步分析更复杂的类,但需要更加小心。我们已经了解了这些文件的生成过程。个人认为有以下好处:知道javap-v反编译class文件的输出是怎么来的。类文件如何描述Java方法或变量。字节码增强、动态修改或生成等应用方向都是可以实现的。更多信息请访问:OpenHarmony技术社区https://ost.51cto.com