首页 > 其他 > 详细

JVM学习(八)字节码执行引擎

时间:2021-01-22 00:10:30      阅读:47      评论:0      收藏:0      [点我收藏+]

再次复习,jvm的组成三大部分

内存模型+类加载系统+字节码执行引擎

jvm是以方法作为最基本的执行单元,所以当我们讨论字节码执行引擎的时候,我们讨论的是方法具体如何执行。

运行时栈帧结构

技术分享图片

定义:

  • 栈:每个线程都有自己独有的一个空间放自己的局部变量
  • 栈帧:每个线程会为自己的每个方法分配一块对应的内存空间(因为每个方法都有自己的局部变量)只有位于栈顶的栈帧才是在运行的,其被称为当前栈帧
  • 栈深:栈内栈帧数量,递归的时候
  • 栈数:栈区域大小固定,栈深越深,那么对应可创建栈/线程数越少

局部变量表

  • 存放方法参数和方法内定义的局部变量。
  • 局部变量表的容量是以变量槽来表示,并没有明确指出一个槽对应的内存空间,只是说每个变量槽都应该能存放一个boolean,byte,char,short,int,float,reference,returnAddress类型的数据
  • 会使用局部变量表来完成方法参数值到参数列表的传递,也就是所谓的值传递(java所有的参数传递都是 传值,从来没有 传引用 这个事实,传对象就是传地址也就是传引用了)
  • 局部变量表第0位用于方法所属的对象实例,也就是我们使用的this关键字

操作数栈

  • 它是一个Last In Firtst Out 栈,存放的是一些临时计算结果,如算术运算时通过将运算设计的操作数压入栈顶后调用运算指令进行。
  • jvm字节码执行引擎被称为“基于栈的执行引擎”,这个栈就是指操作数栈(大多数操作嗾使在此处执行)

动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了支持方法调用过程中的动态连接

返回地址

方法的完成有两种类型,一种是正常调用完成,一种是异常退出。但无论那种方式退出,都必须返回最初方法被调用的位置。因此一般来说,记录主调方法的PC计数器的值可以作为返回地址

方法调用

方法调用不是指方法中的代码执行,方法调用是指确定调用方法的版本,即调用哪一个方法(因为有方法重写和方法重载的存在)

  • 方法重写:子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写。动态分派 单分派
  • 方法重载: 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。方法签名 = 方法名 + 形参列表。静态分派 多分派

静态解析

编译期间就能完全确定方法调用的版本,适用于静态方法和私有方法,因为它们都不可能被重写,仍可能被重载

分派是一种描述形式,解析也是一种描述形式,并不排他;同时静态分派由于是编译确定所以也有人把它归入静态解析。

静态分派

public class O{
    static class A{}
    static class B extends A{}
    static class C extends A{}
    public void a(A a){
        System.out.println("A method");
    }
    public void a(B b){
        System.out.println("B method");
    }
    public void a(C c){
        System.out.println("C method");
    }
    public static void main(String[] args){
        O o = new O();
        A b = new B();
        A c = new C();
        o.a(b);
        o.a(c);
    }
}
输出 
A method
A method

为什么会输出A method?

对多态的深入理解

//A 称为静态类型,编译期确定
//B 称为实际类型,运行时确定
A b = new B();

什么叫做运行时确定呢?

A b = 3>0?new B():new C();

针对方法重载的选择,jvm使用是静态类型。也就是说重载对应的是静态类型,也就是变量声明的类型是什么就是什么

静态分派会根据参数的静态类型和方法接收者的静态类型,两个来决定目标方法。

动态分派

public class DynamicDispatch {
	static abstract class Human{
		protected abstract void sayHello();
	}
	static class Man extends Human{ 
		@Override
		protected void sayHello() { 
			System.out.println("man say hello!");
		}
	}
	static class Woman extends Human{ 
		@Override
		protected void sayHello() { 
			System.out.println("woman say hello!");
		}
	} 
	public static void main(String[] args) {
		
		Human man=new Man();
		Human woman=new Woman();
		man.sayHello();
		woman.sayHello();
		man=new Woman();
		man.sayHello(); 
	}
}

造成动态分派或者说造成方法重写的根源在于invokevirtual关键字,invokevirtual的执行逻辑:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
  3. 否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

也就是它在运行期确定接收者的实际类型,所以它会根据方法接收者的实际类型来选择方法版本。invokevirtual只对方法有效,对字段无效。也就是字段永远不参与多态,变量永远不参与多态

单分派与多分派

根据方法的接收者和方法参数称为方法的宗量,单分派是指根据一个宗量确定目标方法,多分派是根据多个宗量确定目标方法。

静态分派属于多分派,因为它关心方法的接收者和方法参数。

动态分派属于单分派,因为它只关心方法的接收者。

动态语言

这里稍微了解一下,动态分派不是动态语言,动态语言是变量的类型检查是运行时进行还是编译时进行。也就是像python,不用声明int a。jdk8之后提出的类型推导或者lambda表达式都为java提供了动态语言的支持。所以java其实式动态语言和静态语言的结合,大部分仍然属于静态语言。

基于栈的字节码解释执行引擎

初步的理论知识已经讲解过了,本节准备了一段Java代码,看看在虚拟机中实际是如何执行的。下面准备了四则运算的例子,请看下面代码。

public class CalcTest {
 public int calc() {

 int a = 100;

 int b = 200;

 int c = 300;

 return (a+b)*c;

	}

}


从Java语言的角度来看,这段代码没有任何解释的必要,可以直接使用javap命令看看他的字节码指令,如下所示。

技术分享图片


javap提示这段代码需要深度为2的操作数栈和4个Slot的局部变量空间,根据这些信息画了下面共7张图,用他们来描述上面执行过程中的代码、操作数栈和局部变量表的变化情况。

技术分享图片

 

技术分享图片

 

技术分享图片

 

技术分享图片

 

技术分享图片

 

技术分享图片

 

技术分享图片


上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做一些优化来提高性能,实际运行过程不一定完全符合概念模型的描述......更准确地说,实际情况会和上面的字节码进行优化,例如,在HotSpot虚拟机中,有很多以“fast_”开头的非标准字节码指令用于合并、替换输入的字节码以提升解释执行性能,而即时编译器的优化手段更加花样繁多。
不过,我们从这段程序的执行中也可以看出栈结构指令集的一般运行过程,整个运算过程的中间变量都以操作数栈的出栈、入栈为信息交换途径,符合我们在前面分析的特点。

JVM学习(八)字节码执行引擎

原文:https://www.cnblogs.com/KODGV-H/p/14310528.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!