《深入理解jvm》笔记---第八章

虚拟机字节码执行引擎

1. 所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的有效过程,输出的是执行结果。

2. 运行时栈帧结构:

栈帧是支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息,每一个方法调用从调用开始到执行完成都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。栈帧的概念结构如下:

下面详细讲解栈帧各个部分的作用和数据结构:

①局部变量表:存放方法参数和方法内定义的局部变量(注意局部变量表的数据结构并不是一个栈)。局部变量表以变量槽Slot为基本单位,Slot的长度并没有被规定,对于一些长数据要占用多个Slot,会按照高位对齐的方式为其分配连续的Slot空间。

虚拟机如何使用局部变量表?方式如下:

②操作数栈:是一个后入先出的栈,用于存储字节码指令的操作数和操作结果。在方法刚开始执行的时候这个方法的操作数栈是空的,在方法执行过程中,各种字节码指令会往操作数栈中写入和提取内容,也就是入栈/出栈操作。

③动态链接:保存栈帧的基址。

④方法返回地址:保存返回地址。

3.  Java虚拟机的方法调用机制:

方法调用不同于方法执行,方法调用的唯一任务是确定被调用方法的版本,即该调用哪一个方法,暂时还不涉及到方法具体的执行过程。

①解析:所有方法调用中的目标方法在Class文件中都是一个常量池的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转换为直接引用,这种解析能成立的前提是:方法在真正运行之前就有一个可确定的调用版本,并且这个方法的调动版本在运行期是不可改变的。这类方法的调用称为解析。

②分派:

Java是一个面向对象语言,因为java具备面向对象的三个基本特征:继承、封装和多态。这一节我们关心的是重载和覆盖是如何实现的,如何找到正确的目标方法。

⑴静态分派:

依赖静态类型(即外观类型,也就是声明这个变量时所采用的类型)来定位方法执行版本的分派动作称为静态分派。静态分派的典型作用是方法重载。也就是说,虚拟机在重载时是通过参数的静态类型而不是实际类型(你可能会让一个父类的引用指向一个子类的对象,这里子类就是这个父类引用的实际类型)来作为判断依据。比如说:

它的执行结果就是:hello, guy!

hello, guy!

静态分派发生在编译阶段,因此静态分派的动作实际上不是由虚拟机来完成的。有的时候重载版本并不是唯一的,这种情况发生在你用字面量作为实参的时候,字面量没有显示的静态类型,这时候要按照“就近原则”来确定重载的版本。比如代码里有void f(int)和void f(double)两个方法,现在我进行方法调用f(1),那么将会调用f(int)方法,如果代码里没有f(int)方法,那么编译器将会选择f(double)方法来执行。不对这一点详述了。

⑵动态分派:

动态分派则和覆盖有着很重要的联系。先看代码如下:

它的输出结果是:

这个结果并不出乎我们意料,但是虚拟机是如何知道应该调用哪个方法的呢?

我们对这个类的.class文件使用javap工具输出它的字节码,会发现class文件中这两处方法调用都是相同的invokevirtual #22,也就是说指令和参数都是一模一样的,但是它们的执行结果却不相同,这就要从invokevirtual指令的多态查找过程说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

因此上例的结果就不言而喻了。我们把这种在运行期根据实际类型确定方法执行版本的过程称为动态分派。

⑶单分派和多分派:

方法的接受者和方法的参数统称为方法的宗量,根据分派基于多少种宗量,将分派分为单分派和多分派,单分派基于一个宗量对目标方法进行选择,多分派基于多个宗量对目标方法进行选择。

Java语言的静态分派属于多分派类型,动态分派属于单分派类型。

⑷虚拟机动态分派的实现:

前面介绍的分派过程,作为对虚拟机概念模型的解析基本上已经足够了,它已经解决了虚拟机在分派过程中“会做什么”的问题。但是虚拟机“具体是如何做到”的,各个虚拟机会有所差别。

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此虚拟机的实际实现中基于性能考虑,大部分不会真正的进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是,为类在方法区建立一张虚方法表,与此对应,在invokeinterface执行时也会用到接口方法表,使用虚方法表索引代替元数据查找来提高性能。虚方法表结构如下所示:

虚方法表结构

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口;如果子类重写了这个方法,子类方法表中的地址将会替换为子类实现版本的入口地址。

为了程序上实现的方便,具有相同签名的方法,在父类和子类的虚方法表中应当具有相同的索引号,这样当类型变换时,仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值之后,虚拟机会把该类的方法表也初始化完毕。

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

这一节介绍虚拟机如何执行方法中的字节码指令。

①基于栈的指令集与基于寄存器的指令集:

Java编译器输出的指令流,基本上是一种基于栈的指令集结构。指令流中的指令大部分都是零地址指令(零地址指令就是指令中包含0个操作数地址),它们依赖操作数栈进行工作。与之相对应的另外一套常用的指令集结构是基于寄存器的指令集结构。举个简单的例子来说明这两者的差别:分别使用这两者计算1+1:

基于栈的指令集会是这样的:

两个iconst_1指令会连续把两个常数1压入栈,iadd指令把栈顶的两个值出栈,相加,然后把结果存回栈顶,最后istore_0把栈顶的值存回局部变量表的第0个Slot中。

基于寄存器的指令集则可能是这样的:

mov指令把eax寄存器设置为1,然后让eax加1保存回eax。

两种指令集结构的优缺点比较:

②基于栈的解释器执行过程:

考察这样一段代码:

使用javap查看他的字节码指令如下:

它在执行过程中的代码、操作数栈、局部变量表的变化情况如下:

值得一提的是,在生成的字节码指令中,并没有任何关于”a”、”b”、”c”这些标识符的信息,字节码指令中只有对局部变量表、操作数栈内的操作数进行操作的指令,它是怎么知道计算(a+b)*c时该取局部变量表的哪些对应的操作数的呢?我们可以看到,int a=100时生成的指令是istore_1,int b=200时生成的是istore_2,int c=300时生成的是istore_3,也就是说,虽然没有显式的把变量标识符跟局部变量表中的Slot对应,但其实它已经使用了默认的按变量声明顺序(以及参数传递顺序)对应的方式。

同样,在C、C++等高级语言程序编译连接后产生的exe文件中,反汇编后我们可以看到对于一个int a=100;语句产生的汇编语言指令是003513BE  mov        dword ptr [a],64h ,他表示把100存储到ptr [a]所指示的地址的双字中,在机器最终运行的指令中,它是一个具体地址,没有任何关于标识符a的信息。

时间: 2024-10-24 20:54:00

《深入理解jvm》笔记---第八章的相关文章

《深入理解JVM虚拟机》读书笔记

前言:<深入理解JVM虚拟机>是JAVA的经典著作之一,因为内容更偏向底层,比较枯燥难啃,所以之前一直没有好好的阅读过.最近因为刚好有空,又有了新目标.所以打算和<构架师的12项修炼>一起看,这样荤素搭配,吃饭不累~ 笔记: 1.如果开发人员不了解虚拟机的一些技术特性的运行原理,就无法写错最适合虚拟机运行和自优化的代码. 2. 原文地址:https://www.cnblogs.com/xujanus/p/8513587.html

Android群英传笔记——第八章:Activity与Activity调用栈分析

Android群英传笔记--第八章:Activity与Activity调用栈分析 开篇,我们陈述一下Activity,Activity是整个应用用户交互的核心组件,了解Activity的工作模式,生命周期和管理方式,是了解Android的基础,本节主讲 Activity的生命周期与工作模式 Activity调用栈管理 一.Activity Activity作为四大组建出现平率最高的组件,我们在哪里都能看到他,就让我们一起先来了解一下他的生命周期 1.起源 Activity是用户交互的第一接口,他

构建之法阅读笔记06-第八章用户需求

阅读笔记 第八章:需求分析 第八章的需求分析介绍了软件需求的类型.利益相关者,获取用户需求的常用方法和步骤,竞争性需求分析的框架NABCD以及项目计划和估计的技术. 在软件需求方面,可以从利益相关者那里,引导他们表达需求,从而获取.从用户那里获取了需求之后,需要分析和定义需求,也就是对需求进行规整,来定义一下需求的内容.下一步就要像用户去验证这些规整好的需求,看看是否满足用户的需要.另外在软件开发过程中也会对需求进行调整,来适应新的变化. 在对软件的需求方面,可以分为对产品功能性的需求,也就是要

深入理解JVM—性能监控工具

我们知道,在JVM编译期和加载器,甚至运行期已经做了大量的调优操作,但是那些都是JVM针对Java程序所做的通用的.简单的优化,程序在运行时由于运行环境的复杂性.业务逻辑的复杂性,很多JVM是无法进行优化处理的,这就需要我们自己在写代码的时候就注意,以便我们的程序在特定的业务场景发挥到最佳性能. 要进行性能调优,首先我们要找到程序的性能瓶颈在哪里?而要知道性能瓶颈在哪里,我们需要借助一定的工具进行处理. 在windows操作系统下,当我们的系统运行很慢的时候,80%的人首先查看的就是任务管理器,

HashMap工作原理、深入理解JVM、正则

HashMap工作原理: http://www.importnew.com/7099.html: http://blog.csdn.net/ghsau/article/details/16843543: http://blog.csdn.net/ghsau/article/details/16890151. 深入理解JVM: http://www.importnew.com/17770.html: http://www.cnblogs.com/dingyingsi/p/3760447.html.

【转】[译]深入理解JVM

http://www.cnblogs.com/enjiex/p/5079338.html 深入理解JVM 原文链接:http://www.cubrid.org/blog/dev-platform/understanding-jvm-internals 每个使用Java的开发者都知道Java字节码是在JRE中运行(JRE: Java 运行时环境).JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作,而Java程序员通常并不需要深入了解JVM运行情况就可以开发出大型应用和类库.尽管

深入理解JVM之JVM内存区域与内存分配

深入理解JVM之JVM内存区域与内存分配 在学习jvm的内存分配的时候,看到的这篇博客,该博客对jvm的内存分配总结的很好,同时也利用jvm的内存模型解释了java程序中有关参数传递的问题. 博客出处: http://www.cnblogs.com/hellocsl/p/3969768.html?utm_source=tuicool&utm_medium=referral 看了此博客后,发现应该去深入学习下jvm的内存模型,就是去认真学习下<深入理解Java虚拟机>,其内容可能会<

【转】理解JVM内存区域

引言 对于C++程序员,内存分配与回收的处理一直是令人头疼的问题.Java由于自身的自动内存管理机制,使得管理内存变得非常轻松,不容易出现内存泄漏,溢出的问题. 不容易不代表不会出现问题,一旦内存泄漏或溢出的情况发生,调试起来会变得非常困难.这就要求我们对虚拟机的内存区域有深入的理解.最终能够判断内存方面的异常发生时,具体在JVM中的位置. 内存区域 JVM运行时,首先需要类加载器(ClassLoader) 加载所需类的字节码,加载完毕交由执行引擎执行,执行过程中需要一段空间来存储数据(类比CP

[译]深入理解JVM

深入理解JVM 原文链接:http://www.cubrid.org/blog/dev-platform/understanding-jvm-internals 每个使用Java的开发者都知道Java字节码是在JRE中运行(JRE: Java 运行时环境).JVM则是JRE中的核心组成部分,承担分析和执行Java字节码的工作,而Java程序员通常并不需要深入了解JVM运行情况就可以开发出大型应用和类库.尽管如此,如果你对JVM有足够了解,就会对Java有更好的掌握,并且能解决一些看起来简单但又尚

【深入理解JVM】:类加载机制

概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 与那些在编译时需要进行链接工作的语言不同,在Java语言里,类型的加载.连接和初始化过程都是在程序运行期间完成的,例如import java.util.*下面包含很多类,但是,在程序运行的时候,虚拟机只会加载哪些我们程序需要的类.这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性. 类加载的时机 类从