字节码中含义
javap -c Test 反编译字节码
javap -verbose Test 查看详细的反编译字节码文件
或者用16进制打开 字节码文件
1:使用javap -verbose 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。
2:魔数:所有的.class字节码文件的前4位都是魔数,魔数值为固定值:0xCAFEBABE
3:魔数之后的4个字节码为版本信息,前2个字节表示minor version(次版本号),后2个字节为major version(主版本号)。这里的版本号为 00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。
所以该文件的版本号为:1.8.0。可以通过 java -version命令来验证这一点。
4:常量池(constant pool): 紧接着主版本号之后的就是常量池入口,一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是Classs文件的资源仓库,比如java类中定义的方法与
变量信息,都是存储在常量池中,常量池中主要存储两种常量:字面量与符号引用。字面量如文本字符串,java中声明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,
方法的名称和描述符等。
5:常量池的总体结构:java类所对应的常量池主要是由常量池数量和常量池数组(常量表)这两部分共同构成。常量池数量紧跟在主版本号后面,占据2个字节;常量池数组则紧跟在常量池数量之后。
常量池数组与一般的数组不同的是,常量池数组中不同元素的类型、结构都是不同的,长度当然也不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据1个字节。jvm在解析
常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中的元素的个数 = 常量池数 -1 (其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下需要表达
【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量就对应null值;所以常量池的索引从1而非0开始。
6:在jvm规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型、方法的参数列表(包含数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代码无返回值的Void
类型都用一个大写字母来表示,对象类型则使用字符L加对象的全限定名称来表示。为了压缩字节码文件的体积,对于基本数据类型,jvm都只使用一个大写字母来表示,如下所示:B - byte
C - char,D - double,F -float,I -int ,J - long,S - short,Z - boolean,V - void,L - 对象类型,如Ljava/lang/String;
7:对于数组类型来说,每一个维度使用一个前置的[来表示,如int[]被记录为[I,String[][]被记录为[[Ljava/lang/String;
8:用描述符描述方法时,按照先参数列表,后返回值的顺序来描述。参数列表按照严格的顺序放在一组()之内,如方法:String getAgeAndName(int id,String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String;
字节码数据结构
Class字节码中共有两种数据类型
字节数据直接量: 这是基本的数据类型。共细分为u1、u2、u4、u8四种。分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
表(数组): 表是由多个基本数据或其他表,按照既定的顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都是已经严格定义好的。
安装 jsclasslib工具 ,通过此工具可以更好的分析字节码的组成
如下面的代码,通过jclasslib观察其结果:
字节码的格式为:
对上述的字节码简单的分析:
cafebabe java的魔数值
00 00:小版本号 0
00 34:大版本号 52 ,52对应的就是为1.8同理51对应1.7
00 18:表示常量池数量 1*16+8=24,表示有常量池数组中有24个元素,其中0被默认占用,只有23个
0a:表示第一个常量池中元素的u1值 0a对应10进制中的10,10根据字节码常量截图表示 方法应用 一
00 04:表示 一 指向申明方法的类描述符 ,表示对应第 四 个常量池数组的元素
00 14:表示 一 指向名称及类型描述符,
09:表示第二个常量池中元的u1值,9表示字段的引用 二
00 03:表示指向申明的接口或者类的描述 3
00 15:字段描述符的索引 21
07:ClassInfo 三
00 16: 指向 22
07: 四
00 17:指向 23
01: UTF8字符串 五
00 01: 长度为1
61 a
01: 六
00 01: 1
49: I 大写字母i
01: 七
00 06:
3c 69 6e 69 74 3e: < i n i t >
01: 八
00 03: 3
28 29 56: ( ) V
01:` 九
00 04: 4
43 6f 64 65 : C o d e
01: 十
字段表用于描述类和接口中申明的变量,这里的字段包含了类级别变量以及实列变量,但是不包含方法内部申明的局部变量。
Code的结构
字节码中的每个方法都有一个code属性:
Code结构
attibute_length表示attribute所包含的字节数,不包含attribute_name_index和atrribute_length字段
max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入参数的局部变量
code_length表示该方法所包含的字节码的字节数以及具体的指令码
具体字节码即是该方法被调用时,虚拟机所执行的字节码
exception_table,这里存放的是处理异常的信息
每个exception_table表项由start_pc,end_pc,handle_pc,catch_type组成
start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出异常会由这个表项来处理
handle_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常。
字节码中的this关键字
对于java类中的每一个实例方法(非静态的方法),其在编译后所生成的字节码当中,方法参数的数量总是比源代码中方法参数的数量多一个(this),它位于方法的第一个参数位置处;这样,我们就可以在java
的实列方法中使用this去访问当前对象的属性以及其他方法。
这个操作是在编译期间完成的,即由javac编译器在编译的时候将对this的访问转化为对一个普通实列方法参数的访问,接下来在运行期间,由jvm在调用实例方法时,自动向实例方法传入该this参数。
所以,在实例方法的局部变量表中,至少有一个指向当前对象的局部变量。
字节码中异常处理
java字节码对于异常的处理方式:
1: 统一采用异常表的方式来对异常进行处理。
2:在jdk1.4.2之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特别的指令方式。
3:当异常处理存在finally语句块时,现代化的jvm采取的处理方式是将finally语句块的字节码拼接到每一个catch块后面,换句话说,程序中有多个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
栈帧的概念
栈帧(stack frame)
栈帧是一种帮助虚拟机执行方法调用与方法执行的数据结构。
栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
现代jvm在执行java代码的时候,都会将解释执行与编译执行二者结合起来进行。
所谓解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令
所谓编译执行,就是通过即使编译器(Just in Time,JIT)将字节码转换为本地机器码来执行,现代jvm会根据代码热点生成本地的机器码
基于栈的指令集与基于寄存器的指令集之间的关系:
1.jvm执行指令时是基于栈的指令集。
2.基于栈的指令集主要包含出栈入栈操作。
3.基于栈的指令集的优势在于它可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密关联的,无法做到可移植。
4.基于栈的指令集的缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接
由CPU来执行的,它是在高速缓冲区中进行执行的,速度要快很多。虽然虚拟机可以采取一些优化手段,但是总体来说,基于栈的指令集的执行速度还是要慢一些。
其栈的深度为2,局部变量为 6个 ,this、a、b、c、d、result,args_size=1 ,args_size指的的隐世的this
0 iconst_1 将常量1放入栈顶
1 istore_1 将常量1从栈顶放入到局部变量槽 1 中
2 iconst_2 将常量2放入栈顶
3 istore_2 将常量2从栈顶放入到局部变量槽 2 中
4 iconst_3 将常量3放入栈顶
5 istore_3 将常量3从栈顶放入到局部变量槽 3 中
6 iconst_4 将常量4放入栈顶
7 istore 4 将常量4从栈顶放入到局部变量槽 4 中
9 iload_1 从局部变量表槽中加载 索引为 1 的变量到栈顶,及为 1
10 iload_2 从局部变量表槽中加载 索引为 2 的变量到栈顶,及为 2 栈顶的为2、栈底的为1
11 iadd 从栈中弹出2个元素进行 add 操作吗,并将操作的结果放入栈顶 栈顶的值为 3
12 iload_3 从局部变量表槽中加载 索引为 3 的变量到栈顶,及为 3
13 isub 从栈中弹出2个元素进行 sub 操作吗,并将操作的结果放入栈顶 栈顶的值为 0
14 iload 4 从局部变量表槽中加载 索引为 4 的变量到栈顶,及为 4
16 imul 从栈中弹出2个元素进行 mul 操作吗,并将操作的结果放入栈顶 栈顶的值为 0
17 istore 5 将结果存储到局部变量的槽 5 中
19 iload 5 从局部变量表的槽5中加载元素 5放入栈顶
21 ireturn 将栈顶的值返回,如果此时栈中还有其他元素都会丢弃,只返回栈顶的值含义
符号引用与直接引用
符号引用,直接引用
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析;另一种符号引用则是在每次运行期间转换为直接引用,这种转换叫做动态链接,这体现java的多态性。
1:invokeinterface: 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现该类接口的哪个对象的特定方法。
2:invokestatic: 调用静态方法
3: invokespecial: 调用自己的私有方法、构造方法(<init>)以及父类的方法
4: invokevirtual: 调用虚方法,运行期动态查找的过程
5: invokedynamic: 调用动态方法。
静态解析的4种情形:
1: 静态方法
2: 父类方法
3: 构造方法
4: 私有方法
以上4种称为非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的。
方法的静态分派与动态分派
方法的静态分派
Grandpa g1=new Father();
以上代码,g1的静态类型是Grandpa,而g1的实际类型(真正指向的类型)是Father。
我们可以得出如下的结论: 变量的静态类型是不会发生变化的,而变量的实际类型则是可以发生变量的(多态的一种体现),实际类型是在运行期间方可确定
方法的动态分派
方法的动态分派涉及到一个重要概念:方法接收者。
invokevirtual字节码指令的多态查找流程
比较方法重载(overload)与方法重写(overwrite),我们可以得出这样的一个结论:
方法重载是静态的,是编译期行为;方法重写是动态的是运行期行为。
方法重载与方法重写的重要区别
由谁来调用的 如 MyTest5的方法重载 都是由 MyTest5来调用的 ,而MyTest6中的是由 apple 与 orange来调用的
invokevirtual动态查找的流程,解析过程的几个步骤:
1:找到操作树栈顶的第一个元素他所指向的实际的类型,如果找到的实际类型与常量池中的方法描述符以及方法名称都完全相同的方法,就直接引用
2: 如果没有找到就沿着这种继承的层次从下往上依次查找,找到就直接引用,如果一直没有找到就抛出异常
通过这个可以发现即便在字节码中2个完全相同的符号引用,在运行期间也会有2种不同的直接引用
网友评论