用 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 宏和準引號減少樣板代碼