尝试一下Scala JVM字节码

本文概述

在过去的几年中, 由于Scala语言将功能和面向对象的软件开发原理完美地结合在一起, 并且在经过验证的Java虚拟机(JVM)之上进行了实现, 因此它一直在继续流行。

尽管Scala可以编译为Java字节码, 但它旨在改善Java语言的许多缺点。提供完整的功能编程支持, Scala的核心语法包含许多隐式结构, 这些结构必须由Java程序员显式构建, 其中有些涉及相当大的复杂性。

看一下为Scala供电的机器内部。

创建一种可编译为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并重新编译, 则将删除字段的最终修饰符, 并添加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
}

如果在类主体中定义了任何val或var, 则将创建相应的私有字段和访问器方法, 并在创建实例时对其进行适当的初始化。

请注意, 这样的类级别val和var字段的实现意味着, 如果在类级别使用某些变量来存储中间值, 并且程序员从未直接访问它们, 则每个此类字段的初始化将为方法添加一到两个方法类足迹。为此类字段添加私有修饰符并不意味着相应的访问器将被删除。他们将变为私有。

变量和函数定义

假设我们有一个方法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, 该指令分配的结果为此方法将通过常量池中的索引#22引用到字段m1。第二次调用后是相同的模式, 这次将值分配给字段m2, 在常量池中的索引#24处索引。

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

但是, 使用关键字def会得到不同的结果。方法m3()除了包含要返回的字段值之外, 还包括指令invokevirtual#30。也就是说, 每次调用此方法时, 它都会随后调用m()并返回此方法的结果。

因此, 正如我们所看到的, Scala提供了三种使用类字段的方法, 可以通过关键字val, var和def轻松指定这些方法。在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类具有一个输出字段, 类型为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方法的实际实现。要查看其字节码, 请确保根据命令外壳程序的规则正确地转义$, 以避免将其解释为特殊字符:

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

该方法创建一个打印机。然后, 它创建一个Hello $$ anonfun $ 1, 其中包含我们的匿名函数s => println(s)。使用该对象作为输出字段初始化打印机。然后将该字段加载到堆栈上, 并使用操作数” 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变量一样被对待。它存储在类字段输出中, 并创建getter output()。唯一的区别是此变量现在必须实现Scala接口scala.Function1(AbstractFunction1可以实现)。

因此, 这种优雅的Scala功能的代价是底层实用程序类, 这些实用程序类的创建是为了表示和执行一个可用作值的匿名函数。你应考虑此类功能的数量以及VM实现的详细信息, 以了解其对特定应用程序的意义。

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

鸣叫

Scala特质

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来调用随附类提供的静态方法。

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

  • 确定此调用假定的确切特征。
  • 确定提供为trait定义的静态方法字节码的伴随类的名称。
  • 产生必要的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方法。在这种情况下, 这只是一种方法。如果单例具有更多字段, 它将具有更多的吸气剂以及可变字段的设置器。

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

总结

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

我们现在也拥有自己探索语言的工具。 Scala语法有许多有用的功能, 本文没有介绍, 例如案例类, currying和列表推导。我鼓励你自己研究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的对象引用压入操作数堆栈。实际上, 这几乎始终是对当前类的引用。
  • 这将为下一个调用invokevirtual#31设置堆栈, 该调用执行实例方法numSides()。 invokevirtual将顶部操作数(对此的引用)弹出堆栈, 以标识它必须从哪个类调用该方法。方法返回后, 其结果将被压入堆栈。
  • 在这种情况下, 返回的值(numSides)为整数格式。必须将其转换为双浮点格式, 才能与另一个double值相乘。 i2d指令将整数值弹出堆栈, 将其转换为浮点格式, 然后将其压回堆栈。
  • 此时, 堆栈在顶部包含this.numSides的浮点结果, 然后是传递给方法的sideLength参数的值。 dmul从堆栈中弹出前两个值, 对它们执行浮点乘法, 然后将结果压入堆栈。

调用方法时, 将在其操作的堆栈框架中创建一个新的操作数堆栈。我们在这里必须谨慎使用术语:”堆栈”一词可能是指调用堆栈, 为方法执行提供上下文的框架堆栈, 或者是JVM指令在其上操作的特定框架的操作数堆栈。

局部变量

每个堆栈帧都有一个局部变量表。这通常包括对此对象的引用, 在调用方法时传递的所有参数以及在方法主体内声明的任何局部变量。使用-v选项运行javap将包含有关如何设置每种方法的堆栈框架的信息, 包括其局部变量表:

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

在此示例中, 有两个局部变量。插槽0中的变量名为this, 类型为RegularPolygon。这是对该方法自己的类的引用。插槽1中的变量命名为sideLength, 类型为D(表示双精度)。这是传递给我们的getPerimeter()方法的参数。

诸如iload_1, fstore_2或aload [n]之类的指令在操作数堆栈和局部变量表之间传输不同类型的局部变量。由于表中的第一项通常是对此的引用, 因此指令aload_0常见于在对其自己的类进行操作的任何方法中。

至此, 我们结束了JVM基础知识的演练。单击此处返回主要文章。

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?