深入理解java虚拟机(5)---字节码执行引擎
字节码是什么东西?
以下是百度的解释:
字节码(Byte-code)是一种包含执行程序、由一序列 op 代码/数据对组成的二进制文件。字节码是一种中间码,它比机器码更抽象。
它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是因为通常每个 opcode 是一字节长,
但是指令码的长度是变化的。每个指令有从 0 到 255(或十六进制的: 00 到FF)的一字节操作码,被参数例如寄存器或内存地址跟随。
说了这么多,你可能还是不明白到底是什么东西。好吧,简单点,就是java编译以后的那个东东,“.class”文件。
所以class文件就是字节码文件,是由虚拟机执行的文件。也就是java语言和C & C++语言的区别就是,整个编译执行过程多了一个虚拟
机这一步。这个在“深入理解java虚拟机(3)---类的结构” 一文中已经解释,这是一个里程碑式的设计。上一节讲了虚拟机是如何加载
一个class的,这一节就讲解虚拟机是如何执行class文件的。
java虚拟机规范,规定了虚拟机字节码的执行概念模型。具体的虚拟机可以有不同的实现。
运行时栈帧结构
栈是每个线程独有的内存。
栈帧存储了局部变量表,操作数栈,动态连接,和返回地址等。
每一个方法的执行 对应的一个栈帧在虚拟机里面从如栈到出栈的过程。
只有位于栈顶的栈帧才有有效的,对应的方法称为当前方法。
执行引擎运行的所有指令只针对当前栈帧和当前方法。
1.局部变量表
局部变量表存放的一组变量的存储空间。存放方法参数和方法内部定义的局部变量表。
在java编译成class的时候,已经确定了局部变量表所需分配的最大容量。
局部变量表的最小单位是一个Slot。
虚拟机规范没有明确规定一个Slot占多少大小。只是规定,它可以放下boolean,byte,...reference &return address.
reference 是指一个对象实例的引用。关于reference的大小,目前没有明确的指定大小。但是我们可以理解为它就是类似C++中的指针。
局部变量表的读取方式是索引,从0开始。所以局部变量表可以简单理解为就是一个表.
局部变量表的分配顺序如下:
this 引用。可以认为是隐式参数。
方法的参数表。
根据局部变量顺序,分配Solt。
一个变量一个solt,64为的占2个solt。java中明确64位的是long & double
为了尽可能的节约局部变量表,Solt可以重用。
注意:局部变量只给予分配的内存,没有class对象的准备阶段,所以局部变量在使用前,必须先赋值。
2.操作数栈
操作数栈在概念上很像寄存器。
java虚拟机无法使用寄存器,所以就有操作数栈来存放数据。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,
它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
操作数栈 的数据读取、写入就是出栈和如栈操作。
3.动态连接
每个栈帧都包含一个指向运行时常量池的引用,持有这个引用是为了支持动态连接。
符号池的引用,有一部分是在第一次使用或者初始化的时候就确定下来,这个称为静态引用。
还有一部分是在每次执行的时候采取确定,这个就是动态连接。
4.方法返回地址
方法只有2中退出方式,正常情况下,遇到return指令退出。还有就是异常退出。
正常情况:一般情况下,栈帧会保存 在程序计数器中的调用者的地址。虚拟机通过这个方式,执行方法调用者的地址,
然后把返回值压入调用者中的操作数栈。
异常情况:方法不会返回任何值,返回地址有异常表来确定,栈帧一般不存储信息。
5.方法调用
方法调用阶段不是执行该方法,而仅仅时确认要调用那个方法。class文件在编译阶段没有连接这一过程,、
所以动态连接这个在C++就已经有的技术,在java运用到了一个新的高度。所有的函数(除了私有方法,构造方法 & 静态方法,下同),理论上
都可以时C++里面的虚函数。所以所有的函数都需要通过动态绑定来确定“明确”的函数实体。
解析
所有方法调用的目标方法都是常量池中的符号引用。在类的加载解析阶段,会将一部分目标方法转化为直接引用。(可以理解为具体方法的直接地址)
可以转化的方法,主要为静态方法 & 私有方法。
Java虚拟机提供5中方法调用命令:
invokestatic:调用静态方法
invokespecial:调用构造器,私有方法和父类方法
invokevirtual:调用虚方法
invokeinterface:调用接口方法
invokedynamic:现在运行时动态解析出该方法,然后执行。
invokestatic & invokespecial 对应的方法,都是在加载解析后,可以直接确定的。所以这些方法为非虚方法。
java规定 final修饰的是一种非虚方法。
分派
静态分派
先看一个例子:
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 13:53 */ public class StaticDispatch { static abstract class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void sayHello(Human guy) { TraceLog.i("Hello guy!"); } public void sayHello(Man man) { TraceLog.i("Hello gentleman!"); } public void sayHello(Woman man) { TraceLog.i("Hello lady!"); } public static void action() { Human man = new Man(); Human woman = new Woman(); StaticDispatch dispatch = new StaticDispatch(); dispatch.sayHello(man); dispatch.sayHello(woman); } }
05-19 13:58:05.538 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 13:58:05.539 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]
结果执行了public void sayHello(Human guy)函数。这不是应该多态吗?
Human man = new Man();
这里的Human我们理解为静态类型,后面的Man是实际类型。我们在编译器只知道静态类型,后面的实际类型等到动态连接的时候才知道。
所以对于sayHello方法,虚拟机在重载时,是通过参数的静态类型,而不是实际类型来判断使用那个方法的。
如果对类型做强制转换:
public static void action() { Human man = new Man(); Human woman = new Woman(); StaticDispatch dispatch = new StaticDispatch(); dispatch.sayHello(man); dispatch.sayHello(woman); dispatch.sayHello((Man)man); dispatch.sayHello((Woman)woman); } 05-19 14:08:29.000 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)] 05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello gentleman! [at (StaticDispatch.java:29)] 05-19 14:08:29.002 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello lady! [at (StaticDispatch.java:34)]
如果强转了以后,类型也跟着变化了。
静态分配的典型应用是方法重载。但是方法重载有时候不是唯一的,所以只能选合适的。
比如:
public void sayHello(int data) { TraceLog.i("Hello int!"); } public void sayHello(long data) { TraceLog.i("Hello long"); }
当sayHello(1)的时候,一般情况下会调用int型的方法,但是如果注释调,只有long型的方法,long型参数方法就会被调用。
动态分派
上面讲的是重载,这里是重写(@Override)
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 14:26 */ public class DynamicDispatch { static abstract class Human{ protected abstract void sayHello(); } static class Man extends Human{ @Override protected void sayHello() { TraceLog.i("Hello gentleman!"); } } static class Woman extends Human{ @Override protected void sayHello() { TraceLog.i("Hello lady!"); } } public static void action() { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); } }
先来看上面标红的这句:方法要解析man 的sayhello,问题是man是什么东西,我在解析的时候,是不知道的。所以“man.sayHello();”具体执行的那个类的方法,是需要在虚拟机
动态连接的时候才知道,这个就是多态。如果使用javap分析就可以知道这句话,在class文件里面是ynamicDispatch$Human: sayHello. 是的class文件不知道这个sayhello到底要去
调哪个方法。
invokevirtual指令解析的过程大概如下:首先在操作数栈里第一个元素的实际类型,即为C。
如果在类型C中找到与常量描述符相同的类名和方法,则权限校验通过后,即为找到该法方法,则返回这个方法的直接引用。
否则,对C的父类进行依次查找。
这个过程通俗一点就是,先从当前类里面寻找“同名”的该方法,如果没有,就从C的父类里面找,知道找到为止!
这个找到的方法,就是我们实际要调的方法。
如果找不到,就是exception。一般情况下,编译工具会帮我们避免这种情况。
单分派和多分派
概念上理解比较麻烦,说白了一点就是重载和重写都存在的情况:
package com.joyfulmath.jvmexample.dispatch; import com.joyfulmath.jvmexample.TraceLog; /** * @author deman.lu * @version on 2016-05-19 15:02 */ public class MultiDispatch { static class QQ{} static class _360{} public static class Father{ public void hardChoice(QQ qq){ TraceLog.i("Father QQ"); } public void hardChoice(_360 aa){ TraceLog.i("Father 360"); } } public static class Son extends Father{ public void hardChoice(QQ qq){ TraceLog.i("Son QQ"); } public void hardChoice(_360 aa){ TraceLog.i("Son 360"); } } public static void action() { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); } }
05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Father: hardChoice: Father 360 [at (MultiDispatch.java:19)] 05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Son: hardChoice: Son QQ [at (MultiDispatch.java:25)]
结果没有任何悬念,但是过程还是需要明确的。hardChoice的选择是在静态编译的时候就确认的。
而son.hardchoise 已经确认了函数的类型,只是需要进一步确认实体类型。所以动态连接是单分派。
动态语言支持:
使用C++语言可以定义一个调用方法:
void sort(int list[],const int size,int (*compare)(int,int));
但是java很难做到这一点,
void sort(List list,Compare c);Compare 一般要用接口实现。
在java 1.7 有一种方法可以支持该功能 MethodHandle。
这部分内容,由于我本地环境无法配置还调用,将会再后续更新。
铺垫了这么多,下面来讲讲字节码的执行
6.基于栈的字节码执行引擎
基于栈的指令集 和基于寄存器的指令集。
先看一个加法过程:
iconst_1
iconst_1
iadd
istore_0
这是基于栈的,也就是上文说的操作数栈。
先把2个元素要入栈,然后相加,放回栈顶,然后把栈顶的值存在slot 0里面。
基于寄存器的就不解释了。
基于寄存器 和基于栈的指令集现在都存在。所以很难说孰优孰劣。
基于栈的指令集 是和硬件无关的,而基于寄存器则依赖于硬件基础。基于寄存器在效率上优势。
但是虚拟机的出现,就是为了提供跨平台的支持,所以jvm的执行引擎是基于栈的指令集。
public int calc() { int a = 100; int b = 200; int c = 300; return (a+b)*c; }
以下是javap的分析结果:
以下图片描述了整个执行过程中代码,操作数栈,& 局部变量表的变化。
这些过程只是一个概念模型,实际虚拟机会有很多优化的情况。
声明:本文相关图片来之参考书面,相关版权归原作者所有。
参考:
《深入理解java虚拟机》 周志明
深入理解java虚拟机(5)---字节码执行引擎相关推荐
- 《深入理解Java虚拟机》笔记5——类加载机制与字节码执行引擎
第七章 虚拟机类加载机制 7.1 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在J ...
- JAVA类加载对字节码的处理_深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)...
[本文版权归微信公众号"代码艺术"(ID:onblog)所有,若是转载请务必保留本段原创声明,违者必究.若是文章有不足之处,欢迎关注微信公众号私信与我进行交流!] 周志明的< ...
- 深入理解Java虚拟机(周志明第三版)- 第八章:虚拟机字节码执行引擎
系列文章目录 第一章: 走近Java 第二章: Java内存区域与内存溢出异常 第三章: Java垃圾收集器与内存分配策略 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言 ...
- 深入理解java虚拟机-第五章:虚拟机字节码执行引擎
本章将介绍虚拟机如何调用方法 一.java虚拟机字节码执行引擎 执行引擎在执行代码的时候可能有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种. 执行流程:输入的是字节码文件 ...
- JAVA 虚拟机类加载机制和字节码执行引擎
引言 我们知道java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢? 一 类文件结构 无关性基石 java有一个口号叫做一次编写,到处运行 ...
- 深入理解JVM虚拟机(七):虚拟机字节码执行引擎
代码编译的结果就是从本地机器码转变为字节码.我们都知道,编译器将Java源代码转换成字节码?那么字节码是如何被执行的呢?这就涉及到了JVM字节码执行引擎,执行引擎负责具体的代码调用及执行过程.就目前而 ...
- java虚拟机调用linux_Java虚拟机字节码执行引擎
定义 Java虚拟机字节码执行引擎是jvm最核心的组成部分之一,它做的事情很简单:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.在不同的虚拟机实现里,执行引擎在执行java代码 ...
- java 虚拟机 字节码,JAVA虚拟机:虚拟机字节码执行引擎
"虚拟机"是一个相对"物理机"的概念,这两种机器都有代码执行能力. 物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的. 虚拟机的执行引擎由自己 ...
- jvm(8)-虚拟机字节码执行引擎
[0]README 0.1)本文转自 "深入理解jvm",旨在学习 虚拟机字节码执行引擎 的基础知识: [1]概述 1)物理机和虚拟机的执行引擎: 物理机的执行引擎是直接建立在处理 ...
最新文章
- 315 · Istio1.1 功能预告,真的假不了
- 报名开启 | 神策 2019 数据驱动大会「矩·变」等你!
- CVPR 2018 STRCF:《Learning Spatial-Temporal Regularized Correlation Filters for Visual Tracking》论文笔记
- html或者jsp页面刷新问题
- Visual 2015创建新项,缺少ADO.NET 实体数据模型的解决方法
- 方法练习3_打印指定次数的HelloWorld
- mybatis注解开发使用二级缓存
- lasso回归和岭回归_如何计划新产品和服务机会的回归
- 基于模型的系统工程MBSE软件工具(ModelCoder)
- LateX在windows中运用MiKTeX
- python 深度 视差 计算_2,Learn about Parallax(视差贴图)
- javabean_企业JavaBean,基础架构预测以及更多行业趋势
- C语言为四维数组申请动态内存空间的方法(二)
- go语言和python-新学语言,选GO还是Python
- python 调用sqldr_SQLLDR用武之地
- 【预训练语言模型】MacBERT: Revisiting Pre-trained Models for Chinese Natural Language Processing
- java程序cpu飙升问题排查过程
- 开发中mock什么意思_开发中
- 使用k-means及k-prototype对混合型数据集进行聚类分析
- JSR303 数据效验
热门文章
- C#控件之DataGridView
- Entering emergency mode
- 移动OA系统,让企业管理更加科学高效
- 螺吡喃(SP)和有色开环的部花菁结构(MC)光致变色示意图
- js屏蔽键盘退格键(backspace或者叫后退键)1
- UG/NX二开Siemens官方实例解析 4.1 EX_Curve_CreateArc(创建曲线)
- 转载:图像滤波概念知识解释
- 『高可用短链服务』基于.NET开源项目SuperShortLink打造稳定可靠的短链转换系统
- 中老年眼镜眼睛散光度数增加
- JAVA_XXBJ(15)面板添加图片按钮和进度条