用 Scala JVM 字节码弄脏你的手

已发表: 2022-03-11

Scala 语言在过去几年中继续流行起来,这要归功于它出色地结合了函数式和面向对象的软件开发原则,以及它在经过验证的 Java 虚拟机 (JVM) 之上的实现。

尽管 Scala 编译为 Java 字节码,但它旨在改进 Java 语言的许多已知缺点。 Scala 的核心语法提供完整的函数式编程支持,包含许多必须由 Java 程序员显式构建的隐式结构,其中一些结构相当复杂。

创建一种编译为 Java 字节码的语言需要深入了解 Java 虚拟机的内部工作原理。 要了解 Scala 的开发人员所取得的成就,有必要深入了解,探索编译器如何解释 Scala 的源代码以生成高效且有效的 JVM 字节码。

让我们看看所有这些东西是如何实现的。

先决条件

阅读本文需要对 Java 虚拟机字节码有一些基本的了解。 完整的虚拟机规范可以从 Oracle 的官方文档中获得。 阅读整个规范对于理解本文并不重要,因此,为了快速了解基础知识,我在文章底部准备了一份简短指南。

单击此处阅读有关 JVM 基础知识的速成课程。

需要一个实用程序来反汇编 Java 字节码以重现下面提供的示例,并继续进行进一步调查。 Java 开发工具包提供了它自己的命令行实用程序javap ,我们将在这里使用它。 javap如何工作的快速演示包含在底部的指南中。

当然,对于想要学习示例的读者来说,安装 Scala 编译器是必要的。 本文是使用 Scala 2.11.7 编写的。 不同版本的 Scala 可能会产生略有不同的字节码。

默认的 Getter 和 Setter

尽管 Java 约定总是为公共属性提供 getter 和 setter 方法,但 Java 程序员必须自己编写这些方法,尽管事实上它们的模式几十年来都没有改变。 相比之下,Scala 提供了默认的 getter 和 setter。

让我们看下面的例子:

 class Person(val name:String) { }

让我们看一下Person类的内部。 如果我们用scalac编译这个文件,那么运行$ javap -p Person.class会给我们:

 Compiled from "Person.scala" public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

我们可以看到,对于 Scala 类中的每个字段,都会生成一个字段及其 getter 方法。 该字段是私有的和最终的,而方法是公共的。

如果我们在Person源代码中将val替换为var并重新编译,那么字段的final修饰符将被删除,并且 setter 方法也会被添加:

 Compiled from "Person.scala" public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

如果在类体内定义了任何valvar ,则创建相应的私有字段和访问器方法,并在创建实例时适当地初始化。

请注意,类级别valvar字段的这种实现意味着,如果在类级别使用某些变量来存储中间值,并且程序员从不直接访问,则每个此类字段的初始化都会向班级足迹。 为此类字段添加private修饰符并不意味着将删除相应的访问器。 他们将成为私人的。

变量和函数定义

假设我们有一个方法m() ,并为这个函数创建三个不同的 Scala 风格的引用:

 class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

这些对m的引用是如何构造的? m在每种情况下何时被执行? 让我们看一下生成的字节码。 以下输出显示了javap -v Person.class的结果(省略了很多多余的输出):

 Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object."<init>":()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

在常量池中,我们看到对方法m()的引用存储在索引#30中。 在构造函数代码中,我们看到这个方法在初始化期间被调用了两次,指令invokevirtual #30首先出现在字节偏移量11处,然后出现在偏移量19处。第一次调用之后是指令putfield #22 ,它分配结果此方法适用于字段m1 ,由常量池中的索引#22引用。 第二次调用之后是相同的模式,这次将值分配给字段m2 ,在常量池中索引为#24

换句话说,将方法分配给使用valvar定义的变量只会将方法的结果分配给该变量。 我们可以看到创建的方法m1()m2()只是这些变量的 getter。 在var m2的情况下,我们还看到创建了 setter m2_$eq(int) ,它的行为就像任何其他 setter 一样,覆盖了字段中的值。

但是,使用关键字def会产生不同的结果。 方法m3()还包括指令invokevirtual #30 ,而不是获取要返回的字段值。 也就是说,每次调用此方法时,它都会调用m() ,并返回此方法的结果。

因此,正如我们所见,Scala 提供了三种使用类字段的方法,并且可以通过关键字valvardef轻松指定这些方法。 在 Java 中,我们必须显式地实现必要的 setter 和 getter,而这种手动编写的样板代码的表达能力要差得多,而且更容易出错。

懒惰的价值观

声明惰性值时会产生更复杂的代码。 假设我们已将以下字段添加到先前定义的类中:

 lazy val m4 = m

运行javap -p -v Person.class现在将显示以下内容:

 Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

在这种情况下,直到需要时才计算字段m4的值。 生成特殊的私有方法m4$lzycompute()来计算惰性值,并生成字段bitmap$0来跟踪其状态。 方法m4()检查该字段的值是否为 0,表示m4尚未初始化,在这种情况下调用m4$lzycompute() ,填充m4并返回其值。 此私有方法还将bitmap$0的值设置为 1,以便下次调用m4()时,它将跳过调用初始化方法,而是简单地返回m4的值。

第一次调用 Scala 惰性值的结果。

Scala 在这里生成的字节码被设计为线程安全和有效的。 为了线程安全,惰性计算方法使用monitorenter / monitorexit指令对。 该方法仍然有效,因为此同步的性能开销仅发生在第一次读取惰性值时。

只需要一位来指示惰性值的状态。 因此,如果惰性值不超过 32 个,则单个 int 字段可以跟踪它们。 如果源代码中定义了多个惰性值,则编译器将修改上述字节码以实现位掩码。

同样,Scala 允许我们轻松利用必须在 Java 中显式实现的特定类型的行为,从而节省工作量并降低拼写错误的风险。

作为价值的功能

现在让我们看一下下面的 Scala 源代码:

 class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output("Hello"); } }

Printer类有一个字段output ,类型为String => Unit :一个接受String并返回Unit类型对象的函数(类似于 Java 中的void )。 在 main 方法中,我们创建这些对象之一,并将该字段分配给打印给定字符串的匿名函数。

编译这段代码会生成四个类文件:

源代码被编译成四个类文件。

Hello.class是一个包装类,其主要方法只是调用Hello$.main()

 public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

隐藏的Hello$.class包含 main 方法的真正实现。 要查看它的字节码,请确保根据命令 shell 的规则正确转义$ ,以避免将其解释为特殊字符:

 public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1."<init>":()V 11: invokespecial #22 // Method Printer."<init>":(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string "Hello" 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

该方法创建一个Printer 。 然后它创建一个Hello$$anonfun$1 ,其中包含我们的匿名函数s => println(s) 。 使用此对象作为output字段初始化Printer 。 然后将该字段加载到堆栈中,并使用操作数"Hello"执行。

让我们看看下面的匿名函数类Hello$$anonfun$1.class 。 我们可以看到它通过实现apply()方法扩展了 Scala 的Function1 (作为AbstractFunction1 )。 实际上,它创建了两个apply()方法,一个包装另一个,它们一起执行类型检查(在这种情况下,输入是String ),并执行匿名函数(使用println()打印输入)。

 public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1<java.lang.String, scala.runtime.BoxedUnit> implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

回顾上面的Hello$.main()方法,我们可以看到,在偏移量 21 处,匿名函数的执行是由调用其apply( Object )方法触发的。

最后,为了完整起见,让我们看一下Printer.class的字节码:

 public class Printer // ... // field private final scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output; // field getter public scala.Function1<java.lang.String, scala.runtime.BoxedUnit> output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1<java.lang.String, scala.runtime.BoxedUnit>); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object."<init>":()V 9: return

我们可以看到这里的匿名函数被视为与任何val变量一样。 它存储在类字段output中,并创建了 getter output() 。 唯一的区别是这个变量现在必须实现 Scala 接口scala.Function1AbstractFunction1实现了)。

所以,这个优雅的 Scala 特性的代价是底层的实用程序类,它被创建来表示和执行一个可以用作值的匿名函数。 您应该考虑此类功能的数量以及 VM 实现的细节,以了解它对您的特定应用程序意味着什么。

深入了解 Scala:探索如何在 JVM 字节码中实现这种强大的语言。
鸣叫

斯卡拉特征

Scala 的特征类似于 Java 中的接口。 以下特征定义了两个方法签名,并提供了第二个的默认实现。 让我们看看它是如何实现的:

 trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) } 

源代码被编译成两个类文件。

生成了两个实体: Similarity.class ,声明这两种方法的接口,以及合成类Similarity$class.class ,提供默认实现:

 public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); }
 public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

当一个类实现这个特征并调用方法isNotSimilar时,Scala 编译器会生成字节码指令invokestatic来调用伴随类提供的静态方法。

可以从特征创建复杂的多态性和继承结构。 例如,多个 trait 以及实现类都可能覆盖具有相同签名的方法,调用super.methodName()将控制权传递给下一个 trait。 当 Scala 编译器遇到此类调用时,它会:

  • 确定此调用假定的确切特征。
  • 确定提供为特征定义的静态方法字节码的随附类的名称。
  • 产生必要的invokestatic指令。

因此我们可以看到,强大的特征概念是在 JVM 级别以不会导致显着开销的方式实现的,Scala 程序员可以享受这个特性,而不必担心它在运行时会太昂贵。

单身人士

Scala 使用关键字object提供了单例类的显式定义。 让我们考虑以下单例类:

 object Config { val home_dir = "/home/user" }

编译器生成两个类文件:

源代码被编译成两个类文件。

Config.class是一个非常简单的:

 public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

这只是嵌入单例功能的合成Config$类的装饰器。 使用javap -p -c检查该类会产生以下字节码:

 public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method "<init>":()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object."<init>":()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value "/home/user" and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

它由以下部分组成:

  • 合成变量MODULE$ ,其他对象通过它访问这个单例对象。
  • 静态初始化器{} (也称为<clinit> ,类初始化器)和私有方法Config$ ,用于初始化MODULE$并将其字段设置为默认值
  • 静态字段home_dir的 getter 方法。 在这种情况下,它只是一种方法。 如果单例有更多字段,它将有更多的 getter,以及可变字段的 setter。

单例是一种流行且有用的设计模式。 Java 语言不提供在语言级别指定它的直接方法; 相反,开发人员有责任在 Java 源代码中实现它。 另一方面,Scala 提供了一种使用object关键字显式声明单例的清晰便捷的方法。 正如我们所看到的,它是以一种经济实惠且自然的方式实现的。

结论

我们现在已经看到 Scala 如何将几个隐式和函数式编程特性编译成复杂的 Java 字节码结构。 通过对 Scala 内部工作原理的了解,我们可以更深入地了解 Scala 的强大功能,帮助我们充分利用这种强大的语言。

我们现在还拥有自己探索语言的工具。 Scala 语法有许多有用的特性在本文中没有涉及,例如案例类、柯里化和列表推导。 我鼓励您自己研究 Scala 对这些结构的实现,这样您就可以学习如何成为下一个级别的 Scala 忍者!


Java 虚拟机:速成课程

就像 Java 编译器一样,Scala 编译器将源代码转换为.class文件,其中包含要由 Java 虚拟机执行的 Java 字节码。 为了了解这两种语言在底层的不同之处,有必要了解它们都针对的系统。 在这里,我们简要概述了 Java 虚拟机体系结构、类文件结构和汇编程序基础的一些主要元素。

请注意,本指南仅涵盖与上述文章一起启用的最低要求。 尽管这里没有讨论 JVM 的许多主要组件,但完整的详细信息可以在官方文档中找到,这里。

使用javap反编译类文件
常量池
字段和方法表
JVM 字节码
方法调用和调用堆栈
在操作数栈上执行
局部变量
返回顶部

使用javap反编译类文件

Java 附带javap命令行实用程序,它将.class文件反编译为人类可读的形式。 由于 Scala 和 Java 类文件都针对同一个 JVM,因此javap可用于检查 Scala 编译的类文件。

让我们编译以下源代码:

 // RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( "Calculating perimeter..." ) return sideLength * this.numSides } }

scalac RegularPolygon.scala将产生RegularPolygon.class 。 如果我们然后运行javap RegularPolygon.class我们将看到以下内容:

 $ javap RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

这是一个非常简单的类文件分解,它简单地显示了类的公共成员的名称和类型。 添加-p选项将包括私有成员:

 $ javap -p RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

这仍然不是很多信息。 要查看这些方法是如何在 Java 字节码中实现的,让我们添加-c选项:

 $ javap -p -c RegularPolygon.class Compiled from "RegularPolygon.scala" public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object."<init>":()V 9: return }

这更有趣一点。 然而,要真正了解整个故事,我们应该使用-v-verbose选项,如javap -p -v RegularPolygon.class

Java 类文件的完整内容。

在这里,我们终于看到了类文件中的真正内容。 这是什么意思呢? 让我们来看看一些最重要的部分。

常量池

C++ 应用程序的开发周期包括编译和链接阶段。 Java 的开发周期跳过了显式链接阶段,因为链接发生在运行时。 类文件必须支持此运行时链接。 这意味着当源代码引用任何字段或方法时,生成的字节码必须以符号形式保存相关引用,一旦应用程序加载到内存中就可以取消引用,并且运行时链接器可以解析实际地址。 此符号形式必须包含:

  • 班级名称
  • 字段或方法名称
  • 类型信息

类文件格式规范包括一个称为常量池的文件部分,它是链接器所需的所有引用的表。 它包含不同类型的条目。

 // ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

每个条目的第一个字节是一个数字标签,指示条目的类型。 其余字节提供有关条目值的信息。 字节数及其解释规则取决于第一个字节指示的类型。

例如,使用常量整数365的 Java 类可能具有带有以下字节码的常量池条目:

 x03 00 00 01 6D

第一个字节x03标识条目类型CONSTANT_Integer 。 这会通知链接器接下来的四个字节包含整数的值。 (请注意,十六进制的 365 是x16D )。 如果这是常量池中的第 14 个条目, javap -v将呈现如下:

 #14 = Integer 365

许多常量类型由对常量池中其他地方的更多“原始”常量类型的引用组成。 例如,我们的示例代码包含以下语句:

 println( "Calculating perimeter..." )

字符串常量的使用将在常量池中产生两个条目:一个为CONSTANT_String类型的条目,另一个为CONSTANT_Utf8类型的条目。 Constant_UTF8类型的条目包含字符串值的实际 UTF8 表示。 CONSTANT_String类型的条目包含对CONSTANT_Utf8条目的引用:

 #24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

这种复杂性是必要的,因为还有其他类型的常量池条目引用Utf8类型的条目,而不是String类型的条目。 例如,对类属性的任何引用都会产生一个CONSTANT_Fieldref类型,其中包含对类名、属性名和属性类型的一系列引用:

 #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

有关常量池的更多详细信息,请参阅 JVM 文档。

字段和方法表

类文件包含一个字段表,该表包含有关类中定义的每个字段(即属性)的信息。 这些是对描述字段名称和类型以及访问控制标志和其他相关数据的常量池条目的引用。

类文件中存在类似的方法表。 但是,除了名称和类型信息之外,对于每个非抽象方法,它还包含要由 JVM 执行的实际字节码指令,以及方法的堆栈帧使用的数据结构,如下所述。

JVM 字节码

JVM 使用自己的内部指令集来执行编译后的代码。 使用-c选项运行javap会在输出中包含已编译的方法实现。 如果我们以这种方式检查RegularPolygon.class文件,我们将看到getPerimeter()方法的以下输出:

 public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

实际的字节码可能如下所示:

 xB2 00 17 x12 19 xB6 00 1D x27 ...

每条指令都以标识 JVM 指令的一字节操作码开头,然后是零个或多个要操作的指令操作数,具体取决于特定指令的格式。 这些通常是常量值,或者是对常量池的引用。 javap有助于将字节码转换为人类可读的形式,显示:

  • 代码中指令的第一个字节的偏移量或位置。
  • 指令的人类可读名称或助记符
  • 操作数的值(如果有)。

用井号显示的操作数,例如#23 ,是对常量池中条目的引用。 正如我们所看到的, javap还在输出中产生有用的注释,确定从池中引用的确切内容。

我们将在下面讨论一些常见的说明。 有关完整 JVM 指令集的详细信息,请参阅文档。

方法调用和调用堆栈

每个方法调用都必须能够在其自己的上下文中运行,其中包括诸如本地声明的变量或传递给该方法的参数之类的内容。 这些一起构成了一个堆栈帧。 在调用方法时,会创建一个新框架并将其放置在调用堆栈的顶部。 当方法返回时,当前帧从调用堆栈中移除并丢弃,并恢复调用方法之前有效的帧。

堆栈帧包括几个不同的结构。 两个重要的是操作数栈局部变量表,接下来讨论。

JVM 调用堆栈。

在操作数栈上执行

许多 JVM 指令在其帧的操作数堆栈上运行。 这些指令不是在字节码中明确指定一个常量操作数,而是将操作数堆栈顶部的值作为输入。 通常,这些值会在进程中从堆栈中删除。 一些指令还将新值放在堆栈顶部。 通过这种方式,可以组合 JVM 指令来执行复杂的操作。 例如,表达式:

 sideLength * this.numSides

在我们的getPerimeter()方法中编译为以下内容:

 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 

JVM 指令可以对操作数栈进行操作以执行复杂的功能。

  • 第一条指令dload_1将对象引用从局部变量表(接下来讨论)的插槽 1 推送到操作数堆栈上。 在这种情况下,这是方法参数sideLength 。- 下一条指令aload_0将局部变量表的插槽 0 处的对象引用推入操作数堆栈。 实际上,这几乎总是对当前类this的引用。
  • 这将为下一次调用设置堆栈,invokevirtual invokevirtual #31执行实例方法numSides()invokevirtual将顶部操作数(对this的引用)从堆栈中弹出,以识别它必须从哪个类调用该方法。 一旦方法返回,它的结果就会被压入堆栈。
  • 在这种情况下,返回的值 ( numSides ) 是整数格式。 它必须转换为双精度浮点格式才能与另一个双精度值相乘。 i2d指令将整数值从堆栈中弹出,将其转换为浮点格式,然后将其推回堆栈。
  • 此时,堆栈顶部包含this.numSides的浮点结果,后面是传递给该方法的sideLength参数的值。 dmul从堆栈中弹出这两个值,对它们执行浮点乘法,并将结果压入堆栈。

调用方法时,将创建一个新的操作数堆栈作为其堆栈帧的一部分,将在其中执行操作。 我们必须注意这里的术语:“堆栈”一词可能指调用堆栈,即为方法执行提供上下文的帧堆栈,或者是指特定帧的操作数堆栈,JVM 指令在该堆栈上运行。

局部变量

每个堆栈帧都有一个局部变量表。 This typically includes a reference to this object, any arguments that were passed when the method was called, and any local variables declared within the method body. Running javap with the -v option will include information about how each method's stack frame should be set up, including its local variable table:

 public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

In this example, there are two local variables. The variable in slot 0 is named this , with the type RegularPolygon . This is the reference to the method's own class. The variable in slot 1 is named sideLength , with the type D (indicating a double). This is the argument that is passed to our getPerimeter() method.

Instructions such as iload_1 , fstore_2 , or aload [n] , transfer different types of local variables between the operand stack and the local variable table. Since the first item in the table is usually the reference to this , the instruction aload_0 is commonly seen in any method that operates on its own class.

This concludes our walkthrough of JVM basics.

相关:使用 Scala 宏和准引号减少样板代码