国产日韩欧美一区二区三区三州_亚洲少妇熟女av_久久久久亚洲av国产精品_波多野结衣网站一区二区_亚洲欧美色片在线91_国产亚洲精品精品国产优播av_日本一区二区三区波多野结衣 _久久国产av不卡

?

提高Java程序動態(tài)性的一個新途徑

2015-10-21 18:12嚴(yán)忠林
計算機(jī)時代 2015年9期

嚴(yán)忠林

摘 要: 為支持Groovy、JRuby等新的動態(tài)類型語言,JDK1.7在Java虛擬機(jī)上特意引入了新的動態(tài)調(diào)用指令。文章提出將其應(yīng)用于Java程序,在生成的Java類文件中用它替換某些成員訪問指令,由此可以突破Java原本固有的運(yùn)行模式,引入滿足應(yīng)用需要的新運(yùn)行機(jī)制,使程序更簡單、靈活,提高開發(fā)效率。同時分析了原有成員訪問指令的局限,討論了新的動態(tài)調(diào)用指令的優(yōu)勢,給出了指令替換的實(shí)現(xiàn)方法。

關(guān)鍵詞: 動態(tài)調(diào)用指令; 成員訪問; Java虛擬機(jī); Java類文件

中圖分類號:TP311 文獻(xiàn)標(biāo)志碼:A 文章編號:1006-8228(2015)09-01-03

New approach to improve dynamic of Java programs

Yan Zhonglin

(College of information, mechanical and electrical engineering, Shanghai Normal University, Shanghai 200234, China)

Abstract: Since JDK1.7, the invokedynamic instruction has been introduced to JVM to support dynamically typed languages such as Groovy, JRuby. This paper proposes replacing some appropriate member access instructions with it in a Java class file, which may break Java inherent operation mode, introduce a new mechanism that meets the application needs, make the program more simple and flexible, and improve development efficiency. At the same time, the limitations of the original member access instruction and the advantages of the invokedynamic instruction is analyzed, and the method to realize the instruction replacement is given.

Key words: invokedynamic instruction; member access; JVM; Java class file

0 引言

自JDK1.7起,為了方便在JVM上實(shí)現(xiàn)像JRuby、Jython、Groovy、Clojure那樣的動態(tài)類型語言,在虛擬機(jī)層面引入了動態(tài)類型語言支持(JSR-292)[1]。與Java這樣的靜態(tài)類型語言不同,動態(tài)類型語言的變量、方法不需要事先聲明,直到運(yùn)行時,才根據(jù)當(dāng)時數(shù)據(jù)的實(shí)際類型決定所要進(jìn)行的操作。例如對表達(dá)式a+b,要一直到運(yùn)行至此處時,才根據(jù)a、b當(dāng)時的數(shù)值,決定是做整數(shù)加、浮點(diǎn)數(shù)加,還是字符串加。因此對于這類語言,計算機(jī)所執(zhí)行的代碼不是在編譯時,而要到運(yùn)行時才確定。

所謂動態(tài)性是指這種能根據(jù)運(yùn)行時狀態(tài),自主決定實(shí)際操作的能力。Java本是靜態(tài)類型語言,大多數(shù)操作都不能在運(yùn)行時改變,與此相適應(yīng)的JVM指令也因此很難滿足動態(tài)類型語言的要求。為改變這種狀況,JSR-292引入了新的動態(tài)調(diào)用指令invokedynamic,它執(zhí)行的操作取決于內(nèi)含的方法句柄(MethodHandle)。MethodHandle是新添加的java.lang.invoke包中的類,是對類內(nèi)各種成員的引用[2]。使用它,動態(tài)類型語言就能按照自己的規(guī)則,根據(jù)運(yùn)行時參數(shù)和其他狀態(tài),實(shí)現(xiàn)希望的操作。

新指令原本是為動態(tài)語言而設(shè)計的,但我們嘗試將它用于Java程序,替換某些成員訪問指令。這樣可以改變Java一些固有的行為模式,引入希望的運(yùn)行機(jī)制,給軟件開發(fā)帶來便利。這種替換不要求改變運(yùn)行時方法棧中的參數(shù)和返回值,能保持Java原有的語法習(xí)慣和可理解性。

1 成員訪問指令的特性

JVM原有的數(shù)據(jù)訪問指令有g(shù)etfield/putfield(獲得/修改實(shí)例變量),getstatic/putstatic(獲得/修改類變量)。方法調(diào)用有invokestatic(調(diào)用類方法),invokespecial(調(diào)用可在編譯時綁定的實(shí)例方法,如構(gòu)造、private、super方法等),invokevirtual(調(diào)用動態(tài)綁定的實(shí)例方法),invokeinterface(調(diào)用接口定義的方法)。Java程序中所有成員訪問都是通過這8條指令實(shí)現(xiàn)的[3]。它們是專為Java設(shè)計的,有固定的處理流程,有良好的執(zhí)行效率,多數(shù)情況下能滿足要求。但由于缺乏靈活性,在某些場合,也會出現(xiàn)不便使用,導(dǎo)致程序復(fù)雜化的情形。

這些指令除最后兩條外,所做的操作在運(yùn)行前就已確定,不能在運(yùn)行時動態(tài)改變。以圖1的處理點(diǎn)(Point)、線(Line)的代碼為例,Line對象用k、b記錄直線的斜率和y軸截距,但對垂直于x軸的直線,這倆數(shù)據(jù)都無意義,還需構(gòu)建子類VLine,添加新的b字段,記錄x軸的截距(此處僅為說明問題,并非倡導(dǎo)此種設(shè)計。繼承關(guān)系也極簡單,實(shí)際上只有在相當(dāng)規(guī)模和復(fù)雜度的系統(tǒng)中,此處討論的缺乏動態(tài)性帶來的不便才會突顯出來)。語句①本來是想獲得數(shù)組中各直線的有意義的截距進(jìn)行處理,但實(shí)際上卻只能獲得y軸的截距。傳統(tǒng)上解決此類問題的途徑是在相關(guān)類中編寫大量get/set方法,這將使程序臃腫龐大,把一個簡單操作復(fù)雜化了。

方法調(diào)用也有類似問題,用super可調(diào)用父類方法,但和this不同,它是靜態(tài)的。圖1中的語句②會根據(jù)this對象的不同執(zhí)行不同的f0,但語法上極其類似的語句③永遠(yuǎn)只會執(zhí)行GElem類中的f0,如果設(shè)計者的真實(shí)想法是調(diào)用每個對象自己的父類中的方法,恐怕又得添加很多輔助代碼[4]。

即使是采用動態(tài)綁定的最后兩條指令(invokevirtual和invokeinterface),其動態(tài)性也是有限的。它只能根據(jù)方法的第一個參數(shù)(即this引用)的不同作出選擇。當(dāng)處理方式不取決于單個對象,而必須考慮所有參與處理的對象時,就會帶來困難。例如,為求兩直線交點(diǎn),在Line和VLine中分別重載實(shí)現(xiàn)了針對不同類型直線的crossPoint方法。main()中語句④想求出數(shù)組內(nèi)任意兩條直線間的交點(diǎn),但它是錯誤的。要完成此任務(wù),必須先用if語句作類型判斷,再選擇合適的方法體。這顯然增加了程序復(fù)雜性,更重要的是這種“硬編碼”會破壞系統(tǒng)的可擴(kuò)展性,如果再要添加一個子類,將不得不修改所有相關(guān)的if語句,對于大系統(tǒng),這絕不是一件輕松的工作。

2 動態(tài)調(diào)用指令的優(yōu)勢

上述問題都是由于對應(yīng)的成員訪問指令只能按照固定模式處理,缺乏動態(tài)性造成的。如使用新的invokedynamic指令替代,就能引入新的執(zhí)行模式,讓其在運(yùn)行時根據(jù)實(shí)際參數(shù)動態(tài)選擇合適的數(shù)據(jù)、方法,這些問題就可在bytecode層面解決了,上層的Java源代碼則不必做任何改變。由于新指令中的方法句柄是自定義的,因此可根據(jù)需要實(shí)現(xiàn)多種特定機(jī)制,運(yùn)行時不但可做類型判斷,還可進(jìn)行各種添加、變換。比如變量賦值前進(jìn)行合法性校驗(yàn),調(diào)用關(guān)鍵操作時作日志記錄,將對某個方法的調(diào)用轉(zhuǎn)為對其他方法的調(diào)用等等,面向方面編程(AOP)所需的在方法調(diào)用前后“編織”的橫切操作,都可以在這兒實(shí)現(xiàn)。

這種通過使用invokedynamic指令引入新機(jī)制、獲得動態(tài)性的方法,實(shí)際上把系統(tǒng)實(shí)現(xiàn)分成了上、下兩層,上層是和具體事務(wù)相關(guān)的業(yè)務(wù)邏輯,下層是根據(jù)參數(shù)和其他狀態(tài)進(jìn)行選擇、變換、處理的機(jī)制實(shí)現(xiàn)。只要設(shè)計合理,兩者可以清晰劃分。動態(tài)機(jī)制在上層看來,是自動實(shí)現(xiàn)的,可直接使用,編程時只需專注于業(yè)務(wù)處理。而下層實(shí)現(xiàn),比如根據(jù)參數(shù)類型選擇重載方法等,也和上層絕少關(guān)聯(lián)。這對軟件的開發(fā)、維護(hù)無疑都是極其有利的。

在過去,要在程序中實(shí)現(xiàn)各種動態(tài)機(jī)制,大多要用到“反射”。它有一套特殊的API,通常難以將處理過程隱藏于無形,會增加程序的復(fù)雜性。更重要的是它還會影響程序的執(zhí)行效率,眾所周知,現(xiàn)代Java程序的高效運(yùn)行是通過JIT、Hotspot等技術(shù)將bytecode轉(zhuǎn)為本地碼,并采用大量積極的優(yōu)化措施取得的。而“反射”機(jī)制不在bytecode層面實(shí)現(xiàn),無法使用這些手段,因而其運(yùn)行是低效的。

與“反射”相比,invokedynamic指令更有優(yōu)勢。動態(tài)實(shí)現(xiàn)都隱藏于最基本的訪問指令中,上層代碼就是普通的Java程序,直接表達(dá)事務(wù)的處理過程,清晰自然。它還便于系統(tǒng)的修改、切換,例如在試驗(yàn)階段,底層可添加檢查、校驗(yàn)等功能,到成品階段能方便地撤除。在A環(huán)境下的一些操作,到B環(huán)境下可用另一些方法替換。這些修改只發(fā)生在下層,上層源代碼不需改變。

從運(yùn)行效率上來看,它比“反射”也更有利。JSR-292的設(shè)計目標(biāo)就是要讓動態(tài)類型語言能在JVM上高速運(yùn)行,所有MethodHandle都直接在虛擬機(jī)層面執(zhí)行。為保證高效率,甚至于在其他訪問指令中每次都要執(zhí)行的權(quán)限檢查操作,也被挪到該對象初始構(gòu)造時進(jìn)行,執(zhí)行時沒有性能消耗。JIT、Hotspot等技術(shù)帶來的豐富的優(yōu)化措施,也可充分利用。所以使用它可以獲得很高的執(zhí)行效率。

3 指令替換的實(shí)現(xiàn)

JSR-292是為新型語言而設(shè)計,沒有打算用于Java。所以通過Java編譯器不可能生成invokedynamic指令,我們只能在編譯獲得的類文件中自行完成需要的替換。好在Java程序各個類分開存儲,有定義明確的格式和語義(圖2),較易于理解和處理。

類文件的常量池含有代碼中使用的所有常量和標(biāo)識符,以及各種類型表達(dá)。類本身、各數(shù)據(jù)、方法的細(xì)節(jié)信息都通過對常量池的索引獲得描述。一個類和其他類的關(guān)聯(lián)、對其他類的訪問也基于常量池中的符號引用進(jìn)行。bytecode代碼出現(xiàn)于對應(yīng)方法的Code屬性中,在我們關(guān)注的成員訪問指令中有編譯時確定的常量池索引,指出了它所訪問的數(shù)據(jù)或方法。

要進(jìn)行替換,首先要找到這些指令,為此定義了標(biāo)注DynReplace(圖3)。其中Instruction是成員訪問指令的枚舉,用于說明查找指令的種類,refClass、refName、refType是對被訪問成員所在類、名字和類型的描述,構(gòu)成對它的符號引用。bsmClass、bsmMethod指出替換后新指令所需代碼所在的類和方法名。這是一個可重復(fù)標(biāo)注,可同時說明需要替換的多條指令。它們用于啟動類前,處理程序?qū)膯宇愰_始,遍歷相關(guān)類文件,進(jìn)行搜尋處理。

[public @interface DynReplace { Instruction instr();

String refClass(); String refName(); String refType();

String bsmClass(); String bsmMethod();

}]

圖3 DynReplace標(biāo)注

每條invokedynamic指令都對應(yīng)一個CallSite對象,它含有指令執(zhí)行所需的方法句柄。但指令首次執(zhí)行時,該對象不存在,需執(zhí)行一個特殊的自舉方法(Bootstrap Method),生成此對象。JSR-292為此在類文件中引入新結(jié)構(gòu),在類屬性池中加入了BootstrapMethods屬性,這是一個自舉方法句柄數(shù)組,通過常量池索引指出各條invokedynamic指令的自舉方法。常量池中也引入三種新類型常量,MethodHandle_info(句柄的類型和對應(yīng)方法的符號引用),MethodType_info(句柄的參數(shù)、返回值描述),InvokeDynamic_info(被invokedynamic指令直接使用,指出它的自舉方法句柄索引和調(diào)用的方法名及類型參數(shù))。

每條替換用的invokedynamic指令都需要以下代碼,這些代碼應(yīng)出現(xiàn)在上述標(biāo)注的bsmClass字段指定的類中。作為舉例,圖4是對圖1中語句④的調(diào)用指令進(jìn)行替換,使它能正確執(zhí)行所需提供的類代碼。

[public class Handle { static HashMap map=

new HashMap();

private static void putMHs() {

Class<?> c0= Point.class, c1= Line.class, c2= VLine.class, ... ...

map.put(methodType(c1, c2), //將各方法句柄按對應(yīng)參數(shù)放入map中

lookup().findVirtual(c1, "crossPoint", methodType(c0, c2)));

public static Point crossPoint(Line la, Line lb) {

MethodHandle mh=map.get(methodType(la.getClass(),lb.getClass()));

return (Point) mh.invoke(la,lb);

public static CallSite bsm(Lookup caller, String name, MethodType type) {

putMHs();

MethodHandle mh=caller.findStatic(Handle.class, name, type);

return new ConstantCallSite(mh);

圖4 示例代碼2

⑴ 代替原來的成員訪問而實(shí)現(xiàn)希望的動態(tài)機(jī)制的方法。它可以參照參數(shù)和其他狀態(tài)選擇執(zhí)行代碼,也可插入“橫切”操作。它為static方法,參數(shù)應(yīng)與原指令在方法棧中的參數(shù)相同。圖4中的crossPoint()即為此方法,它用兩個參數(shù)的實(shí)際類型構(gòu)造MethodType對象,以此為key在散列表map中獲取方法句柄加以執(zhí)行。使用散列表的原因是為了避免在參數(shù)個數(shù)較多,繼承關(guān)系復(fù)雜的情況下做過多的if判斷,以保證執(zhí)行效率。

⑵ 可選的初始化方法。如果動態(tài)處理方法需要初始化操作,應(yīng)提供該方法,它將在invokedynamic指令首次執(zhí)行時,在其自舉方法中被調(diào)用。圖4中的putMHs()即為該方法,它生成需要的map對象,為crossPoint()調(diào)用時參數(shù)的各種可能組合準(zhǔn)備好正確的方法句柄。這里以MethodType為key,可防止生成過多的對象。MethodType是為描述方法句柄返回值和參數(shù)引入的不可變對象,它的methodType方法對相同的類型只生成一個對象,以后可一直復(fù)用。這樣就避免了每次調(diào)用crossPoint()時產(chǎn)生新對象,保證了運(yùn)行效率。

⑶ invokedynamic指令首次執(zhí)行需要的自舉方法,該方法有固定參數(shù),僅執(zhí)行一次。它應(yīng)該用上述的動態(tài)處理方法句柄構(gòu)造CallSite對象并返回,如有初始化方法也應(yīng)先執(zhí)行之。圖4中的bsm即是該方法。其方法名應(yīng)出現(xiàn)在標(biāo)注的bsmMethod字段,這樣執(zhí)行指令替換的代碼能將其置入新指令中。

執(zhí)行指令替換的代碼要對類文件做下列操作。實(shí)現(xiàn)這些操作需要熟悉Java類文件的結(jié)構(gòu)。筆者在學(xué)習(xí)過程中制作了一個解析和處理程序,但讀者不必一切從頭開始,完全可以使用已有的ASM[5]等開源工具來完成。

⑷ 生成正確的invokedynamic指令。該指令內(nèi)有指向常量池中InvokeDynamic_info的索引,通過它可在BootstrapMethods屬性中獲得對應(yīng)的自舉方法句柄,該句柄也在常量池中用MethodHandle_info描述。因此在常量池和類屬性池中需要正確添加這些元素。

⑸ 進(jìn)行指令替換。在方法代碼中將原成員訪問指令替換為對應(yīng)的invokedynamic指令。由于該指令長度是5byte,而舊指令長度有3和5兩種,故替換時,可能要將后續(xù)代碼下移。這又會引起代碼長度、轉(zhuǎn)移偏移量以及諸如局部變量、StackMap等聲明的有效范圍的改變,替換時對這些元素也要同時修正。

這五個步驟只有前兩步驟是應(yīng)該公開的,其他都是與處理業(yè)務(wù)無關(guān)的執(zhí)行指令替換的細(xì)節(jié),完全可以封裝起來,一般程序員不必了解,不會影響他們的開發(fā)工作。

4 結(jié)束語

用invokedynamic指令替代JVM中原來的各成員訪問指令,其內(nèi)部的處理規(guī)則就不再像舊指令那樣固定不變,可靈活制定。實(shí)現(xiàn)的操作能在運(yùn)行時由計算機(jī)根據(jù)實(shí)際情況自行選擇、變換,更具動態(tài)性。能高效地實(shí)現(xiàn)原來采用“反射”才能完成的功能。雖然它的實(shí)現(xiàn)需要對Java虛擬機(jī)有較深的理解,但這僅是對個別核心開發(fā)人員的要求,一般程序員只會感覺編程更方便。和原來的成員訪問指令相比,在性能上它也可能有一點(diǎn)損失,但在軟件規(guī)模及復(fù)雜性日漸增長的今天,提高開發(fā)人員的工作效率無疑是最重要的,這里介紹的做法正好符合這種要求。

參考文獻(xiàn):

[1] John Rose. JSR-292 Supporting Dynamically Typed Languages

on the Java Platform (FinalRelease)[EB/OL].https://jcp.org/aboutJava/communityprocess/final/jsr292/

[2] Oracle co. Java Platform, Standard Edition 8 API Specification

[EB/OL].http:// docs.oracle.com/javase/8/docs/api/

[3] Tim Lindholm, Frank Yellin. The Java Virtual Machine Specifica-

tion Java SE 8 Edition[M] New Jersey Addison-Wesley Professional,2015.2.

[4] 周志明.深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第2版)[M].

機(jī)械工業(yè)出版社,2013.

[5] Eric Bruneton. ASM 4.0 A Java bytecode engineering library [EB/OL].

http://download.forge.objectweb.org/asm/asm4-guide.pdf