五、字节码剖析(代码分析)
- 使用javap -verbose命令分析一个字节码文件时,将会分析文件的魔数、版本号、常量池、类信息、类的构造方法信息、类变量与成员变量等信息。
-
代码示例分析:
package com.lv.jvm.bytecode; public class MyTest1 { private int a = 1; public int getA() { return a; } public void setA(int a) { this.a = a; } }
-
反编译字节码文件:javap -verbose com.lv.jvm.bytecode.MyTest1 命令生成
Classfile /Users/mac/IdeaProjects/java代码阅读包/jvm_ex/out/production/classes/com/lv/jvm/bytecode/MyTest1.class Last modified 2019-8-10; size 485 bytes MD5 checksum 0b9150678e8b5b99574161e996009c9e Compiled from "MyTest1.java" public class com.lv.jvm.bytecode.MyTest1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = Fieldref #3.#21 // com/lv/jvm/bytecode/MyTest1.a:I #3 = Class #22 // com/lv/jvm/bytecode/MyTest1 #4 = Class #23 // java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/lv/jvm/bytecode/MyTest1; #14 = Utf8 getA #15 = Utf8 ()I #16 = Utf8 setA #17 = Utf8 (I)V #18 = Utf8 SourceFile #19 = Utf8 MyTest1.java #20 = NameAndType #7:#8 // "<init>":()V #21 = NameAndType #5:#6 // a:I #22 = Utf8 com/lv/jvm/bytecode/MyTest1 #23 = Utf8 java/lang/Object { public com.lv.jvm.bytecode.MyTest1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 5: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/lv/jvm/bytecode/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lv/jvm/bytecode/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 12: 0 line 13: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/lv/jvm/bytecode/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java"
-
生成16进制文件:(拓展:自己动手实现一个简单反编译的工具,反编译下面的十六进制文件成java代码)
CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 09 00 03 00 15 07 00 16 07 00 17 01 00 01 61 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 1D 4C 63 6F 6D 2F 6C 76 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 31 3B 01 00 04 67 65 74 41 01 00 03 28 29 49 01 00 04 73 65 74 41 01 00 04 28 49 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0C 4D 79 54 65 73 74 31 2E 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 1B 63 6F 6D 2F 6C 76 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 31 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 00 06 00 00 00 03 00 01 00 07 00 08 00 01 00 09 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 04 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 03 00 04 00 05 00 0B 00 00 00 0C 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 06 00 01 00 00 00 08 00 0B 00 00 00 0C 00 01 00 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 02 00 00 00 0C 00 05 00 0D 00 0B 00 00 00 16 00 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13
-
魔数:所有的.class字节码文件的前4个字节都是魔数:魔数值为固定值 0xCAFEBABE
-
魔数之后的四个字节是版本信息,前两个字节是minor version(次版本号),后两个字节表示 major version(主版本号)。如00 00 00 34,换算成十进制,表示次版本号为0,主版本号为52。所以该文件的版本号为1.8.0。可以通过java -version来验证。(注意java是向后兼容的)
-
常量池(constant pool):(注意 不要将常量池中的量理解成不变的量)紧接着版本号之后的就是常量池入口。一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看做是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。
- 字面量:如文本字符串,java中声明的final的常量值
- 符号引用:如类饿和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
-
常量池的总体结构:java类所对应的常量池主要由常量池数量与常量池数组(常量表)这两部分共同构成。常量池数量紧跟着主板号后面,占据两个字节;常量池数组紧跟在常量池数量之后。常量池数组与一般的数组不同的是,常量池数组中不同的元素类型、结构都是不同的,长度当然也就不同,但是每一种元素的第一个数据都是u1类型,该字节是标志位,占据1个字节。jvm在解析常量池时,会根据这个u1类型来获取元素的具体类型。值得注意的是,常量池数组中元素的个数 = 常量池数 - 1(其中0暂时不使用)。目的是:满足某些常量池索引值在特定情况下需要表达「不引用任何一个常量池」的含义;根本原因在与,索引为0也是一个常量(保留常量),只不过它不位于常量表中,这个常量表对应null值,常量池的索引从1而非0开始。
-
class文件结构中常量池数据类型结构总表
image
- 在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;
- 对于数组类型来说,每一个维度使用一个前置[来表示,如int[]被记录为[I;
String[][]
被记录为 [[Ljava/lang/String; - 用描述符来描述方法时,按照先参数列表,后返回值的顺序来描述,参数列表按照参数的严格顺序放在一组()之内,如方法:
String getRealnamebyIdAndNickname(int id,String name)
的描述符为:(I,Ljava/lang/String;) Ljava/lang/String
-
-
java字节码结构
-
Class字节码中有两种数据类型
- 字节数据直接量:这是基本的数据类型。共细分为u1、u2、u4、u8,分别代表连续的1个字节、2个字节、4个字节、8个字节组成的整体数据。
- 表(数组):表是由多个基本数据或其他表,按照既定顺序组成的大的数据集合。表是有结构的,它的结构体现在:组成表的成分所在的位置和顺序都已经严格定义好的。
-
Access_Flag访问标志
-
访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final。通过上面的源代码,我们知道该文件是类并且是public
-
Access_Flag访问标志表
image -
如 0x 00 21:是0x 0020和0x 0001的并集,表示ACC_PUBLIC与ACC_SUPER
-
-
字段表集合
-
字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及示例变量,但是不包括方法内部声明的局部变量
-
字段表结构图
image
-
-
方法表
-
方法表结构图
image -
方法表结构字符描述
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attribute_count; attribute_info attributes[attributes_count]; }
-
-
方法属性结构
-
jvm预定义了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用
-
不同的attribute通过attribute_name_index来区分
-
方法中的每个属性都是一个attribute_info结构
attribute_info{ u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
-
-
Code结构
-
Code attribute的作用是保存该方法的结构,如所对应的字节码
image -
attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
-
max_stack表示中国方法运行的任何时刻所能到达的操作数栈的最大深读
-
max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量
-
code_length表示该方法所包含的字节码的字节数以及具体指令码
-
具体字节码即是该方法被调用时虚拟机所执行的字节码
-
Exception_table,这里存放的是处理异常的信息
-
每个exception_table表项由start_pc,end_pc,hand_pc,catch_type组成
-
Start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理
-
Handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常
-
-
附加属性
-
接下来是该方法的附加属性
-
LineNumberTable:这个属性用来表示code数组中的字节码和java代码行数之间的关系。这个属性可以用来在调式的时候定位代码的执行行数
-
LineNumberTable结构表
image
-
-
栈帧(stack frame)
-
栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构
-
栈帧本身是一种数据结构,封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。
-
动态链接引申出了:符号引用,直接引用
-
有些符号引用是在类加载阶段或是第一次使用时就会转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在每次运行期间转换为直接引用,这种转换 叫做动态链接,这体现为java的多态性。
-
静态解析的4中情形。(非虚方法,他们是在加载阶段就可以将符号引用转化成直接引用)
- 静态方法
- 父类方法
- 构造方法
- 私有方法(为什么不能是公有方法:因为公有方法是可以被重写的)
-
方法调用的指令:
- Invokinterface :调用接口中的方法。实际上是在运行期决定的,决定调用实现该接口的哪个对象的特定方法。
- Invokestatic: 调用静态方法
- Invokespecial :调用自己的私有方法、构造方法(<init>)以及父类的方法
- Invokevirtual: 调用虚方法,运行期动态查找的过程
- invokedynamic :动态调用方法
-
方法的静态分派
/* 方法的静态分派 Gradpa g1 = new Father(); g1的静态类型是 Gradpa,而g1的实际类型(真正执行的类型)是Father 我们可以得出这样的结论,变量的静态类型是不会变化,而变量的实际类型是可以发送变化的 (多态的一种体现)实际类型在运行期可以确定 */ public class MyTest4 { // 方法的重载,是一种静态的行为,编译期就可以完全确定的 // 重载和重写的区别 public void test(Gradpa gradpa){ System.out.println("gradpa"); } public void test(Father father){ System.out.println("father"); } public void test(Son son){ System.out.println("son"); } public static void main(String[] args) { MyTest4 myTest4 = new MyTest4(); Gradpa g1 = new Father(); Gradpa g2 = new Son(); myTest4.test(g1); myTest4.test(g2); } } class Gradpa{ } class Father extends Gradpa{ } class Son extends Father{ } // 输出: // gradpa // gradpa
-
- 方法动态分派
- 方法动态分派涉及到一个重要的概念:方法的接收者
- invokevirtual字节码指令的多态查找流程
首先到操作数栈顶寻找到栈顶的元素所指向的对象的实际类型
如果在这个类型当中它寻找到了与常量池当中的描述符和名称都相同的方法,并且也具备相应的访问权限,如果都符合就直接返回目标方法的直接引用,流程就结束了。
如果找不到就按继承的层次关系从子类往父类以此重复查找的过程,直到找到能找符合上面条件的对象并调用 如果最后都没有找到则抛出异常
```java
/*
方法动态分派
方法动态分派涉及到一个重要的概念:方法的接收者
invokevirtual字节码指令的多态查找流程
1. 首先到操作数栈顶寻找到栈顶的元素所指向的对象的实际类型
2. 如果在这个类型当中它寻找到了与常量池当中的描述符和名称都相同的方法,并且也具备相应的访问权限
如果都符合就直接返回目标方法的直接引用,流程就结束了。
如果找不到就按继承的层次关系从子类往父类以此重复查找的过程,直到找到能找符合上面条件的对象并调用
如果最后都没有找到则抛出异常
通过比较方法重载 和 方法重写可以得出这样一个结论
方法重载是静态的,是编译期行为。方法重写是动态的,是运行期行为。
*/
public class MyTest5 {
public static void main(String[] args) {
Fruit apple = new Apple();
Fruit orange = new Orange();
apple.test();
orange.test();
apple = new Orange();
apple.test();
}
}
class Fruit {
public void test(){
System.out.println("fruit");
}
}
class Apple extends Fruit{
@Override
public void test() {
System.out.println("apple");
}
}
class Orange extends Fruit{
@Override
public void test() {
System.out.println("orange");
}
}
// 输出:
// apple
// orange
//orange
// 对应的main方法字节码指令
/*
0 new #2 <com/lv/jvm/bytecode/Apple>
3 dup
4 invokespecial #3 <com/lv/jvm/bytecode/Apple.<init>>
7 astore_1
8 new #4 <com/lv/jvm/bytecode/Orange>
11 dup
12 invokespecial #5 <com/lv/jvm/bytecode/Orange.<init>>
15 astore_2
16 aload_1
17 invokevirtual #6 <com/lv/jvm/bytecode/Fruit.test>
20 aload_2
21 invokevirtual #6 <com/lv/jvm/bytecode/Fruit.test>
24 new #4 <com/lv/jvm/bytecode/Orange>
27 dup
28 invokespecial #5 <com/lv/jvm/bytecode/Orange.<init>>
31 astore_1
32 aload_1
33 invokevirtual #6 <com/lv/jvm/bytecode/Fruit.test>
36 return
*/
```
-
重载和重写的区别
- 重载是一种静态的行为(在编译期确定),重写是一种动态行为(运行期确定)(体现的静态类型上)
-
虚方法表、接口方法表
- 针对方法调用动态分派的过程,虚拟机会在类的方法区建立一个虚方法表的数据结构(virtual method table,vtable)
- 针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,itable)
-
基于栈指令集和基于寄存器指令集
-
现代jvm在执行java代码的时候,通常都会将解释执行与编译执行(JIT)二者结合起来进行
-
所谓解释执行,就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。所谓编译执行,就是通过即时编译器(Just In Time,JIT)将字节码转换为本地机器码来执行。现代JVM会根据代码热点(执行频次高的)来生成相应的本地机器码。
-
基于栈指令集和基于寄存器指令集对比
- JVM执行指令时所采取的方式是基于栈的指令集
- 基于栈的指令集的主要操作有 入栈和出栈两种方式
- 基于栈的指令集的优势,可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构紧密关联的 无法做到可移植
- 基于栈的指令集的缺点,在与完成相同的操作指令来数量通常要比基于寄存器指令集数量要多;基于栈的指令集是在内存中完成操作的,而基于寄存器的指令集是直接由cpu来完成,它是在cpu的高速缓冲区来执行,速度要快很多,虽然虚拟机可以采用一些优化手段,但总的来说,基于栈的指令集的执行速度要慢一些。
-
例: 2 - 1的例子分析
/* 例子 2-1 对应java的指令集 1. iconst_1 将1压入栈顶 2. iconst_2 将2压如栈顶 3. isub 将栈顶以及栈顶下面的元素弹出,将栈顶的数字减去栈顶下面的数字。将相减的结果压入栈顶 4. istore_0 将栈顶的元素放入到局部变量表中第0个位置上 基于寄存器的指令集 1. mov 将2放入的一个寄存器中 2. sub 1 将2-1得到的结果在放入到寄存器中 */
-
-
从字节码角度审视jdk动态代理
-
JVM内存
虚拟机栈:Stack Frame 栈帧 每一个方法执行时都会生成独有的一个数据结构 程序计数器:(Program Counter):描述线程执行字节码的指定 本地方法栈:主要用于执行本地方法 堆(Heap):JVM管理的最大的一块内存空间 方法区(Method Area):存储元信息。永久代(Permanent Generation ) 从JDK1.8开始,已经彻底废弃了永久代,使用元空间(meta space) 运行时常量池:方法区的一部分内容 直接内存:Direct Memory
-
网友评论