深刻理解Java虚拟机06--虚拟机字节码执行引擎

一.前言安全

物理机的执行引擎是直接在物理硬件如CPU、操做系统、指令集上运行的,可是对于虚拟机来说,他的执行引擎由本身实现。 执行引擎有统一的外观(Java虚拟机规范),不一样类型的虚拟机都遵循了这一规范,输入字节码文件,解析字节码处理,而后输出结果。数据结构

二.运行时栈帧结构优化

一、栈帧概念this

栈帧(Stack Frame)用于支持方法调用和执行的数据结构,包含了局部变量表、操做数栈、动态链接和方法返回地址。操作系统

(1)局部变量表大小(max_locals),栈帧深度在编译时已经肯定,并写入到了Code属性中;线程

(2)执行引擎运行的全部字节码指令都只针对当前栈进行操做; 二、局部变量表cdn

局部变量表存储了方法参数以及方法内定义的局部变量。对象

Slot(变量槽):局部变量表容量最小单位,能够存放32位之内的数据类型; refrence: 直接或者间接找到到该对象在“堆内存”中数据存放的起始地址索引;blog

直接或者间接找到对象所属数据类型在方法区中存储的类型信息继承

局部变量表创建在线程的堆栈上,因此操做两个连续的slot是否为原子操做,都不会引发数据安全问题,可是若是是64位的话,不容许任何方式单独访问其中的一个;

this:实例方法(非static)默认第一个(第0位索引)slot为当前对象本身的引用;

slot重用:

当前字节码的pc计数器超出某个变量的做用域,那这个变量的slot能够交给别的变量使用;

影响到正常的Java垃圾回收机制;

赋null:由于上述slot重用的缘由,当方法域内前面有局部变量定义了大内存实际再也不使用的变量,紧接着后面的代码又是一个耗时的操做,这个时候及时赋null就显得有大的意义。由于一旦触发后,这部分的slot就能够被重用了。看起来就像是方法区内部进行“类gc"操做同样。可是,并非任什么时候候都要进行赋null.以恰当的变量做用域来控制变量回收时间才是最优雅的方式,而且赋null值操做在通过JIT编译优化后会被消除掉,这样的话实际是没有任何意义的。

初始值:和类变量不一样,局部变量系统不会自动赋初始值,因此没有赋值是没法使用的,编译都没法经过。即便经过,字节码校验阶段也会检查出来而致使类加载失败;

三、操做数栈(Operand Stack)

操做栈,后入先出;

最大深度:Code属性表中的max_stacks;

32位数据类型所占栈容量为1,64位所占容量为2;

栈元素的数据类型必须和栈指令保持一致

两个栈帧之间能够存在一部分的重叠,共享数据,这样在方法调用的时候避免的额外的参数复制。

Java虚拟机的解释执行引擎也是:基于栈的执行引擎;

四、动态链接(Dynamic Linking)

字节码中的方法的调用都是经过常量池中指定方法的符号做为参数

静态解析:这种符号有的是类加载阶段或者首次使用初始化的时候转化为直接的引用

动态链接:另一部分是在运行时转化为直接引用

五、方法返回地址

退出:

正常退出:遇到返回的字节码指令;

异常退出:本方法异常表中没有匹配的异常;

退出后,恢复上层方法的局部变量表和操做栈,有返回值就把返回值压入上层调用者的栈中;

三.方法调用

定义:肯定被调用方法的版本

一、解析

编译器可知,运行期不可变。这类方法的调用成为解析,在类加载阶段进行解析。

静态方法、私有方法、实例构造器方法、父类方法,符合上述条件。特色是:

只能被invokestatic和invokespecial指令调用

不可继承或者重写,编译时已经肯定了一个版本。

在类加载时会把符合引用解析为该方法的直接引用。

非虚方法(注意final也是非虚方法,其余的都是虚方法)

二、静态分派

概念:根据静态类型来定位方法的执行版本

典型表明:方法的重载(方法名相同,参数类型不一样)

发生时间:编译阶段

三、动态分派

概念:调用invokevirtual时,把常量池中的类方法符号解析到了不一样的直接引用上。

典型表明:重写,多态的重要体现

过程:

执行invokevitual指令

在虚方法表(类加载阶段,类变量初始化结束后会初始化虚方法表)中查找方法,没有向上的父类进行查找

方法宗量:方法的接收者与方法参数的总称

单分派和多分派:

只有一个宗量做为方法的选择依据,称为单分派。多个,则称为多分派。

当前的Java是静态多分派、动态单分派的语言;

四.动态语言支持

特色:变量无类型,变量的值才有类型

invoke包:Java实现动态语言新增的包

五.指令集

基于栈的指令集

过程:入栈、计算、出栈

优势:

可移植性,不依赖于硬件

代码紧凑

缺点:

速度较慢

产生至关多的指令数量

频繁内存访问

基于寄存器的指令集

表明:x86

六.方法内联

方法内联的方式是经过吧“目标方法”的代码复制到发起调用的方法内,避免真实的方法调用。

内联消除了方法调用的成本,还为其余优化手段创建良好的基础。

编译器在进行内联时,若是是非虚方法,那么直接内联。若是遇到虚方法,则会查询当前程序下是否有多个目标版本可供选择,若是查询结果只有一个版本,那么也能够内联,不过这种内联属于激进优化,须要预留一个逃生门(Guard条件不成立时的Slow Path),称为守护内联。

若是程序的后续执行过程当中,虚拟机一直没有加载到会令这个方法的接受者的继承关系发现变化的类,那么内联优化的代码能够一直使用。不然须要抛弃掉已经编译的代码,退回到解释状态执行,或者从新进行编译

七.逃逸分析

逃逸分析的基本行为就是分析对象动态做用域:当一个对象在方法里面被定义后,它可能被外部方法所引用,这种行为被称为方法逃逸。被外部线程访问到,被称为线程逃逸。

若是对象不会逃逸到方法或线程外,能够作什么优化?

栈上分配:通常对象都是分配在Java堆中的,对于各个线程都是共享和可见的,只要持有这个对象的引用,就能够访问堆中存储的对象数据。可是垃圾回收和整理都会耗时,若是一个对象不会逃逸出方法,可让这个对象在栈上分配内存,对象所占用的内存空间就能够随着栈帧出栈而销毁。若是能使用栈上分配,那大量的对象会随着方法的结束而自动销毁,垃圾回收的压力会小不少。

同步消除:线程同步自己就是很耗时的过程。若是逃逸分析能肯定一个变量不会逃逸出线程,那这个变量的读写确定就不会有竞争,同步措施就能够消除掉。

标量替换:不建立这个对象,直接建立它的若干个被这个方法使用到的成员变量来替换。

八.小结

  在前面咱们已经了解到栈帧、方法区的内存时线程私有的,本篇更加详细的讲了方法是怎么找到并执行的。Java虚拟机规范:输入字节码,解析字节码处理,输出结果。首先,栈帧包含了局部变量表、操做数栈、动态链接、方法返回地址。字节码中的方法都是经过常量池中的符号做为参数指定的,有些编译解析肯定,有些运行行时转化为直接引用。首先记住,JVM是基于栈的执行引擎。栈有着先入后出的特色,执行引擎的指令也仅执行当前栈。而局部变量表存储了方法内须要的变量信息,是以Slot 为单位进行存储,超出操做域后,本来占用的内存区域能够被其余的局部变量使用,相似“回收”。而后,记住Java是静态多分派,动态单分派的语言。静态分派,如方法的重载。经过方法的参数不一样就能够肯定要调用哪一个方法,这个再编译阶段就定好。动态分派,如方法的重写。执行方法时,有一个虚方法表。这这个表里搜索,本身有就执行本身的,没有向上找父类的。这个是Java实现多态的重要原理。Java也有支持动态语言的invoke包,平时用的较少。