10.1常量入栈操作
许多操作码执行常量入栈操作。操作码在执行常量入栈操作之前,使用如下三种方式指明 常量的值:常量值隐式包含在操作码内部,常量值在字节码流中如同操作数一样紧随在操作码 之后,或者从常量池中取出常量。
一些操作码自行指明入栈的常量的类型和值,例如,iconst_1操作码告知Java虚拟机向栈压 人一个值为1的int类型数。Java虚拟机为经常压人栈的各种不同类型的数据定义了一些这样的操 作码。相对于从字节码流中取出操作数或者指向常量池的指令来说,上述这些指令都是冗余指 令,但它们更有效率。因为这些指令在字节码流中仅仅占据一个字节的空间,它们提高了字节 码的执行效率,并烕少了字节码的尺寸。向栈中压人int和float类型值的操作码如表10-1所示。
表10-1中所示的操作码把int和float类型压入桟,int和float都是一个字长的值。Java栈中的每 一个位置的长度都是一个字长(至少32位宽)。因此,每当一个int或者float类型被压人栈时,它都将占据一个位置。
表10-2中所示的操作码将long和double类型值压人找。long和double类型值是64位长度的值。 每当一个long或者double类型的值被压人栈时,它都将占据2个位置。
还有一个操作码能够将一个隐式声明的常量压入栈。如表10-3所示,aconst_null操作码将一 个空的对象引用类型压人栈。
如前所述,对象引用的格式取决于具体的Java虚拟机实现。一个对象引用可能会奠名其妙地 指向垃圾收集堆中的Java对象。一个空的对象引用表明一个当前还没有指向任何有效对象的对象 引用变量。给一f对象引用变量賦空值的过程中,将会使用aconst_nuli操作码。
如表10-4所示的两个操作码用来将整数常量压人栈,该整数常量的值在byte和short数据类型 的有效范围之内,这两个操作码通过使用一个紧随在操作码之后的操作数明确指定将要压入栈 的常量。紧随操作码的byte或者short类型在压人栈之前被扩展成int类型值。将int类型值压入栈 的操作实际上取代了将byte和short类型值压入桟的操作。
如表10-5所示的三个操作码从常量池中取出常量.然后将其压人栈。这些操作码使用表示常量池索引的操作数,Java虚拟机通过给定的索引査找相应的常量池入口,决定这些常量的类型和 值,并把它们压人栈。
常量池索引是一个无符号值,它在字节码流中直接跟随在操作码后面。ldc和ldc_w这两种操作码把一个字长的项压入入栈,该项或者是一个int类型、float类型的值,或者是一个String类型的对象引用。Idc和ldc_w之间的区别在于:由于ldc的索引只有一个字长,它只能指向常量池中1-255 (常量池位置0没有使用)范围内的位置。而ldc_w有两个字节长度的索引,因此,它能指向 任何包含long类型或者double类型(占据两个字节长度)的常量池位置。把从常量池取出的常量 压人找的操作码如表10-5所示。
Java源代码中所有的字符串文字最终都作为入口存储于常量池中。如果同一个应用程序的多个类都使用同样的字符串文字,那么此字符串文字将在使用它的所有类的class文件中出现。例 如,如果有三个类使用了字符串文字“Harumph!'那么这个字符串将分别在这三个class文件的 常量池中出现。这些类的方法可以使用Idc或者ldc_w指令来把指向一个有着“Harumph!”值的 String对象的引用压入栈。
如第8章所述,Java虚拟机把所有具有相同字符顺序的字符文字处理为同一个String对象。 换句话说,如果多个类使用了同一个字符串文字,比如“Harumph!”,Java虚拟机将只会创建一个具有“Harumph!”值的String对象来表示所有的字符串文字。
当虚拟机解析一个字符串文字的常量池入口时,它“拘留”这个字符串。首先,虚拟机检 查这个字符串中字符的顺序是否已经被拘留了。如果是这样,那么虚拟机就会使用与已拘留的 字符串同样的引用。否则,它将会创建一个新的String对象,把对这个新String对象的引用加入 到已拘留的字符串集合中去,然后,再把这个引用賦给新的已拘留的字符串。
10.2通用栈操作
尽管Java虚拟机指令集中的大多数指令都只处理一种特定的类型,但还是有一些指令可以进 行类型无关的找操作。如第5章所述,这些通用(无类型)指令不能用于分解两个字长的值。这 些指令如表10-6所示。
10.3把局部变量压入栈
有几个操作码用于把int类型和float类型局部变量压人栈。一些操作码隐式地指向一个通常 使用的局部变量位置。例如,iload_0把int类型局部变量读人位置0。而其他局部变量则被一个从紧随操作码后第一个字节位置读取局部变量索引的操作码压人栈。
把int类型和float类型局部变量压人栈的操作码如表10-7所示。
表10-8列出了将long类型和double类型的局部变量压人栈的指令。这些指令从栈帧的局部变 量段向操作数栈段移动了两个字长的数据。
把局部变量压人栈的最后的操作码组从栈帧的局部变量段向操作数段移动对象引用(占据 一个字长的空间)。这些操作码如表10-9所示
10.4弹出栈顶部元素,将其赋给局部变量
对于每个将局部变量压入栈的操作码而言,都存在相对应的弹出找顶部元素并将其存储到 局部变量中的操作码。执行弹出操作的操作码助记符可以通过把执行压人栈操作的操作码助记 符中的“save”改为“load”的方式来表示。表10-10列出了从操作数找顶部弹出int类型和fioat 类型值并将其存储到局部变量中的操作码。这些操作码从栈顶部向局部变量移动—个字长的值。
10.5 wide指令
无符号8位局部变量索引(比如iload指令后面的那个索引),把方法中局部变量数限制在256 以下。一条单独的wide的指令可以将8位的索引再扩展8位,这样就可以把对局部变量数的限制 扩展到65 536。wide操作码修改了其他的操作码。wide指令能够在诸如iload之类使用8位无符号局部变量索引的指令的前面执行。跟随在wide操作码和修改过的操作码之后的两个字节组成指向局部变量的16位无符号索引。
表10-13列出了所有能够被Wide指令修改的操作码(有两条例外)例外的两条指令(iinc和 ret)将在后面的章节中讨论。iinc指令和它的wide变量将在第12章讨论。ret指令和它的wide变量 将在第18章讨论。
当验证包含wide指令的字节码序列时,被wide指令修改的操作码看上去像—个传递wide的操作数。跳转指令并不允许直接跳转到被wide指令修改过的操作码。例如,如果一个字节流序列包含了下面的指令
wide iload 257
那么在这个方法的字节流序列中,不会允许其他任何操作码直接跳转到iload操作码的位置。 在这种情况下,iload操作码必须一直作为wide操作码的一个操作数来执行。
javac编译器把局部变量a从源文件中放到了栈帧中0和|位置的局部变暈中,把b放到了栈帧 中2和3的位置,把fiboNum放到了4和5的位置。每当计算出一个连续的斐波那契数,javac编译 器就会把这个数字存储到fiboNum变量中。因此,运行的时候会发现斐波那契序列以|ong类型值 的形式存放在4和5位置的局部变量中。
也许会注意到,long类型值分为两段存放,低位(0~31位)存放在第一个位置,髙位(32 ~63位)存放在第二个位置。例如,fiboNum变量的低位存放在位置4的局部变量中,髙位存放 在位置5的局部变量中。操作数桟中也一样,当一个long类型值被压入栈,低位将会首先被压入栈,接下来再轮到高位。
需要记住的是:这种在局部变量和操作数找中表示long类型值的方式只是对Java虚拟机的待 定实现的一个模拟。如第5章所述,规范并没有规定任何特定的方法来确定如何在栈帧中放置 long类型和double类型的两个字。