JVM

来源:互联网 发布:魔方软件下载 编辑:程序博客网 时间:2024/06/06 19:49

目录:
1.Java字节码
2.字节码执行引擎
3.类加载机制
4.方法解析

参考文献:
https://segmentfault.com/a/1190000003020075

Java字节码

加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。
1)将一个局部变量加载到操作数栈的指令包括:iload,iload_,lload、lload_、float、 fload_、dload、dload_,aload、aload_。
2)将一个数值从操作数栈存储到局部变量标的指令:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_
3)将常量加载到操作数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
4)局部变量表的访问索引指令:wide
一部分以尖括号结尾的指令代表了一组指令,如iload_,代表了iload_0,iload_1等,这几组指令都是带有一个操作数的通用指令。

运算指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
1)加法指令:iadd,ladd,fadd,dadd
2)减法指令:isub,lsub,fsub,dsub
3)乘法指令:imul,lmul,fmul,dmul
4)除法指令:idiv,ldiv,fdiv,ddiv
5)求余指令:irem,lrem,frem,drem
6)取反指令:ineg,leng,fneg,dneg
7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr
8)按位或指令:ior,lor
9)按位与指令:iand,land
10)按位异或指令:ixor,lxor
11)局部变量自增指令:iinc
12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp

Java虚拟机没有明确规定整型数据溢出的情况,但规定了处理整型数据时,只有除法和求余指令出现除数为0时会导致虚拟机抛出异常。

类型转换指令

类型转换指令将两种Java虚拟机数值类型相互转换,这些操作一般用于实现用户代码的显式类型转换操作。JVM支持宽化类型转换(小范围类型向大范围类型转换):1)int类型到long,float,double类型2)long类型到float,double类型3)float到double类型窄花类型转换指令:i2b,i2c,i2s,l2i,f2i,f2l,d2l和d2f,窄化类型转换可能会导致转换结果产生不同的正负号,不同数量级,转换过程可能会导致数值丢失精度。如int或long类型转化整数类型T时,转换过程是仅仅丢弃最低位N个字节意外的内容(N是类型T的数据类型长度)

对象创建与操作

虽然类实例和数组都是对象,Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。
1)创建实例的指令:new
2)创建数组的指令:newarray,anewarray,multianewarray
3)访问字段指令:getfield,putfield,getstatic,putstatic
4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
6)取数组长度指令:arraylength
7)检查实例类型指令:instanceof,checkcast

操作数栈管理指令

直接操作操作数栈的指令:pop,pop2,dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2和swap

控制转移指令

控制转移指令

让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:
1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等

2)复合条件分支:tableswitch,lookupswitch

3)无条件分支:goto,goto_w,jsr,jsr_w,ret

JVM中有专门的指令集处理int和reference类型的条件分支比较操作,为了可以无明显标示一个实体值是否是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操作,都使用int类型的比较指令完成,而 long,float,double条件分支比较操作,由相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。

方法调用和返回指令

invokevirtual指令:调用对象的实例方法,根据对象的实际类型进行分派(虚拟机分派)。
invokeinterface指令:调用接口方法,在运行时搜索一个实现这个接口方法的对象,找出合适的方法进行调用。
invokespecial:调用需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
invokestatic:调用类方法(static)
方法返回指令是根据返回值的类型区分的,包括ireturn(返回值是boolean,byte,char,short和 int),lreturn,freturn,drturn和areturn,另外一个return供void方法,实例初始化方法,类和接口的类初始化i 方法使用。

同步

JVM支持方法级同步和方法内部一段指令序列同步,这两种都是通过moniter实现的。

方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。

同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。

结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程,M代表一个monitor):

1)T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等

2)任何时刻都不会出现T释放M的次数比T持有M的次数多的情况

字节码执行引擎

Java是一种跨平台的语言,为什么可以跨平台,因为我们编译的结果是中间代码—字节码,而不是机器码,那字节码在整个Java平台扮演着什么样的角色的呢?JDK1.2之前对应的结构图如下所示:
这里写图片描述

从JDK1.2开始,迫于Java运行始终笔C++慢的压力,JVM的结构也慢慢发生了一些变化,JVM在某些场景下可以操作一定的硬件平台,一些核心的Java库甚至也可以操作底层的硬件平台,从而大大提升了Java的执行效率,在前面JVM内存模型和垃圾回收中也给大家演示了如何操作物理内存,下图展示了JDK1.2之后的JVM结构模型。
这里写图片描述

那C++和Java在编译和运行时到底有啥不一样?为啥Java就能跨平台的呢?
这里写图片描述

类加载

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存到卸载出内存的生命周期包括:加载->连接(验证->准备->解析)->初始化->使用->卸载。

类加载的时机

5种情况:
1.使用new关键字实例化对象时,读取或设置一个类的静态字段,除被final修饰经编译结果放在常量池的静态字段,调用类的静态方法时。
2.使用java.lang.reflect包方法对类进行反射调用时。(Class.forName())。
3.初始化子类时,如果父类没有初始化。
4.虚拟机启动时main方法所在的类。
当使用JDK1.7动态语言支持时,java.lang.invoke.MethodHandle实例解析结果为5.REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且对应类没有进行初始化。

类加载过程

加载 加载是类加载的第一个阶段,虚拟机要完成以下三个过程:
1.通过类的全限定名获取定义此类的二进制字节流。
2.将字节流的存储结构转化为方法区的运行时结构。
3.在内存中生成一个代表该类的Class对象,作为方法区各种数据的访问入口。

各个阶段的目的:
1 验证 目的是确保class文件字节流信息符合虚拟机的要求。
2 准备 为static修饰的变量赋初值,例如int型默认为0,boolean默认为false。
3 解析 虚拟机将常量池内的符号引用替换成直接引用。
4 初始化 初始化是类加载的最后一个阶段,将执行类构造器< init>()方法,注意这里的方法不是构造方法。该方法将会显式调用父类构造器,接下来按照java语句顺序为类变量和静态语句块赋值。

类加载器

类加载机制为双亲委托机制,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性(这是为了保证基本类的安全性)

总共有4种类加载器,分别是:

1 启动类加载器(Bootstrap ClassLoader),该加载器会将\lib目录下能被虚拟机识别的类加载到内存中。
2 扩展类加载器(Extension ClassLoader),该加载器会将\lib\ext目录下的类库加载到内存。
3 应用程序类加载器(Application ClassLoader),该加载器负责加载用户路径上所指定的类库。

4 我们自定义的ClassLoader继承自应用3程序类加载器,当自定义类加载器找不到所加在的类时,会使用启动类加载器进行加载,当启动类加载器加载不到时,由扩展类加载,扩展类加载不到时有应用程序类加载。这也是为什么上边的代码能够成功运行的原因。

在收到加载请求时,首先请求父类加载器进行加载,父类能加载则加载,不然加载,则继续向上请求,指定走到根加载器,如果根加载器也能加载,则返回给子类去加载。这样的好处是对于一些系统提供的类,你是没有办法伪造的(除非你把jvm给改造了),不然,在加载这些类时(如String),必定是加载的jre系统库中的String,而不是你篡改后的String.

方法调用解析过程

方法调用
方法调用阶段的唯一任务就是 确定被调用方法的版本(即调用哪一个方法) ,暂时不涉及方法内部的具体运行过程。class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在class文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址,这使得java有着更强大的动态扩展能力,但也使得java方法的调用过程变得相对复杂起来,需要在类的加载甚至运行期间才能确定目标方法的直接引用

解析调用 :之前说到,所有方法在class文件里面都是一个常量池中的符号引用,在 类加载的解析阶段 ,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是, 方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的

符合这个条件的有 静态方法,私有方法,实例构造器和父类方法 四类,它们在 类加载的时候会把符号引用解析为该方法的直接引用。(在加载是就能确定的,不存在重载情况的)

解析调用一定是一个静态的过程,编译期间就完全确定,在类装载的解析阶段就会把涉及到的符号引用全部转化为可确定的直接引用,不会延迟到运行期间再去完成。

分派调用 :分派调用可能是静态的也可能是动态的,根据分派依据的宗量数又可分为单分派和多分派。分派机制与java的多态机制关系密切。

根据分配的的时期,分派又可分为:
1. 静态分派 : 依赖静态类型来定位方法执行版本的分派动作,称为静态分派。静态分派的最典型的应 用就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的

2.动态分派: 在运行期间根据实际类型来确定方法执行版本的分派调用过程称为动态分派。这跟多态性的另一个体现——重写有着很密切的关联。

根据分配的选择所用的宗量,分派又可分为:
1.单分派: 根据一个宗量对目标方法进行选择
2.多分派: 根据多于一个的总量对目标方法进行选择。
注:方法的接收者与方法的参数统称为方法的宗量。

JAVA是静态单分派,动态多分派。即如果静态就能确定的,应该就是只看方法的接受者。
Java中和方法调用相关的指令有:invokestatic,invokespecial,invokevirtual,invokeinterface,invokedynamic。其中,invokestatic,invokespecial是静态解析的。
invokedynamic是和反射相关的
invokeinterface是方法调用时,方法接受者为接口,在具体的查找时解析时,首先需要确定对象的实际类型,之后查找的过程和invokevirtual基本相似
(但是实际上,两者还是有性能上的差别的,由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要在运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真的进行如此频繁的搜索。面对这种情况,最常用的优化手段就是在类的方法区中建立一个虚方法表(Virtual Method Table,也称vtable,与此对应,在invokeinterface执行时也会用到接口方法表,Interface Method Table,也称itable),使用虚方法表索引来代替元数据据查找以提高性能。
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表里面的地址入口和父类方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会被替换为指向子类实现版本的地址入口。invokevirtual和invokeinterface在查方法表是还是有具体的区别的。

invokevirtual指令有多态查找的机制,该指令的运行时解析过程步骤如下:

1.找到操作数栈顶的第一个元素所指向的对象的实际类型,记做c
2.如果在类型c中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,不通过则返回java.lang.IllegalAccessError.
3.否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
4.始终没找到合适的方法,抛出java.lang.AbstractMethodError异常。
这就是java语言中方法重写的本质。
jdk1.6时期的java语言是一种静态多分派、动态单分派的语言。

0 0