精华内容
下载资源
问答
  • 这就涉及到了JVM字节码执行引擎,执行引擎负责具体的代码调用及执行过程。就目前而言,所有的执行引擎的基本一致: 输入:字节码文件 处理:字节码解析 输出:执行结果。 物理机的执行引擎是由硬件实现的,和...

    我们都知道,在当前的Java中(1.0)之后,编译器讲源代码转成字节码,那么字节码如何被执行的呢?这就涉及到了JVM的字节码执行引擎,执行引擎负责具体的代码调用及执行过程。就目前而言,所有的执行引擎的基本一致:

    1. 输入:字节码文件

    2. 处理:字节码解析

    3. 输出:执行结果。

    物理机的执行引擎是由硬件实现的,和物理机的执行过程不同的是虚拟机的执行引擎由于自己实现的。


    运行时候的栈结构

    每一个线程都有一个栈,也就是前文中提到的虚拟机栈,栈中的基本元素我们称之为栈帧。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个栈帧都包括了一下几部分:局部变量表、操作数栈、动态连接、方法的返回地址 和一些额外的附加信息。栈帧中需要多大的局部变量表和多深的操作数栈在编译代码的过程中已经完全确定,并写入到方法表的Code属性中。在活动的线程中,位于当前栈顶的栈帧才是有效的,称之为当前帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。需要注意的是一个栈中能容纳的栈帧是受限,过深的方法调用可能会导致StackOverFlowError,当然,我们可以认为设置栈的大小。其模型示意图大体如下:  


    针对上面的栈结构,我们重点解释一下局部变量表,操作栈,指令计数器几个概念:

    1、局部变量表

    是变量值的存储空间,由方法参数和方法内部定义的局部变量组成,其容量用Slot1作为最小单位。在编译期间,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法,那局部变量表第0位索引的Slot存储的是方法所属对象实例的引用,因此在方法内可以通过关键字this来访问到这个隐含的参数。其余的参数按照参数表顺序排列,参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。举例说明:

    为了方便起见,假设以上两段代码在同一个类中。这时call()所对应的栈帧中的局部变量表大体如下:  
      
    而call2()所对应的栈帧的局部变量表大体如下:  


    2、操作数栈

    后入先出栈,由字节码指令往栈中存数据和取数据,栈中的任何一个元素都是可以任意的Java数据类型。和局部变量类似,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数中写入和提取内容,也就是出栈/入栈操作。操作数栈中元素的数据类型必须与字节码指令的序列严格匹配2,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。另外我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。


    3、动态连接

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


    4、方法返回地址

    存放调用调用该方法的pc计数器的值。当一个方法开始之后,只有两种方式可以退出这个方法:1、执行引擎遇到任意一个方法返回的字节码指令,也就是所谓的正常完成出口。2、在方法执行的过程中遇到了异常,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种方式成为异常完成出口。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。  
    无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,方法正常退出时,调用者的pc计数器的值作为返回地址,而通过异常退出的,返回地址是要通过异常处理器表来确定,栈帧中一般不会保存这部分信息。本质上,方法的退出就是当前栈帧出栈的过程。


    方法调用

    方法调用的主要任务就是确定被调用方法的版本(即调用哪一个方法),该过程不涉及方法具体的运行过程。按照调用方式共分为两类:

    1. 解析调用是静态的过程,在编译期间就完全确定目标方法。

    2. 分派调用即可能是静态,也可能是动态的,根据分派标准可以分为单分派和多分派。两两组合有形成了静态单分派、静态多分派、动态单分派、动态多分派

    解析

    在Class文件中,所有方法调用中的目标方法都是常量池中的符号引用,在类加载的解析阶段,会将一部分符号引用转为直接引用,也就是在编译阶段就能够确定唯一的目标方法,这类方法的调用成为解析调用。此类方法主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,因此决定了他们都不可能通过继承或者别的方式重写该方法,符合这两类的方法主要有以下几种:静态方法、私有方法、实例构造器、父类方法。虚拟机中提供了以下几条方法调用指令:

    1. invokestatic:调用静态方法,解析阶段确定唯一方法版本

    2. invokespecial:调用 <init> 方法、私有及父类方法,解析阶段确定唯一方法版本

    3. invokevirtual:调用所有虚方法

    4. invokeinterface:调用接口方法

    5. invokedynamic:动态解析出需要调用的方法,然后执行

    前四条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外[^footnote4])称为虚方法。

    分派

    分派调用更多的体现在多态上。

    1. 静态分派:所有依赖静态类型3来定位方法执行版本的分派成为静态分派,发生在编译阶段,典型应用是方法 重载 。

    2. 动态分派:在运行期间根据实际类型4来确定方法执行版本的分派成为动态分派,发生在程序运行期间,典型的应用是方法的重写 。

    3. 单分派:根据一个宗量5 对目标方法进行选择。

    4. 多分派:根据多于一个宗量对目标方法进行选择。

    JVM实现动态分派

    动态分派在Java中被大量使用,使用频率及其高,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率,因此JVM在类的方法区中建立虚方法表(virtual method table)来提高性能。每个类中都有一个虚方法表,表中存放着各个方法的实际入口。如果某个方法在子类中没有被重写,那子类的虚方法表中该方法的地址入口和父类该方法的地址入口一样,即子类的方法入口指向父类的方法入口。如果子类重写父类的方法,那么子类的虚方法表中该方法的实际入口将会被替换为指向子类实现版本的入口地址。  
    那么虚方法表什么时候被创建?虚方法表会在类加载的连接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。


    方法的执行

    解释执行

    在jdk 1.0时代,Java虚拟机完全是解释执行的,随着技术的发展,现在主流的虚拟机中大都包含了即时编译器(JIT)。因此,虚拟机在执行代码过程中,到底是解释执行还是编译执行,只有它自己才能准确判断了,但是无论什么虚拟机,其原理基本符合现代经典的编译原理,如下图所示:  
      
    在Java中,javac编译器完成了词法分析、语法分析以及抽象语法树的过程,最终遍历语法树生成线性字节码指令流的过程,此过程发生在虚拟机外部。

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

    Java编译器输入的指令流基本上是一种基于 栈 的指令集架构,指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。另外一种指令集架构则是基于 寄存器 的指令集架构,典型的应用是x86的二进制指令集,比如传统的PC以及Android的Davlik虚拟机。两者之间最直接的区别是,基于栈的指令集架构不需要硬件的支持,而基于寄存器的指令集架构则完全依赖硬件,这意味基于寄存器的指令集架构执行效率更高,单可移植性差,而基于栈的指令集架构的移植性更高,但执行效率相对较慢,初次之外,相同的操作,基于栈的指令集往往需要更多的指令,比如同样执行2+3这种逻辑操作,其指令分别如下:  
    基于栈的计算流程(以Java虚拟机为例):

    而基于寄存器的计算流程:


    基于栈的代码执行示例

    下面我们用简单的案例来解释一下JVM代码执行的过程,代码实例如下:

    使用javap指令查看字节码:

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


    1. 也成为容量槽,虚拟规范中并没有规定一个Slot应该占据多大的内存空间。 

    2. 这里的严格匹配指的是字节码操作的栈中的实际元素类型必须要字节码规定的元素类型一致。比如iadd指令规定操作两个整形数据,那么在操作栈中的实际元素的时候,栈中的两个元素也必须是整形。 

    3. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。 

    4. Animal dog=new Dog();其中的Animal我们称之为静态类型,而Dog称之为动态类型。两者都可以发生变化,区别在于静态类型只在使用时发生变化,变量本身的静态类型不会被改变,最终的静态类型是在编译期间可知的,而实际类型则是在运行期才可确定。 

    5. 宗量:方法的接受者与方法的参数称为方法的宗量。  
      举个例子:  
      public void dispatcher(){  
      int result=this.execute(8,9);  
      }  
      public void execute(int pointX,pointY){  
      //TODO  
      }  


      在dispatcher()方法中调用了execute(8,9),那此时的方法接受者为当前this指向的对象,8、9为方法的参数,this对象和参数就是我们所说的宗量。

    【本文转载自Java高级架构师,原文链接:https://mp.weixin.qq.com/s/rXdd7zEJxY4SBSSAg5Dw3w,转载授权请联系原作者】

    展开全文
  • 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步首先,抛出灵魂三问:虚拟机在执行代码的时候,如何找到正确的方法呢?如何执行方法内的字节码呢?执行代码时涉及的内存...

    6b681d01799f711d3c1da36dee1db72b.png

    学习导图

    2609202dd5b52f59c3c834c9fb16a899.png

    学习导图

    一.为什么要学习字节码执行引擎?

    代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步

    首先,抛出灵魂三问:

    • 虚拟机在执行代码的时候,如何找到正确的方法呢?
    • 如何执行方法内的字节码呢?
    • 执行代码时涉及的内存结构有哪些呢?

    如果你对上述问题理解得还不是特别透彻的话,可以看下这篇文章;如果理解了,你可以关闭网页,打开游戏放松了hhh

    下面,笔者将带你探究 JVM 核心的组成部分之一——执行引擎。

    二.核心知识点归纳

    2.1 概述

    Q1:虚拟机与物理机的异同

    • 相同点:都有代码执行能力
    • 不同点:
    物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的虚拟机的执行引擎是由自定义的,可自行制定指令集与执行引擎的结构体系,且能够执行不被硬件直接支持的指令集格式

    Q2:有关 JVM 字节码执行引擎的概念模型

    • 外观上:所有 JVM 的执行引擎都是一致的。输入的是字节码文件,处理的是字节码解析的等效过程,输出的是执行结果

    57a6eb723db87d57144a47ac7162491d.png

    执行引擎的外观

    • 从实现上,执行引擎有多种执行 Java 代码的选择
    解释执行:通过解释器执行编译执行:通过即时编译器产生本地代码执行两者兼备,甚至还会包含几个不同级别的编译器执行引擎

    2.2 运行时栈帧结构

    2.2.1 基本概念

    • 栈帧:用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素
    • 存储内容:方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息
    • 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
    • 一个栈帧需要分配多少内存在程序编译期就已确定,而不会受到程序运行期变量数据的影响
    • 对于执行引擎来说,只有位于栈顶的栈帧(当前栈帧)才是有效的,即所有字节码指令只对当前栈帧进行操作,与当前栈帧相关联的方法称为当前方法

    a0e083d660735826502c1975546d4af0.png

    栈帧结构

    2.2.2 局部变量表

    • 定义:局部变量表是一组变量值存储空间
    • 作用:存放方法参数和方法内部定义的局部变量
    • 分配时期:Java 程序编译为 Class 文件时,会在方法的 Code 属性的 max_locals 数据项中确定了该方法所需要分配的局部变量表的最大容量
    • 最小单位:变量槽
    大小:虚拟机规范中没有明确指明一个变量槽占用的内存空间大小,允许变量槽长度随着处理器、操作系统或虚拟机的不同而发生变化对于 32 位以内的数据类型(boolean、byte、char、short、int、float、reference、returnAddress ),虚拟机会为其分配一个变量槽空间对于 64 位的数据类型(long、double ),虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间特点:可重用。为了尽可能节省栈帧空间,若当前字节码 PC 计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
    • 访问方式:通过索引定位。索引值的范围是从 0 开始至局部变量表最大的变量槽数量
    • 局部变量表第一项是名为 this 的一个当前类引用,它指向堆中当前对象的引用(由反编译得到的局部变量表可知)局部变量表

    2.2.3 操作数栈

    • 操作数栈是一个后入先出栈
    • 作用:在方法执行过程中,写入(进栈)和提取(出栈)各种字节码指令
    • 分配时期:同上,在编译时会在方法的 Code 属性的 max_stacks 数据项中确定操作数栈的最大深度
    • 栈容量:操作数栈的每一个元素可以是任意的 Java 数据类型 ——32 位数据类型所占的栈容量为 1,64 位数据类型所占的栈容量为 2
    • 注意:操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证

    2.2.4 动态连接

    • 定义:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
    • 静态解析和动态连接区别:
    Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用:
    一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态解析)另一部分会在每一次运行期间转化为直接引用(动态连接

    2.2.5 方法返回地址

    • 方法退出的两种方式:
    正常退出:执行中遇到任意一个方法返回的字节码指令异常退出:执行中遇到异常、且在本方法的异常表中没有搜索到匹配的异常处理器区处理
    • 作用:在方法返回时都可能在栈帧中保存一些信息,用于恢复上层方法调用者的执行状态
    正常退出时,调用者的 PC 计数器的值可以作为返回地址异常退出时,通过异常处理器表来确定返回地址
    • 方法退出的执行操作:
    恢复上层方法的局部变量表和操作数栈若有返回值把它压入调用者栈帧的操作数栈中调整 PC 计数器的值以指向方法调用指令后面的一条指令等

    在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部一起称为栈帧信息

    2.3 方法调用

    • 方法调用是最普遍且频繁的操作
    • 任务:确定被调用方法的版本,即调用哪一个方法,不涉及方法内部的具体运行过程
    下面笔者将为大家详细讲解方法调用的类型

    2.3.1 解析调用

    • 特点:是静态过程在编译期间就完全确定,在类装载解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,而不会延迟到运行期再去完成,即编译期可知、运行期不变
    • 适用对象:private 修饰的私有方法,类静态方法,类实例构造器父类方法

    2.3.2 分派调用

    Q1:什么是静态类型?什么是实际类型?

    A1:这个用代码来说比较简便, Talk is cheap ! Show me the code !

    //父类
    public class Human {
    }
    //子类
    public class Man extends Human {
    }
    public class Main {
    
        public static void main(String[] args) {
            //这里的 Human 是静态类型,Man 是实际类型
            Human man=new Man();
        }
    
    }

    1.静态分派

    依赖静态类型来定位方法的执行版本典型应用是方法重载发生在编译阶段,不由 JVM 来执行
    单纯说未免有些许抽象,所以特地用下面的 DEMO 来帮助了解
    public class Father {
    }
    public class Son extends Father {
    }
    public class Daughter extends Father {
    }
    public class Hello {
        public void sayHello(Father father){
            System.out.println("hello , i am the father");
        }
        public void sayHello(Daughter daughter){
            System.out.println("hello i am the daughter");
        }
        public void sayHello(Son son){
            System.out.println("hello i am the son");
        }
    }
    public static void main(String[] args){
        Father son = new Son();
        Father daughter = new Daughter();
        Hello hello = new Hello();
        hello.sayHello(son);
        hello.sayHello(daughter);
    }

    输出结果如下:

    hello , i am the father
    hello , i am the father

    我们的编译器在生成字节码指令的时候会根据变量的静态类型选择调用合适的方法。就我们上述的例子而言:

    3c63f62f887db4dc2d9c791abd2bb43b.png

    字节码指令调用情况

    2.动态分派

    依赖动态类型来定位方法的执行版本
    典型应用是方法重写
    发生在运行阶段,由 JVM 来执行
    单纯说未免有些许抽象,所以特地用下面的 DEMO 来帮助了解
    public class Father {
        public void sayHello(){
            System.out.println("hello world ---- father");
        }
    }
    
    //继承 + 方法重写
    public class Son extends Father {
        @Override
        public void sayHello(){
            System.out.println("hello world ---- son");
        }
    }
    
    public static void main(String[] args){
        Father son = new Son();
        son.sayHello();
    }
    

    输出结果如下:

    hello world —- son

    我们接着来看一下字节码指令调用情况

    1801714da78259ccebe42567fa7a37da.png

    字节码指令

    7c16e108e59df21dc49a73d505b4948d.png

    字节码指令

    疑惑来了,我们可以看到,JVM 选择调用的是静态类型的对应方法,但是为什么最终的结果却调用了是实际类型的对应方法呢?

    当我们将要调用某个类型实例的具体方法时,会首先将当前实例压入操作数栈,然后我们的 invokevirtual 指令需要完成以下几个步骤才能实现对一个方法的调用:

    ed934cd056ef39c1ee71c826ecc9c00b.png

    因此,疑惑释然

    3949129ce6475362a1fcf396c030269b.png

    正解

    3.单分派

    • 含义:根据一个宗量对目标方法进行选择(方法的接受者与方法的参数统称为方法的宗量)

    4.多分派

    • 含义:根据多于一个宗量对目标方法进行选择

    小编每天都会发布或转载一些优秀的文章内容,如果觉得有帮助的话给小编点个关注8,也可以给小编点赞、收藏、转发。你们的支持是小编最大的动力!谢谢大家了~~

    展开全文
  • 深入理解JVM-1】JVM字节码文件剖析一、背景二、二进制字节码文件分析1. 使用javap -verbose 命令分析字节码文件1.1 分析字节码文件的魔数、版本号、常量池1.2 常量池1.3 AccessFlag访问标志、当前类名、当前父类名...

    一、背景

        

    二、二进制字节码文件分析

    1. 使用javap -verbose 命令分析字节码文件

    Java字节码整体结构如下所示:
    Java字节码整体结构

    1.1 分析字节码文件的魔数、版本号、常量池

    • 工具:Hex_Fiend,下载地址
    • 以单字节为间隔展示文件如下:
      真实二进制字节码
    • 魔数:CA FE BA BE,所有的.class字节码文件的前4字节都是魔数,固定值为0xCAFEBABE
    • 版本号:00 00 00 31,前面两字节为次版本号0,后面两字节为主版本号49

    1.2 常量池

    • 常量池( constant pool):紧接着主版本号之后的即常量池。一个Java类中定义的很多信息都是由常量池来维护和描述的,可将常量池看作是class文件的资源仓库,例如Java类中定义的方法与变量信息,均存储在常量池中。常量池中主要存储两类信息:字面量、符号引用。
      • 字面量:文本字符串、Java类中声明为final的常量值等
      • 符号引用:类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符(与源码的对应关系)
      • 常量池总体结构:Java类所对应的常量池主要由常量池数据量(紧跟主版本号后面,占2字节)与常量池数组( 紧跟常量池数量后面)两部分共同构成。常量池数组中不同元素的类型、结构均不同,但是每个元素的第一个数据都是一个u1类型,该字节是个标志位,占1个字节,JVM在解析常量池时,会根据这个u1类型来获取元素的具体类型。
      • 常量池数组中元素个数=常量池数-1(0暂不使用),满足某些常量池索引值的数据表达为【不引用任何一个常量池】的含义
      • 如图,00 18代表常量池数量为23。

    常量池中11种数据类型的结构
    在这里插入图片描述

    • 如上图,标注为第一个常量池,0A为1字节的tag,值为10,代表CONSTANT_Methodref_info,
    • 00 04为2字节的index,值为4,指向声明方法的类描述符,即如下的“#4 = Class #23 // java/lang/Object”
    • 00 14为2字节的index,值为20,指向名称及类型描述符,即如下的“#20 = NameAndType #7:#8 // “”😦)V”,指向#7和#8,方法名称为构造方法,类型描述符为()V。十六进制可以直接通过ASCLL转换成具体的字符
    STARLYWANG-MB0:classes starlywang$ javap -verbose com.bytecode.MyTest1
    Classfile /Users/starlywang/IdeaProjects/comtest/target/classes/com/bytecode/MyTest1.class
      Last modified 2020-3-1; size 471 bytes
      MD5 checksum c75cdac216156e899c9491c4738e0b60
      Compiled from "MyTest1.java"
    public class com.bytecode.MyTest1
      minor version: 0 //小版本
      major version: 49  //大版本
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:  //常量池
       #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#21         // com/bytecode/MyTest1.a:I
       #3 = Class              #22            // com/bytecode/MyTest1
       #4 = Class              #23            // java/lang/Object
       #5 = Utf8               a
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               Lcom/bytecode/MyTest1;
      #14 = Utf8               getA
      #15 = Utf8               ()I
      #16 = Utf8               setA
      #17 = Utf8               (I)V
      #18 = Utf8               SourceFile
      #19 = Utf8               MyTest1.java
      #20 = NameAndType        #7:#8          // "<init>":()V
      #21 = NameAndType        #5:#6          // a:I
      #22 = Utf8               com/bytecode/MyTest1
      #23 = Utf8               java/lang/Object
    {
      public com.bytecode.MyTest1(); //构造方法
        descriptor: ()V  //描述符
        flags: ACC_PUBLIC  //标记
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: iconst_1
             6: putfield      #2                  // Field a:I
             9: return
          LineNumberTable:
            line 9: 0
            line 10: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      10     0  this   Lcom/bytecode/MyTest1;
    
      public int getA();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field a:I
             4: ireturn
          LineNumberTable:
            line 13: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/bytecode/MyTest1;
    
      public void setA(int);
        descriptor: (I)V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: iload_1
             2: putfield      #2                  // Field a:I
             5: return
          LineNumberTable:  //行号表
            line 17: 0
            line 18: 5
          LocalVariableTable:  //局部变量表
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcom/bytecode/MyTest1;
                0       6     1     a   I
    }
    SourceFile: "MyTest1.java"
    
    
    • JVM规范中,每个变量/字段斗鱼描述信息,主要作用是描述字段的数据类型、方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对象全限定名称来表示。
    • 为了压缩字节码文件的体积,JVM只使用一个大写字母来表示,如下所示:B-byte、C-char、D-double、F-float、I-int、J-long、S-short、Z-boolean、V-void、L-对象类型,如Ljava/lang/String
    • 对于数组类型来说,每一个维度使用一个前置的 [ 来表示,如int[]被表示为[I, String[][]被记录为[[Ljava/lang/String
    • 用描述符描述方法时,按照先参数列表,后返回值的顺序来描述#### 1.2 类信息、类的构造方法、类中方法信息、类变量与成员变量等信息

    1.3 AccessFlag访问标志、当前类名、当前父类名、当前接口数量

    访问标志信息包括:该类是类还是接口、是否被定义成public、是否是abstract、如果是类则是否被声明成final.
    访问标志信息

    • 如下图,00 21是0020与0001的并集,表示ACC_PUBLIC与ACC_SUPER,为public并且能调用父类方法。
    • 00 03,即对应的常量池中#3,为当前类全路径名的索引
    • 00 04,即对应的常量池中#4,为当前类父类的全路径名的索引
    • 00 00,即当前接口数量为0
      在这里插入图片描述

    1.4 字段表结构

    在这里插入图片描述
    在这里插入图片描述
    字段表用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,不包括方法内部声明的局部变量。

    • 00 01:表示包含一个字段
    • 00 02:字段即#2
    • 00 05:字段名字索引即a
    • 00 06:字段描述符索引即I
    • 00 00:没有属性

    1.5 方法结构

    在这里插入图片描述
    方法的属性结构

    • 00 03:总共有3个方法
    • 00 01:访问信息为ACC_PUBLIC
    • 00 07:方法名#7
    • 00 08:方法描述符#8
    • 00 01:方法的属性结构
    • 00 09:#9即Code
    • 00 00 00 38:属性长度为56字节

    Code结构:Code属性的作用是保留该方法的结构,arrtribute_length表示属性所包含的字节数,不包含arrtibute_name_index和attribute_length字段;max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度;max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入参数的局部变量。

    Code结构

    1.6 附加属性

    其余更详细信息可通过jclasslib来查看,IDEA亦有可直接用的插件。

    2. 方法表结构

    方法表以指令的形式来标记字节码,每一个方法里面都会有Code属性,用来保存方法的结构,如对应字节码的信息。如上图为Code属性的主要结构:

    • arrtribute_length表示属性所包含的字节数,不包含arrtibute_name_index和attribute_length字段;
    • max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度;
    • max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入参数的局部变量。
    • code_length表示该方法所包含的字节码的字节数以及具体的字节码,具体字节码即是该方法被调用时,虚拟机所执行的字节码
    • exception_table,存放处理异常的信息,每个exception_table表项由start_pc、end_pc、handler_pc、catch_type组成

    在这里插入图片描述

    展开全文
  • 深入理解JVM-字节码

    千次阅读 2020-03-07 21:11:00
    本文参考圣思园张龙深入理解jvm 目录 Java字节码结构 Access_Flag访问标志 Fileds 字段表 Methods 方法表: 方法的属性结构 Code结构 其他结构 附加属性表 字节码补充注意事项 栈帧 字节码解释执行 ...

    本文参考圣思园张龙深入理解jvm

    目录

    Java字节码结构

    Access_Flag访问标志

    Fileds 字段表

    Methods 方法表:

    方法的属性结构

    Code结构

    其他结构

     附加属性表

    字节码补充注意事项

    栈帧

    字节码解释执行


    Java字节码结构

    Class字节码中有两种数据类型:

    1. 字节数据直接量:基本数据类型,共细分为 u1,u2,u4,u8四种,分表代表连续的1,2,4,8个字节组成的整体数据。
    2. 表(数组):表是由多个基本数据或者他表,按照既定顺序组成的大的数据集合。表示有结构的,它的结构 体现在:组成表我的成分所在的位置和顺序都是已经严格定义好的。

    1. 使用javap –verbose 命令分析一个字节码文件是,将会分析该字节码文件的魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息。
    2. 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数值为固定值:0xCAFEBABE    
    3. 魔数之后的4个字节为版本信息,前两个字节表示为minor version次版本号,后两个字节表示major version 主版本号。主版本号52 jdk8 51-7 50-6 49-5。次版本号0,则表示1.8.0
    4. 常量池 constant pool:紧接着主版本号之后就是常量池入口。一个java类中定义的很多信息都是由常量池来维护和描述的。可以讲常量池看做是class文件的资源仓库,比如说java类中定义的方法与变量信息,都是存储在常量池中。常量池中主要存储两类常量:字面量与符号引用。字面量如文本字符串,java声明为final的常量值等,而符号引用如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符等。
    5. 常量池的总体结构:java类所对应的常量池主要有常量池数量与常量池数组两部分共同构成。常量池数量紧跟在主版本号后面,占2个字节:常量池数组则紧跟在常量池数量之后。常量池数组与一般数组不同的是,常量池数组中不同的元素的类型、结构都是不同的,单独当然不同;但是,每一种元素的第一个数据都是一个u1类型,该字节是个标志位,占据一个字节。Jvm在解析常量池是,会根据这个u1类型来获取元素的具体类型。常量池数组中的元素的个数=常量池数-1(其中0暂时不使用),目的是满足某些常量池索引值的数据在特定情况下徐表达【不引用任何一个常量池】的含义;根本原因在于,索引为0也是一个常量,保留常量,只不过他它部位与常量表中,这个常量就对应null值,所以常量池索引从1开始。
    6. 在jvm规范中,每个变量or字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量、类型与顺序)与返回值。根据描述符规则,基本数据类型和逮捕无返回值的void类型都用一个大写字符来表示,对象类型则使用字符L加对性的全限定名称来表示。为了压缩字节码的提及,对于基本数据类型,jvm都只是用一个大写字母来表示,如:B-byte, C-char, D-double,F-float,I-int,J-long,S-short,Z-boolean,V-void,L-对象类型,如Ljava/lang/String;
    7. 对于一个数组类型来说,每一个维度使用一个前置的[来表示,如int[].被记录为[I, String[][],被记录为[[Ljava/lang/String;
    8. 用描述符来描述方法是,按照先参数列表,后返回值的顺序来描述。参数列表按照参数的严格顺序放在一组()之内,如方法:String getRealnamebyIDAndNickname(int id,String name)的描述符为:(I,Ljava/lang/String;)Ljava/lang/String; <init>类的构造方法。

    Access_Flag访问标志

    访问标志信息包括含该Class文件是类还是接口,是否被定义成Public,是否是abstract。如果是类,是否被声明陈final。通过上面的源代码,我们只带该文件是类并且是Public.

    0x0021 是 0x0020 0x0001的并集,表示acc_public 和 acc_super

    Fileds 字段表

    用于描述类和接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。

    第一个表示该变量为 private

     

    Methods 方法表:

    方法的属性结构,方法中的每个属性都是一个attribute_info结构。

     

     

    方法的属性结构

    Jvm预订了部分attribute,但是编译器自己也可以实现自己的attribute写入class文件里,供运行时使用。

    不同的attribute通过attribute_name_index来区分。

     

    Code结构

    Code attribute的作用是保存该方法的结构,如对应的字节码:

    Attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。

    Max_stack 表示这个方法运行的任何时刻所能达到的操作数栈的最大深度

    Max_loacals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。

    Code_length表示该方法所包含的字节码的字节数以及具体的指令码

    具体字节码即是该方法被调用时,虚拟机执行的字节码

    Exception_table,这里存放的是处理异常的信息

    每个Exception_table都有start_pc.end_pc,handler_pc,catch_type组成

    其他结构

    附加属性:

    LineNumberTable 这个属性用来表示code数组中的字节码和java代码行数之间的关系。这个属性可以用来在调试的时候定位代码执行的行数。

    • “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
    • “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。

    Valuevirtable 表示的是变量的有效域

     附加属性表

    字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。

    参考:https://segmentfault.com/a/1190000020345321?utm_source=tag-newest#item-1-1

    字节码补充注意事项

    Java类中的变量的声明最后会放入到构造方法中。

    对于java类中的每一个实例方法(非static方法),其在编译或所生产的字节码当中,方法参数的数量总是会比源代码中方法的参数多一个this,它唯一方法的第一个参数位置处,这样我就可以在java的实例方法中使用this来访问当前对象的属性以及其他方法。

    这个操作是在编译期间完成的,是由javac编译器在编译的时候将对this的访问转化为对一个普通实例方法参数的访问,接下来在运行期间,由jvm再掉用哪个实例的方法时,自动向实例方法传入该this参数。所以在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量。

     

    Java字节码对与异常的处理方式:

    1. 统一采用异常表的方式来对异常进行处理
    2. 在jdk 1.4.2之前版本,并不是用异常表来对异常处理,而是采用特定的指令方式。
    3. 当异常处理存在finally语句时,现代化的jvm采取的处理方式是将finally语句块的字节码拼接到每一个catch块后,程序中存在多少个catch块,就会在每一个catch块后面重复多少个finally语句块的字节码。

     

    如果我们在方法后throw 一个exception,则会在二进制文件中与code出现一个并列的exceptions

     

    栈帧

    Stack frame https://www.cnblogs.com/jhxxb/p/11001238.html

    栈帧是一种用于帮助jvm执行方法调用与方法执行的数据结构。

    栈帧是一种数据结构,封账了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。 每个栈帧都是有一个线程来执行

    符号引用,直接引用

     https://img-blog.csdn.net/20180814220244123?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjQ0Nzk1OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70  

    有些符号引用会在类加载阶段或者是第一次使用就会转换为直接引用,这种转化叫做静态解析;另一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为java的多态性。

    1. invokeinterface: 调用接口中的方法,实际上是在运行期决定的,决定到底调用实现接口的哪个对象的特点方法。
    2. invokestatic*:调用静态方法
    3. invokespecial: 调用自己的私有方法、构造方法(<init>)以及父类方法。
    4. invokevirtual*: 调用虚方法。运行期动态查找的过程。
    5. invokedynamic:动态调用发放。

     

    静态解析的4中情形:1.静态方法 2.父类方法 3.构造方法 4.私有方法(公有方法会被重写和复写,所以不可以)

    以上四类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的。

    结果:

    上面程序涉及到 方法的静态分派

    g1静态类型是grandpa,而实际类型(真正的指向类型)是father。

    结论:变量的静态类型是不会发生变化的,而实际类型则是可以发生变化的。多态的一种体现。实际类型是在运行期才可以确定的。

     

    上面代码中,test有多个方法,是方法重载 ,是一种静态的行为。Jvm判断的依据就是根据方法接受参数的静态类型来判断调用哪一种方法。编译期就可以完全确定。

     

    重载本身是一种静态的概念,重写是一种动态的概念。

    但是我们会看到apple.test()对应的invokevirtual是fruit.test方法

     

    方法的动态分派

    方法的动态分派涉及到一个重要的概念:方法接受者

    Invokevitrual字节码指令的多态查找流程

     

    Invokevitrual找到操作数栈顶的第一个元素所指向对象的实际类型,也就是apple变量找到了Apple这个类,

    如果在这个类型中,寻找到与常量池中的描述符(Fruit中test方法 )与描述符相同的方法,并且具有访问权限,如果通过,就返回这个目标方法的直接引用。

    如果找到不,从继承关系,从子类到父类寻找。

    Invokevitrual在运行期确定方法接受者的实际类型是什么。所以虽然apple,orange变量Invokevitrual相同的符号引用,但被解析到不同的直接引用上了。

     

    针对于方法调用动态分派的过程,jvm会在类的方法区建立一个虚方法标的数据结构,(virtual method table, vtable),类似于上述invokeinterface,jvm会建立一个接口方法表的数据结构(interface method table, itable)。Vtable,如子类如果继承的方法与父类相同,那么子类不会直接复制一份,子类在方法表中直接指向父类方法的入口地址

     

    字节码解释执行

    现代jvm在执行java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。所谓解释执行就是通过解释器来读取字节码,遇到相应的指令就去执行该指令。编译执行就是通过即时编译器(just in time,JIT)将字节码转换为本地机器码来执行。现代jvm会根据代码特点来生成相应的本地机器码。

    基于栈的指令集与基于计寄存器的指令集之间的关系:

    1. jvm执行指令时所采取的的方式是基于栈的指令集
    2. 基于栈的指令集主要的操作有入栈和出栈
    3. 基于栈的指令集的优势在于可以在不同平台之间移植,而基于寄存器的指令集是与硬件架构密切相关的,无法做到可移植。
    4. 基于栈的指令集缺点在于完成相同的操作,指令数量通常要比基于寄存器的指令集数量要多;基于栈的指令集是在内存中完成操作的,而寄存器指令集是直接由CPU来执行的,他是在高速缓存区中进行执行的,速度要快的多。虽然jvm会有优化,但总体,栈的还是要慢一些。

     

    动态代理设计模式https://www.jianshu.com/p/fc285d669bc5

    ***深入研究

    展开全文
  • 字节码 意义 ...在执行过程中,JVM字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码。会通过JIT动态地编译为机器码,提高执行效率。 字节码主要指令 1、加
  • 深入理解JVMJVM字节码指令集

    千次阅读 2016-03-13 22:27:46
    Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作。如果忽略...
  • 学好字节码指令对于深入理解JVM有更好的帮助,能够清除的了解各种字节码指令的含义与作用。 Java虚拟机你的指令由一个字节长度的,代表着某种特定操作含义的数字(成为操作码,Opcode)以及跟随其后的零至多个代表此...
  • 通常情况下我们都知道编写的.java文件编译成.class之后,由类加载器ClassLoader加载、链接、初始化等一系列操作。...接下来进行分析,class 字节码包含的信息其实就是JVM定义了一系列的格式来表示字节码或者类...
  • 字节码技术 字节码技术应用场景 AOP技术、Lombok去除重复代码插件、动态修改class文件等 字节技术优势 Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序...
  • 读了深入理解JVM之虚拟机字节码执行引擎这一章,
  • 深入理解JVM-字节码执行引擎

    千次阅读 2014-10-16 21:23:42
    前面我们不止一次的提到,Java是一种跨平台的语言,为什么可以跨平台,因为我们编译的结果是中间代码—字节码,而不是机器码,那字节码在整个Java平台扮演着什么样的角色的呢?JDK1.2之前对应的结构图如下所示: ...
  • 前面我们不止一次的提到,Java是一种跨平台的语言,为什么可以跨平台,因为我们编译的结果是中间代码—字节码,而不是机器码,那字节码在整个Java平台扮演着什么样的角色的呢?JDK1.2之前对应的结构图如下所示:   ...
  • 深入理解JVM--字节码文件结构解析 在cmd窗口使用javap -verbose 类名称 命令分析一个字节码文件时,将会分析该字节码文件的魔数、版本号、常量池、类信息、类的构造方法、类中的方法信息、类变量与成员变量等信息。...
  • Understanding bytecode makes you a better programmer ...因此,本文从class字节码文件的结构入手,一步步来解剖二进制字节码的内部工作原理,这对深入理解JVM的运行机制大有裨益,同时,对于想要使用BCEL来动态改变
  • 深入理解 JVM 垃圾回收机制及其实现原理

    万次阅读 多人点赞 2018-05-31 13:47:17
    JVM 有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统,其本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。 Java 语言的可移植性正是建立在 JVM 的基础上。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,676
精华内容 670
关键字:

深入理解jvm字节码