从字节码指令看重写在JVM中的实现

Java是解释执行的,包括动态链接的特性,都给解析或运行期间提供了很多灵活扩展的空间。面向对象语言的继承、封装和多态的特性,在JVM中是怎样进行编译、解析,以及通过字节码指令如何确定方法调用的版本是本文如下要探讨的主要内容,全文围绕一个多态的简单举例来看在JVM中是如何实现的。

先简单介绍几个概念。对于字节码执行模型及字节码指令集的相关概念可以参考之前的一篇介绍http://blog.csdn.net/lijingyao8206/article/details/46562933

一、方法调用的解析

在Class文件中,方法调用是常量池中的一个符号引用,在加载的解析期或者运行时才能确定直接引用。

下面两种指令是在解析期就可以确定直接引用,调用的对应方法也叫作非虚方法。

1、  invokestatic 主要用于调用静态方法

2、  invokespecial 主要用于调用私有方法,构造器,父类方法。

下面两种是在运行时才能确定直接引用的,但是除了final方法,final方法也可以在解析期确定方法的调用版本。

1、  invokevirtual 虚方法,不确定调用那一个实现类

2、  invokeinterface 接口方法,运行时才能确定实现接口的对象。

二、动态分派

先回顾一下静态和动态分派的概念。Java中,所有以静态类型来定位方法执行版本的分派动作,都称为静态分派。其实也就是重载(Overload)就是一种典型的静态分派,在编译期就可以知道方法调用的实际版本。相对得,动态分派是需要在运行期才能确定方法的版本,也就是直接引用,一种典型应用就是重写(OverWrite)。在调用invokevirtual指令时,把常量池中的类方法符号引用解析到直接引用的过程就是重写的过程,运行期根据实际类型确定方法的执行版本。

三、解释执行

Java的解释执行机制,使jaavc编译的过程涵盖了从程序代码的语法、词法分析,再到AST(抽象语法树)生成线性的字节码指令流的过程。而解释执行是在JVM内部,基于栈的指令集提供了整个平台的可移植性支撑,所以这也是java执行慢的要因,因为不像编译执行,过程中需要更多的出入栈指令,而栈又是内存的一个区块,对内存的频繁访问降低了性能。

四、重写(OverWrite)举例

下面通过一个简单的例子来看一下重写在JVM中的字节码执行模型。

父类:

package bytecode;

/**
 * Created by yunshen.ljy on 2015/7/27.
 */
public class Wine {

    public String drink(int ml) {
        return "drink " + ml + "ml wine";
    }

}

子类:

package bytecode;
/**
 * Created by yunshen.ljy on 2015/7/27.
 */
public class Beer extends Wine{

    /**
     * 重写父类方法,实现多态
     */
    public String drink(int ml){
        return "drink " + ml +"ml beer";
    }
}

调用:

package bytecode;

/**
 * Created by yunshen.ljy on 2015/7/26.
 */
public class MethodInvotionTest {

    public String drink(int ml) {

        Wine wines = new Beer();
        return wines.drink(ml);

    }
}

我们都知道,方法调用返回的结果是“drink XX ml beer”。但是在编译期,字节码中的指令是无法确定实际调用的方法版本的。下面看一下调用方法的字节码结构。

public java.lang.String drink(int);
    descriptor: (I)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #2                  // class bytecode/Beer
         3: dup
         4: invokespecial #3                  // Method bytecode/Beer."<init>":()V
         7: astore_2
         8: aload_2
         9: iload_1
        10: invokevirtual #4                  // Method bytecode/Wine.drink:(I)Ljava/lang/String;
        13: areturn
      LineNumberTable:
        line 10: 0
        line 12: 8

第一条指令new 创建对象,把引用入栈指令,new 指令后面的#4就是前文提到的,对于运行时常量池的一个引用,只是javap命令处理成比较易懂的方式来显示。接着,后面的dup 指令复制刚放入的引用(操作数栈栈顶的值的复制并且将这个“副本”放到栈顶)。Invokespecial 指令就是之前介绍的非虚方法调用指令,在操作数栈中通过其中的一个引用调用Beer的构造器,初始化对象,让另一个相同引用指向初始化的对象,然后前一个引用(this)弹出栈。astore_2把引用保存到局部变量表中的索引2位置中。aload_2
把刚才局部变量表中索引2处的值压入操作栈。iload_1将int参数,也就是局部变量表中索引1处的值压入操作数栈。Invokevirtual指令将执行操作数栈中的两个值出栈,执行方法调用,指令后的#4只是常量池中的一个符号引用,只有在运行时,才能确定方法的直接引用。最后的areturn语句将方法执行结果的值出栈,消除当前的栈帧,如果上层有对于当前方法的继续调用,那么会将调用的方法的栈帧设置成当前栈帧(current stack frame)。

下面通过对比每条指令执行后,局部变量表和操作数栈中的值来加强一下上述的出栈、入栈操作。首先通过stack=2, locals=3, args_size=2 ,先知道了方法的局部变量表是占用了3个slot,操作数栈的size是2个slot。而因为我们的方法是实例方法(非类方法),所以隐含的this关键字的指令我们先描述到局部变量表的第一个位置,占用一个slot。(以下不再画出常量池的结构,可参照calss文件自行分析)

这里只是个简单的举例,和一些知识的回顾。知道JVM的执行引擎和字节码指令的一些概念,会让我们对于程序执行的结果预期更加准确,也更易理解一些java语言版本的设计模式实现。这里只是抛砖引玉,对于其他几种方法调用的指令,也建议大家尝试进行更多的分析。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-12-23 10:55:48

从字节码指令看重写在JVM中的实现的相关文章

JVM总括三-字节码、字节码指令、JIT编译执行

JVM总括三-字节码.字节码指令.JIT编译执行 java文件编译后的class文件,java跨平台的中间层,JVM通过对字节码的解释执行(执行模式,还有JIT编译执行,下面讲解),屏蔽对操作系统的依赖.一个字节(8位)可以储存256中不同的指令,这样的指令就是字节码,java所有指令有200个左右,这些指令组成了字节码文件(.class). 一.字节码的主要指令: .class文件里面的十六进制文件(如:图一),其中CAFE BABE是标志这个文件为java的编译后的文件,00000034代表

深入理解java虚拟机(六)字节码指令简介

Java虚拟机指令是由(占用一个字节长度.代表某种特定操作含义的数字)操作码Opcode,以及跟随在其后的零至多个代表此操作所需参数的称为操作数 Operands 构成的.由于Java虚拟机是面向操作数栈而不是寄存器的架构,所以大多数指令都只有操作码,而没有操作数. 字节码指令集是一种具有鲜明特点.优劣势都很突出的指令集架构: 由于限定了Java虚拟机操作码的长度为1个字节,指令集的操作码不能超过256条.Class文件格式放弃了编译后代码中操作数长度对齐,这就意味者虚拟机处理那些超过一个字节数

字节码指令

Java 虚拟机指令集所支持的数据类型: opcode byte short int long float double char reference Tipush bipush sipush Tconst iconst lconst fconst dconst aconst Tload iload lload fload dload aload Tstore istore lstore fstore dstore astore Tinc iinc Taload baload saload ia

利用无效字节码指令引发逆向工具崩溃(二)

上一节我们介绍了一种利用无效字节码指令引发逆向工具崩溃的方法.可惜的是大部分反编译工具已经修复了该bug.但是如果我们插入有效的字节码指令,但是后跟无效的数据引用,结果会是怎么样呢? 使用C32asm,以十六进制的方式打开dex文件.按快捷键"Ctrl + G",定位到"0003A2A4" 把"62 00 02 04 1A 01 8E 07 6E 20 19 10 10 00"改为"12 01 38 01 03 00 1A 00 FF

Java字节码指令

1. 简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成. 由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码. Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条. 2. 字节码和数据类型 在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息.例如,iload指令用于从局部变量中加

[jvm解析系列][十三]字节码指令小节,从字节码看JVM的栈解释器执行过程。

众所周知,JVM以前一直采用的是解释执行,但是后来在历代的版本更迭中也加入了编译执行.所以总的来说JVM是包含了解释执行和编译执行.这一部分不属于JVM的范畴了,已经属于编译了,大多数都是进行词法分析之类的,以后有时间会补充. 同时大家都知道现在大体上分为两种指令集架构,第一种就是基于栈的第二种是基于寄存器的,简单点说,基于寄存器的架构速度更快,但是可移植性不强,但是基于栈的指令集架构虽然慢,但是可移植性很强,大家都知道java本身就是依靠可移植性出名的,所以无可争议的使用了栈的指令集架构.(也

JVM | 字节码指令基础

操作数栈管理指令 1)pop.pop2:将操作数栈的栈顶一个或两个元素出栈.2)dup.dup2.dup_x1.dup2_x1.dup_x2.dup2_x2:复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶.3)swap:将栈最顶端两个数值互换. public static void main(String[] args) { heavyMethod(); } 对应的字节码: public static void main(java.lang.String[]); Signature:

JVM入栈字节码指令

对应指令码 助记符 说明 0x00 nop 什么都不做 0x01 aconst_null 将null推送至栈顶 0x02 iconst_m1 将int型-1推送至栈顶 0x03 iconst_0 将int型0推送至栈顶 0x04 iconst_1 将int型1推送至栈顶 0x05 iconst_2 将int型2推送至栈顶 0x06 iconst_3 将int型3推送至栈顶 0x07 iconst_4 将int型4推送至栈顶 0x08 iconst_5 将int型5推送至栈顶 0x09 lcons

在Myeclipse下查看Java字节码指令信息

在实际项目开发中,有时为了了解Java编译器内部的一些工作,需要查看Java文件对应的具体的字节码指令集,这里提供两种方式供参考. 一.使用javap命令 javap是JDK提供的一个原生工具,它可以反汇编class文件得到对应的字节码信息,通过调节命令参数,可以获取类的package.fileds和methods等的字节码信息,用的最多的参数也就是-verbose了: 二.Bytecode Outline插件 javap固然可以实现反编译字节码的效果,但是每次都要在命令行中执行,显得有点麻烦,