深入理解JVM-字节码
本文参考圣思园张龙深入理解jvm
目录
Java字节码结构
Class字节码中有两种数据类型:
- 字节数据直接量:基本数据类型,共细分为 u1,u2,u4,u8四种,分表代表连续的1,2,4,8个字节组成的整体数据。
- 表(数组):表是由多个基本数据或者他表,按照既定顺序组成的大的数据集合。表示有结构的,它的结构 体现在:组成表我的成分所在的位置和顺序都是已经严格定义好的。
- 使用javap –verbose 命令分析一个字节码文件是,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。
- 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE
- 魔数之后的4个字节为版本信息,前两个字节表示为minor version次版本号,后两个字节表示major version 主版本号。主版本号52 jdk8 51-7 50-6 49-5。次版本号0,则表示1.8.0
- 常量池 constant pool:紧接着主版本号之后就是常量池入口。一个java类中定义的很多信息都是由常量池来维护和描述的。可以讲常量池看做是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,java声明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
- 常量池的总体结构:java类所对应的常量池主要有常量池数量与常量池数组两部分共同构成。常量池数量紧跟在主版本号后面,占2个字节:常量池数组则紧跟在常量池数量之后。常量池数组与一般数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,单独当然不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据一个字节。Jvm在解析常量池是,会根据这个u1类型来获取元素的具体类型。常量池数组中的元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下徐表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量,保留常量,只不过他它部位与常量表中,这个常量就对应null值,所以常量池索引从1开始。
- 在jvm规范中,每个变量or字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和逮捕无返回值的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; <init>类的构造方法。
Access_Flag访问标志
访问标志信息包括含该Class文件是类还是接口,是否被定义成Public,是否是abstract。如果是类,是否被声明陈final。通过上面的源代码,我们只带该文件是类并且是Public.
0x0021 是 0x0020 0x0001的并集,表示acc_public 和 acc_super
Fileds 字段表
用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
第一个表示该变量为 private
Methods 方法表:
方法的属性结构,方法中的每个属性都是一个attribute_info结构。
方法的属性结构
Jvm预订了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用。
不同的attribute通过attribute_name_index来区分。
Code结构
Code attribute的作用是保存该方法的结构,如对应的字节码:
Attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
Max_stack 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度
Max_loacals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
Code_length表示该方法所包含的字节码的字节数以及具体的指令码
具体字节码即是该方法被调用时,虚拟机执行的字节码
Exception_table,这里存放的是处理异常的信息
每个Exception_table都有start_pc.end_pc,handler_pc,catch_type组成
其他结构
附加属性:
LineNumberTable 这个属性用来表示code数组中的字节码和java代码行数之间的关系。这个属性可以用来在调试的时候定位代码执行的行数。
- “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
- “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。
Valuevirtable 表示的是变量的有效域
附加属性表
字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。
参考:https://segmentfault.com/a/1190000020345321?utm_source=tag-newest#item-1-1
字节码补充注意事项
Java类中的变量的声明最后会放入到构造方法中。
对于java类中的每一个实例方法(非static方法),其在编译或所生产的字节码当中,方法参数的数量总是会比源代码中方法的参数多一个this,它唯一方法的第一个参数位置处,这样我就可以在java的实例方法中使用this来访问当前对象的属性以及其他方法。
这个操作是在编译期间完成的,是由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由jvm再掉用哪个实例的方法时,自动向实例方法传入该this参数。所以在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。
Java字节码对与异常的处理方式:
- 统一采用异常表的方式来对异常进行处理
- 在jdk 1.4.2之前版本,并不是用异常表来对异常处理,而是采用特定的指令方式。
- 当异常处理存在finally语句时,现代化的jvm采取的处理方式是将finally语句块的字节码拼接到每一个catch块后,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。
如果我们在方法后throw 一个exception,则会在二进制文件中与code出现一个并列的exceptions
栈帧
Stack frame https://www.cnblogs.com/jhxxb/p/11001238.html
栈帧是一种用于帮助jvm执行方法调用与方法执行的数据结构。
栈帧是一种数据结构,封账了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。 每个栈帧都是有一个线程来执行
符号引用,直接引用
有些符号引用会在类加载阶段或者是第一次使用就会转换为直接引用,这种转化叫做静态解析;另一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为java的多态性。
- invokeinterface: 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现接口的哪个对象的特点方法。
- invokestatic*:调用静态方法
- invokespecial: 调用自己的私有方法、构造方法(<init>)以及父类方法。
- invokevirtual*: 调用虚方法。运行期动态查找的过程。
- invokedynamic:动态调用发放。
静态解析的4中情形:1.静态方法 2.父类方法 3.构造方法 4.私有方法(公有方法会被重写和复写,所以不可以)
以上四类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的。
结果:
上面程序涉及到 方法的静态分派
g1静态类型是grandpa,而实际类型(真正的指向类型)是father。
结论:变量的静态类型是不会发生变化的,而实际类型则是可以发生变化的。多态的一种体现。实际类型是在运行期才可以确定的。
上面代码中,test有多个方法,是方法重载 ,是一种静态的行为。Jvm判断的依据就是根据方法接受参数的静态类型来判断调用哪一种方法。编译期就可以完全确定。
重载本身是一种静态的概念,重写是一种动态的概念。
但是我们会看到apple.test()对应的invokevirtual是fruit.test方法
方法的动态分派
方法的动态分派涉及到一个重要的概念:方法接受者
Invokevitrual字节码指令的多态查找流程
Invokevitrual找到操作数栈顶的第一个元素所指向对象的实际类型,也就是apple变量找到了Apple这个类,
如果在这个类型中,寻找到与常量池中的描述符(Fruit中test方法 )与描述符相同的方法,并且具有访问权限,如果通过,就返回这个目标方法的直接引用。
如果找到不,从继承关系,从子类到父类寻找。
Invokevitrual在运行期确定方法接受者的实际类型是什么。所以虽然apple,orange变量Invokevitrual相同的符号引用,但被解析到不同的直接引用上了。
针对于方法调用动态分派的过程,jvm会在类的方法区建立一个虚方法标的数据结构,(virtual method table, vtable),类似于上述invokeinterface,jvm会建立一个接口方法表的数据结构(interface method table, itable)。Vtable,如子类如果继承的方法与父类相同,那么子类不会直接复制一份,子类在方法表中直接指向父类方法的入口地址
字节码解释执行
现代jvm在执行java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。所谓解释执行就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。编译执行就是通过即时编译器(just in time,JIT)将字节码转换为本地机器码来执行。现代jvm会根据代码特点来生成相应的本地机器码。
基于栈的指令集与基于计寄存器的指令集之间的关系:
- jvm执行指令时所采取的的方式是基于栈的指令集
- 基于栈的指令集主要的操作有入栈和出栈
- 基于栈的指令集的优势在于可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构密切相关的,无法做到可移植。
- 基于栈的指令集缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而寄存器指令集是直接由CPU来执行的,他是在高速缓存区中进行执行的,速度要快的多。虽然jvm会有优化,但总体,栈的还是要慢一些。
动态代理设计模式https://www.jianshu.com/p/fc285d669bc5
***深入研究