讀古今文學網 > Java程序員修煉之道 > 14.4 JVM的新方向 >

14.4 JVM的新方向

我們在第1章介紹了VMSpec(JVM規範)。這一文檔確切指明了作為JVM標準實現的VM必須遵守的行為準則。當引入新行為時(比如Java 7的invokedynamic),所有實現都必須升級以支持新功能。

在這一節裡,我們會談到那些已經在討論並有原型的各種修改最終實現的可能性。這項工作是在OpenJDK項目中開展的,該項目是Java參考實現的基礎,也是Oracle JDK的起點。除了對規範的可能修改,我們也會涉及對OpenJDK/Oracle JDK代碼的顯著改動。

14.4.1 VM的合併

在Oracle收購了Sun公司之後,它就擁有了兩款非常強的Java虛擬機:HotSpot VM(Sun帶的)和JRockit(之前收購的BEA帶的)。

Oracle很快就決定不再同時維護兩個VM來浪費資源,要把它們合併起來。HotSpot VM被選作基礎,JRockit特性會在將來發佈Java時謹慎地引進。

名稱有什麼關係?

這個合併後的VM沒有官方名稱,儘管VM粉和Java社區大部分都支持HotRockit這個名稱。它也確實挺吸引人,但還是要看Oracle的營銷部門同不同意。

所以這對咱開發人員來說,這有什麼關係呢?你現在用的VM(很可能是HotSpot VM)將來會增加很多新特性,包括(但不限於)下面這些:

  • 去掉PermGen,能防止一大類跟類加載有關的崩潰;

  • 加強JMX代理的支持,能讓你對運行的VM有更多深入的瞭解;

  • 新的JIT編譯方式,從JRockit中引入新的優化;

  • 任務控制,提供有助於對生產型應用進行調優和分析的先進工具。這些工具中有些可能是需要付費的外加JVM組件,不包含在免費下載的發佈包中。

去掉PermGen

就像6.5.2節說的,類的元數據當前保存在VM中一個的特殊內存區裡(PermGen)。它很快就會被填滿,特別是對於那些在運行時會創建大量類的非Java語言和框架而言。PermGen區不回收,耗光之後還會導致VM崩潰。有關人員正在開展工作,要把元數據保存在自有內存區中,讓噩夢一般的「java.lang.OutOfMemory-Error: PermGen space」消息永遠地成為過去。

還有很多的小改進全都是為了讓VM更小、更快、更靈活。假定HotSpot上大約已經投入了1000人年的工作量,跟投入工作量更多的JRockit結合起來形成的VM前景一定更加光明。

除了合併VM,還有大量的新特性正在製作中。其中之一就是可能會增加稱為協同程序的並發特性。

14.4.2 協同程序

Java和JVM語言程序員瞭解最多的並發形式就是多線程。它依靠JVM的線程調度服務在處理器核心上啟動和停止線程,但線程沒辦法控制這個調度。出於這一原因,多線程被稱為「搶佔式多任務」,因為調度器可以搶佔正在運行的線程,迫使它放棄對CPU的控制。

協同程序的基本思想是允許執行單元部分參與控制對它們的調度。具體來說,協同程序會像普通線程那樣運行,直到它遇到了一個「退位」指令。這會導致協同程序把自己掛起,並允許另一個協同程序繼續在它的地盤運行。當原來的協同程序再次得到機會運行時,它會從退位之後的下一條語句繼續向下執行,而不會從方法開始的地方。

因為這種多線程的方式靠正在運行的協同程序的相互協作,間或將運行機會退讓給其他協同程序,這種多線程處理被稱為「協作式多任務」。

關於協同程序如何工作的確切設計仍然處於熱烈討論的階段,沒有哪個是肯定要被採納的。一個可能的模型是在一個單例共享線程(或類似於java.util.concurrent裡的線程池)中創建和調度協同程序,如圖14-3所示。

圖14-3 一種可能的協同程序模型

正在執行協同程序的線程可能會被系統內的其他任何線程搶佔,但JVM線程調度器不能強迫協同程序退位。也就是說,以相信執行池中所有其他協同程序為代價,協同程序就可以控制什麼時候切換上下文。

這種控制意味著協同程序之間的同步可以做得更好。多線程代碼必須構建複雜的鎖策略來保護數據,但它很脆弱,因為上下文切換可能隨時都會發生。這是我們在4.1節討論的並發類型安全問題。相較而言,協同程序只要確保退位點數據的一致性,因為它知道其他任何時候自己都不會被搶佔。

這個折中的額外擔保是以相信其他線程為交換條件的,這是對某些線程編程問題的有益補充。一些非Java語言已經開始支持協同程序(或與之很貼近的概念」纖維「),特別是Ruby和較新版的JavaScript。在VM層面增加協同程序的支持(但不一定是對Java語言)會對可以使用協同程序的語言有很大幫助。

在可能會實現的VM修改中,最後要討論的是「元組」,這個VM特性提案對性能敏感的計算空間可能會產生很大的影響。

14.4.3 元組

在當今的JVM裡,所有數據項不是原始類型就是引用類型(可能是引用對像或數組)。比較複雜的類型只能在類裡定義,並傳遞對這些新類型實例對象的引用。這是一個簡單而又相當優雅的模型,過去一直為Java服務得很好。

但要構建高性能系統,這個模型就會暴露幾個缺陷。尤其是在遊戲和金融軟件這樣的應用中,遇到這個簡單模型局限性的情況十分常見。可以解決這個問題的辦法之一就是採用元組。

元組(tuple)有時稱為值對象,是能在原始類型和類之間架起橋樑的語言結構。像類一樣,用元組可以定義包含原始類型、引用類型和其他元組的自定義複雜類型。像原始類型一樣,在將它們傳遞給方法(或從方法中傳遞出來),保存在數組和其他對像中時,用的是整個值。如果你熟悉C(或.NET)環境,可以把它們看做結構(struct)的等價物。

我們來看一個例子:一個現有的Java API。

public class MyInputStream {
  public void write(byte, int off, int len);
}
  

這讓用戶可以將特定數量的數據寫到數組中的特定位置,很實用。但它設計得並不好。在理想的面向對像世界,偏移和長度應該被封裝在數組內,並且無論是用戶還是方法的實現者都應該不用再單獨跟蹤額外的信息。

實際上,在引入NIO時ByteBuffer就封裝了這些信息。可惜這不是白來的,從ByteBuffer中創建新切片需要分配一個新對象,這會給垃圾收集子系統造成壓力。儘管大多數垃圾收集器都非常擅長收集短命的對象,但在吞吐率非常高的延遲敏感環境中,這種分配操作會累加並最終導致應用出現令人無法接受的暫停。

如果我們能定義一個保存數組引用、偏移和長度的值對像(也就是元組)類型Slice會發生什麼呢?在代碼清單14-2中,我們會用新的tuple關鍵字來表示這個新概念。

代碼清單14-2 作為元組的數組切片

public tuple Slice {
  private int offset;
  private int length;
  private byte array;
  public byte get(int i) {
    return array[offset + i];
  }
}
  

這個切片的構造結合了原始類型和引用類型的很多優勢:

  • Slice值可以複製到方法中,也可以從方法中複製出來,就跟手工傳遞數組的引用和int值一樣有效;
  • Slice元組在退出方法後會被清理掉(因為它們跟值類型一樣);
  • 對偏移和長度的處理會乾淨地封裝在元組中。

在日常編程中有很多類型會從元組的使用中受益,比如帶有分子和分母的有理數、帶有實部和虛部的複數,或者由ID和領域標識引用的用戶主檔(獻給那些MMORPG迷們)。

在處理數組時元組也能對性能有所提升。現在的數組中要放同質的數據值集合——要麼是原始類型,要麼是引用類型。在使用數組時,元組允許我們對內存的佈局做更多的控制。

來看一個例子:一個以原始類型long為鍵的簡單散列表。

public class MyHashTable {
  private Entry entries;
}

public class Entry {
  private long key;
  private Object value;
}
  

在當前的JVM化身中,entries數組中只能放Entry實例的引用。調用者每次查找表中的key,在用傳入的值與相關Entry實例的key比較之前,必須把Entry實例解引用。

當用元組實現時,則可以在數組內展開Entry類型,因此能夠省掉訪問key產生的解引用開銷。圖14-4展示了當前情況,以及使用元組後得到的改善。

圖14-4  JVM數組與元組

在考察元組的數組時,採用元組得到性能優勢的關鍵之處也變得更加清晰。我們在第6章討論過,大多數應用程序代碼的性能都是由一級緩存的命中率決定的。在圖14-4中,如果使用元組,掃瞄散列表的代碼效率會更高。它不用再承擔額外的緩存讀取就能得到key值。這就是元組取得性能優勢的本質——程序員在展開內存的數據時可以得到更優的空間局部性。1

1 空間局部性(spatial locality),如果程序訪問某個存儲器地址後,又在較短時間內訪問臨近的存儲器地址,則程序具有良好的空間局部性。兩次訪問的地址越接近,空間局部性越好。——譯者注

對可能出現在Java和JDK 8中的新特性,我們的討論就到此為止了。其中有多少能變成現實也只能等到快要發佈時才知道。如果你對特性的演進感興趣,可以加入OpenJDK項目和Java Community Process,參加這些特性的開發活動。如果你對它們還不熟悉,請找到這些項目並看看如何加入。