java掌握_掌握Java 11的Constantdynamic
java掌握
為了使JVM對動態語言更具吸引力,該平臺的第七版已將invokedynamic引入了其指令集。 Java開發人員通常不會注意到此功能,因為該功能已隱藏在Java字節碼中。 簡而言之,通過使用invokedynamic ,可以將方法調用的綁定延遲到第一次調用之前。 例如,Java語言使用此技術來實現lambda表達式,這些表達式僅在首次使用時才需要出現。 這樣做, invokedynamic已經發展成為一種基本的語言功能,我在先前的博客文章中對此進行了詳細介紹 。 使用constantdynamic ,Java 11引入了類似的機制,只是它延遲了常量值的創建。 這篇文章描述了此功能的目的和內部工作原理,并展示了如何使用Byte Buddy庫生成使用此新指令的代碼。
Java中的常量值是什么?
在Java 5之前,Java程序中的常量值只能是字符串或原始類型。 這些常量作為文字內置在語言中,甚至由javac編譯器假定以減小類文件的大小。 例如,在以下代碼段中,從不實際讀取only字段的值,而是在編譯期間將其復制到其使用站點:
class ConstantSample {final String field = “foo”;void hello() {System.out.print(field);} }代替讀取hello方法中的字段,生成的字節碼將包含對常量foo的直接引用。 實際上,上述類絕不會嘗試讀取該字段的值,可以通過使用Java反射對其進行更改來驗證該字段的值,此后調用hello仍會打印foo 。
為了表示這樣的常量值,任何Java類文件都包含一個常量池,可以將其視為寫出存在于類范圍內的任何常量值的表。 這意味著在方法中使用或用作字段值的常量,但也包含描述類的其他不可變信息,例如類名或被調用方法的名稱以及它們的聲明類型名。 一旦在類的常量池中記錄了值,就可以通過指向常量池中特定條目的偏移量來引用值。 這樣做,在整個類中重復的值僅需要存儲一次,因為偏移量當然可以多次引用。
因此,當在上述源代碼中讀取該字段時, javac發出一個字節碼,該字節碼引用常量池中foo值的偏移量,而不是發出對該字段的讀取指令。 可以將字段聲明為final,因為javac會忽略反射值更改的邊緣情況。 通過發出讀取常量的指令,與讀取字段的指令相比, javac還節省了一些字節。 這就是使這種優化有利可圖的原因,特別是因為在任何Java類中字符串和數字值都相當普遍。 較小的類文件可幫助Java運行時更快地加載類,而顯式的常量性概念可幫助JVM的JIT和AOT編譯器應用進一步的優化。
所描述的針對相同常數的偏移量的重用還隱含了重用值的標識。 由于用單個實例表示相等的字符串值,因此以下語句在Java中將聲明為true:
assert “foo” == “foo”;在幕后,foo的兩個值都指向定義類的常量池中的相同常量池偏移量。 此外,JVM甚至可以通過遍歷在常量池中找到的字符串來跨類對常量字符串進行重復數據刪除。
恒定池存儲的局限性
類文件的常量池中值的這種表格表示形式非常適合簡單值,例如字符串和數字基元。 但是同時,當javac沒有發現恒定的值時,它可能會帶來非直觀的后果。 例如,在以下類中, hello方法中未將唯一字段的值視為常量:
class NoConstantSample {final String field = “foo”.toString();void hello() {System.out.print(field);} }盡管toString方法對于字符串而言是微不足道的,但是這種情況對于不評估Java方法的javac仍然未知。 因此,編譯器不能再發出恒定的池值作為print語句的輸入。 相反,它必須發出該字段的字段讀取指令,如前所述,該指令需要其他字節。 這次,如果通過使用反射更改了字段的值,則調用hello也將打印更新的值。
當然,這個例子是人為的。 但是不難想象,在實踐中如何將經典方法限制為Java中的常量。 例如,想象一個定義為Math.max(CONST_A, CONST_B)的整數值。 當然,兩個編譯時常數的最大值本身就是常數。 但是,由于javac無法評估Java方法,因此派生值不是作為常量發現的,而只能在運行時進行計算。
在類文件的常量池中聲明常量值的另一個問題是它對簡單值的限制。 字符串和數值的表示當然很簡單,但是比傳統方法更復雜的Java對象需要更大的靈活性。 為了支持其他常量,Java類文件格式已經在Java 5中添加了類文字常量,其中諸如String.class類的值將不再被編譯為對Class.forName("java.lang.String")的調用,而是一個常量。包含類引用的池條目。 Java 7發行版還在類文件規范中添加了新的常量池類型,以允許MethodType和MethodHandle實例的常量表示。
與字符串,類和原始值相反,Java編程語言不提供用于創建這些常量的文字。 相反,在javac需要有效表示方式的情況下,添加了此類常量的可能性以更好地支持invokedynamic指令。 本質上,lambda表達式由lambda的表達式類型簽名MethodType以及對其實現的引用MethodHandle 。 如果必須為每個對lambda表達式的調用將兩個值都創建為顯式,非恒定參數,則使用此類表達式的性能開銷肯定會超過其收益。
盡管此解決方案減輕了一些中間的麻煩,但它暗示著對Java的未來不滿意,無法添加其他常量類型。 常量池條目的類型由單個字節編碼,這嚴重限制了類文件中可能的常量類型的總數。 另一個麻煩是,對類文件格式的更改要求對處理類文件的任何工具進行級聯調整,這使得希望使用更通用的方法來表達常量值。 通過引入constantdynamic ,Java虛擬機最終將在即將發布的Java 11版本中支持這種機制。
引入動態常數
動態常量不是通過處理文字表達式來創建的,而是通過調用產生該常量值作為結果的所謂的引導方法來創建的。 這與通過在運行時調用引導程序方法綁定綁定方法調用站點的invokedynamic指令非常相似,在運行時,將返回指向動態綁定調用站點的目標實現的指針。 作為主要區別,自舉常量是不可變的,而動態綁定的方法調用可以在以后重定向到另一個實現。
從本質上講,引導程序只不過是Java方法,這些方法對其簽名有一些要求。 作為第一個參數,任何引導方法都將接收由JVM自動提供的MethodHandles.Lookup實例。 通過此類查找,可以使用類的特定實例表示的類的特權進行訪問。 例如,當從任何類調用MethodHandles.lookup()時,對調用者敏感的方法將返回一個實例,例如,該實例允許讀取調用類的私有字段,而對于從另一個內部創建的查找實例而言,這是不可能的類。 在使用bootstrap方法的情況下,查找表示在創建時定義動態常量的類,而不是在聲明boostrap方法的類。 這樣做,引導方法可以訪問相同的信息,就像從常量定義類本身內部創建常量一樣。 bootstrap方法作為第二個參數接收常量的名稱,作為第三個參數,它接收常量的預期類型。 引導程序方法必須是靜態的,或者是構造函數,其中構造的值表示常量。
在許多情況下,實現自舉方法不需要這三個參數,但是它們的存在允許實現更通用的自舉機制,從而有助于允許重用自舉方法來創建多個常量。 如果需要,在聲明引導方法時也可以省略最后兩個參數。 但是,需要將MethodHandles.Lookup類型聲明為第一個參數。 這樣做是為了允許將來在第一個參數用作標記類型的情況下進一步允許調用模式。 這是與invokedynamic的另一個區別,它允許省略第一個參數。
有了這些知識,我們現在可以表示兩個常量的先前最大值,該常量之前已提到為派生常量。 該值是通過以下引導方法微不足道地計算的:
public class Bootstrapper {public static int bootstrap(MethodHandles.Lookup lookup, String name, Class type) {return Math.max(CONST_A, CONST_B);} }由于作為第一個參數的查找實例具有定義該常量的類的特權,因此即使使用引導程序方法通常看不到這些值,也可以通過使用此查找來獲取CONST_A和CONST_B的值。 ,例如因為它們是私有的。 該類的javadoc詳細解釋了需要使用什么API來定位字段并讀取其值。
為了創建動態常量,必須在類的常量池中引用引導程序方法作為動態常量類型的條目。 到目前為止,Java語言無法創建此類條目,據我所知,目前也沒有其他語言在使用這種機制。 因此,我們將在本文后面探討使用代碼生成庫Byte Buddy創建此類。 但是,在暗示注釋中有常量池值的Java偽代碼中,動態常量及其引導方法將被稱為:
class DynamicConstant {// constant pool #1 = 10// constant pool #2 = 20// constant pool #3 = constantdyamic:Bootstrapper.bootstrap/maximum/int.classfinal int CONST_A = [constant #1], CONST_B = [constant #2];void hello() {System.out.print([constant #3]);} }首次執行hello方法后,JVM將通過調用Bootstrapper.bootstrap方法來解析指定的常量,該方法的最大值為常量名,而int.class為創建的常量的請求類型。 在從bootstrap方法接收到結果之后,JVM將用該結果替換對常量的任何引用,并且不再再次調用bootstrap方法。 如果在多個位置引用了動態常數,這也將是正確的。
避免自定義引導方法
在大多數情況下,創建動態常量不需要實現單獨的引導方法。 為了涵蓋大多數用例,JVM綁定的類java.lang.invoke.ConstantBootstraps已經實現了幾種通用的引導方法,可用于創建大多數常量。 作為核心,類的invoke方法允許通過提供方法引用作為常量值的工廠來定義常量。 為了使這種通用方法有效,引導程序方法可以接收任意數量的附加參數,這些參數本身必須是恒定值。 然后,在描述動態常量的條目時,將這些參數作為對其他常量池條目的引用。
這樣做,可以通過提供Math.max方法的句柄以及CONST_A和CONST_B的兩個常量值作為附加參數來計算上述最大值。 然后, ConstantBootstraps中的invoke方法的實現將使用這兩個值來調用Math.max并返回結果,其中bootstrap方法大致實現如下:
class ConstantBootstraps {static Object invoke(MethodHandles.Lookup lookup, String name, Class type,MethodHandle handle, Object[] arguments) throws Throwable {return handle.invokeWithArguments(arguments);} }當將其他參數提供給引導方法時,它們將按其順序分配給每個其他方法參數。 為了允許更靈活的引導程序方法(例如上面的invoke方法),最后一個參數也可以是Object數組類型,以接收任何多余的參數,在這種情況下為兩個整數值。 如果引導程序方法不接受提供的參數,則JVM將不會調用引導程序方法,而是在失敗的常量解析期間引發BootstrapMethodError 。
使用這種方法,使用ConstantBootstraps.invoke的偽代碼將不再需要單獨的引導程序方法,而是看起來像下面的偽代碼:
class AlternativeDynamicConstant {// constant pool #1 = 10// constant pool #2 = 20// constant pool #3 = MethodHandle:Math.max(int,int)// constant pool #4 = constantdyamic:ConstantBootstraps.invoke/maximum/int.class/#3,#1,#2final int CONST_A = [constant #1], CONST_B = [constant #2];void hello() {System.out.print([constant #4]);} }嵌套動態常數
如前所述,引導方法的參數必須是其他常量池條目。 由于動態常量存儲在常量池中,因此可以嵌套動態常量,這使此功能更加靈活。 這帶有直觀的限制,即動態常量的初始化不得包含圓圈。 例如,如果解決了Qux值,將從頂部到底部調用以下引導程序方法:
static Foo boostrapFoo(MethodHandles.Lookup lookup, String name, Class type) {return new Foo(); }static Bar boostrapBar(MethodHandles.Lookup lookup, String name, Class type, Foo foo) {return new Bar(foo); }static Qux boostrapQux(MethodHandles.Lookup lookup, String name, Class type, Bar bar) {return new Qux(bar); }當需要JVM解析Qux的動態常量時,它將首先解析Bar這將再次觸發Foo的先前初始化,因為每個值都取決于前一個。
當表達靜態常量池條目類型(例如空引用)不支持的值時,也可能需要嵌套動態常量。 在Java 11之前,空值只能表示為字節碼指令,而不能表示為常量池值,其中字節碼均未暗示null的類型。 為了克服此限制, java.lang.invoke.ConstantBootstraps提供了幾種便捷的方法,例如nullValue ,該方法允許將鍵入的null值引導為動態常量。 然后可以將此null值作為參數提供給另一個引導程序方法,該方法希望將null作為參數。 同樣,不可能在常量池中表示只能表示引用類型的原始類型文字,例如int.class 。 相反, javac int.class例如int.class轉換為對靜態Integer.TYPE字段的讀取,該字段在啟動時通過對JVM的本地調用來解析其int.class值。 同樣, ConstantBootstraps提供了primitiveType引導程序方法,可以輕松地將這些值表示為動態常量。
為什么要關心常數值?
以上所有內容聽起來都像是一種技術訣竅,除了靜態字段已經提供的功能外,對Java平臺的添加并不多。 但是,動態常數的潛力很大,但尚未開發。 作為最明顯的用例,動態常量可用于正確實現惰性值。 惰性值通常用于僅在使用時按需表示昂貴的對象。 從今天開始,惰性值通常是通過使用所謂的雙重檢查鎖定來實現的 ,這種模式例如由scalac編譯器為其lazy關鍵字實現:
class LazyValue {volatile ExpensiveValue value;void get() {T value = this.value;if (value == null) {synchronized (this) {value = this.value;if (value == null) {value = new ExpensiveValue();}}}return value;} }盡管值一旦初始化就永遠不會改變,但上述構造需要在每次讀取時都進行易失性讀取。 這意味著不必要的開銷,可以通過將惰性值表示為僅在曾經使用過時才進行引導的動態常量來避免該開銷。 尤其是在Java核心庫中,這對于延遲許多從未使用過的值的初始化很有用,例如在Locale類中,盡管大多數JVM僅使用運行中的機器標準語言,但Locale類卻為任何受支持的語言初始化了值。 通過避免初始化這些多余的值,JVM可以更快地啟動,并避免將內存用于無效值。
另一個重要的用例是使用常量表達式優化編譯器。 不難想象,為什么編譯器比可變值更喜歡處理常量。 例如,如果編譯器可以合并兩個常量,則此合并的結果可以永久替換先前的值。 如果原始值會隨時間變化,那么這當然是不可能的。 盡管即時編譯器可能仍認為可變值在運行時實際上是恒定的,但提前編譯器則依賴于某些明確的恒定性概念。 通過確保引導程序方法無副作用,例如,將來的Java版本可以對其編譯時進行評估,其中常量動態可以用作輕量級宏機制,以擴大使用Graal用Java編寫的本機映像的范圍。
我會使用此功能嗎?
在Java 7中引入invokedynamic時,從Java語言的角度來看,這個新的字節碼功能尚未使用。 但是,從Java 8開始,可以在大多數類文件中找到作為lambda表達式的實現的invokedynamic指令。 同樣,Java 11尚未使用恒定動力學功能,但是可以預料將來會有所改變。
在對暴露constantdynamic幾個潛在的API,已經討論了最新JVMLS(這樣也會使通過API invokedynamic訪問)。 這對于庫作者來說特別有用,它允許他們更好地解析關鍵執行路徑,但也可以釋放一些潛力來改善javac的常量檢測,例如擴大非捕獲lambda表達式的范圍,在這種情況下,字段或變量訪問可能是如果在編譯過程中發現常量,則通過讀取常量來代替。 最后,這種新機制為將來的語言增強提供了潛力,例如一個惰性關鍵字,它避免了替代JVM語言中當前等效項的開銷。
常量動態功能對于經常需要使用其他信息來增強現有類的Java代理也很有用。 Java代理通常無法通過添加靜態字段來更改類,因為這會干擾基于反射的框架,并且由于在重新定義已加載的類時大多數JVM禁止更改類格式。 但是,這兩種限制都不適用于在運行時期間添加的動態常量,在動態常量中,Java代理現在可以輕松地用附加信息標記類。
使用字節好友創建動態常量
盡管缺少對constantdynamic的語言支持,但是版本11的JVM已經完全能夠處理包含動態常量的類文件。 使用字節代碼生成庫Byte Buddy,我們可以創建此類文件并將其加載到JVM的早期訪問版本中 。
在Byte Buddy中,動態常量由JavaConstant.Dynamic的實例表示。 為了方便起見,Byte Buddy為工廠提供了由java.lang.invoke.ConstantBoostraps類聲明的任何引導方法,例如前面討論的invoke方法。
舉一個簡單的例子,下面的代碼創建Callable的子類,并將call方法的返回值定義為示例類的動態常量。 為了引導常量,我們將Sample的構造函數提供給上述的invoke方法:
public class Sample {public static void main(String[] args) throws Throwable {Constructor<? extends Callable<?>> loaded = new ByteBuddy().subclass(Callable.class).method(ElementMatchers.named("call")).intercept(FixedValue.value(JavaConstant.Dynamic.ofInvocation(Sample.class.getConstructor()))).make().load(Sample.class.getClassLoader()).getLoaded().getConstructor();Callable<?> first = loaded.newInstance(), second = loaded.newInstance();System.out.println("Callable instances created");System.out.println(first.call() == second.call());}public Sample() { System.out.println("Sample instance created"); } }如果運行代碼,請注意如何僅創建一個Sample實例,如本文所述。 還要注意如何僅在首次調用call方法時以及在創建Callable實例之后才懶惰地創建實例。
要運行上面的代碼,您當前必須使用-Dnet.bytebuddy.experimental=true運行Byte Buddy才能解鎖對此功能的支持。 Java 11最終確定并準備發布時,情況將發生變化,其中Byte Buddy 1.9.0將是第一個立即支持Java 11的版本。 另外,在處理動態常量時,最新的Byte Buddy版本中仍然存在一些粗糙的地方。 因此,最好從master分支構建Byte Buddy或使用JitPack 。 要查找有關Byte Buddy的更多信息,請訪問bytebuddy.net 。
翻譯自: https://www.javacodegeeks.com/2018/08/hands-on-java-constantdynamic.html
java掌握
總結
以上是生活随笔為你收集整理的java掌握_掌握Java 11的Constantdynamic的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux火狐浏览器打不开(linux
- 下一篇: java 和javafx_Java,Ja