- 深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)
- 周志明
- 6422字
- 2020-01-03 17:41:40
1.5 展望Java技術的未來
本書第1、2版中的“展望Java技術的未來”分別成文于2011年和2013年,將近十年時間已經過去,當時暢想的Java新發展新變化全部如約而至,這部分內容已不再有“展望”的價值。筆者在更新第3版時重寫了本節全部內容,并把第2版的“展望”的原文挪到附錄之中。倘若Java的未來依舊燦爛精彩,倘若下一個十年本書還更新第4、第5版,亦希望屆時能在附錄中回首今日,去回溯哪些預測成為現實,哪些改進中途夭折。
如1.3節結尾所言,今天的Java正處于機遇與挑戰并存的時期,Java未來能否繼續壯大發展,某種程度上取決于如何應對當下已出現的挑戰,本文將按照這個脈絡來組織,向讀者介紹現在仍處于Oracle Labs中的Graal VM、Valhalla、Amber、Loom、Panama等面向未來的研究項目。
1.5.1 無語言傾向
網上每隔一段時間就能見到幾條“未來X語言將會取代Java”的新聞,此處“X”可以用Kotlin、Golang、Dart、JavaScript、Python等各種編程語言來代入。這大概就是長期占據編程語言榜單第一位的煩惱,天下第一總避免不了挑戰者相伴。
如果Java有擬人化的思維,它應該從來沒有懼怕過被哪一門語言所取代,Java“天下第一”的底氣不在于語法多么先進好用,而是來自它龐大的用戶群和極其成熟的軟件生態,這在朝夕之間難以撼動。不過,既然有那么多新、舊編程語言的興起躁動,說明必然有其需求動力所在,譬如互聯網之于JavaScript、人工智能之于Python,微服務風潮之于Golang等。大家都清楚不太可能有哪門語言能在每一個領域都盡占優勢,Java已是距離這個目標最接近的選項,但若“天下第一”還要百尺竿頭更進一步的話,似乎就只能忘掉Java語言本身,踏入無招勝有招的境界。
2018年4月,Oracle Labs新公開了一項黑科技:Graal VM,如圖1-4所示,從它的口號“Run Programs Faster Anywhere”就能感覺到一顆蓬勃的野心,這句話顯然是與1995年Java剛誕生時的“Write Once,Run Anywhere”在遙相呼應。
Graal VM被官方稱為“Universal VM”和“Polyglot VM”,這是一個在HotSpot虛擬機基礎上增強而成的跨語言全棧虛擬機,可以作為“任何語言”的運行平臺使用,這里“任何語言”包括了Java、Scala、Groovy、Kotlin等基于Java虛擬機之上的語言,還包括了C、C++、Rust等基于LLVM的語言,同時支持其他像JavaScript、Ruby、Python和R語言等。Graal VM可以無額外開銷地混合使用這些編程語言,支持不同語言中混用對方的接口和對象,也能夠支持這些語言使用已經編寫好的本地庫文件。

圖1-4 Graal VM
Graal VM的基本工作原理是將這些語言的源代碼(例如JavaScript)或源代碼編譯后的中間格式(例如LLVM字節碼)通過解釋器轉換為能被Graal VM接受的中間表示(Intermediate Representation,IR),譬如設計一個解釋器專門對LLVM輸出的字節碼進行轉換來支持C和C++語言,這個過程稱為程序特化(Specialized,也常被稱為Partial Evaluation)。Graal VM提供了Truffle工具集來快速構建面向一種新語言的解釋器,并用它構建了一個稱為Sulong的高性能LLVM字節碼解釋器。
從更嚴格的角度來看,Graal VM才是真正意義上與物理計算機相對應的高級語言虛擬機,理由是它與物理硬件的指令集一樣,做到了只與機器特性相關而不與某種高級語言特性相關。Oracle Labs的研究總監Thomas Wuerthinger在接受InfoQ采訪時談到:“隨著GraalVM 1.0的發布,我們已經證明了擁有高性能的多語言虛擬機是可能的,并且實現這個目標的最佳方式不是通過類似Java虛擬機和微軟CLR那樣帶有語言特性的字節碼。”對于一些本來就不以速度見長的語言運行環境,由于Graal VM本身能夠對輸入的中間表示進行自動優化,在運行時還能進行即時編譯優化,因此使用Graal VM實現往往能夠獲得比原生編譯器更優秀的執行效率,譬如Graal.js要優于Node.js
,Graal.Python要優于CPtyhon
,TruffleRuby要優于Ruby MRI,FastR要優于R語言等。
對Java而言,Graal VM本來就是在HotSpot基礎上誕生的,天生就可作為一套完整的符合Java SE 8標準的Java虛擬機來使用。它和標準的HotSpot的差異主要在即時編譯器上,其執行效率、編譯質量目前與標準版的HotSpot相比也是互有勝負。但現在Oracle Labs和美國大學里面的研究院所做的最新即時編譯技術的研究全部都遷移至基于Graal VM之上進行了,其發展潛力令人期待。如果Java語言或者HotSpot虛擬機真的有被取代的一天,那從現在看來Graal VM是希望最大的一個候選項,這場革命很可能會在Java使用者沒有明顯感覺的情況下悄然而來,Java世界所有的軟件生態都沒有發生絲毫變化,但天下第一的位置已經悄然更迭。
1.5.2 新一代即時編譯器
對需要長時間運行的應用來說,由于經過充分預熱,熱點代碼會被HotSpot的探測機制準確定位捕獲,并將其編譯為物理硬件可直接執行的機器碼,在這類應用中Java的運行效率很大程度上取決于即時編譯器所輸出的代碼質量。
HotSpot虛擬機中含有兩個即時編譯器,分別是編譯耗時短但輸出代碼優化程度較低的客戶端編譯器(簡稱為C1)以及編譯耗時長但輸出代碼優化質量也更高的服務端編譯器(簡稱為C2),通常它們會在分層編譯機制下與解釋器互相配合來共同構成HotSpot虛擬機的執行子系統(這部分具體內容將在本書第11章展開講解)。
自JDK 10起,HotSpot中又加入了一個全新的即時編譯器:Graal編譯器,看名字就可以聯想到它是來自于前一節提到的Graal VM。Graal編譯器是以C2編譯器替代者的身份登場的。C2的歷史已經非常長了,可以追溯到Cliff Click大神讀博士期間的作品,這個由C++寫成的編譯器盡管目前依然效果拔群,但已經復雜到連Cliff Click本人都不愿意繼續維護的程度。而Graal編譯器本身就是由Java語言寫成,實現時又刻意與C2采用了同一種名為“Sea-of-Nodes”的高級中間表示(High IR)形式,使其能夠更容易借鑒C2的優點。Graal編譯器比C2編譯器晚了足足二十年面世,有著極其充沛的后發優勢,在保持輸出相近質量的編譯代碼的同時,開發效率和擴展性上都要顯著優于C2編譯器,這決定了C2編譯器中優秀的代碼優化技術可以輕易地移植到Graal編譯器上,但是反過來Graal編譯器中行之有效的優化在C2編譯器里實現起來則異常艱難。這種情況下,Graal的編譯效果短短幾年間迅速追平了C2,甚至某些測試項中開始逐漸反超C2編譯器。Graal能夠做比C2更加復雜的優化,如“部分逃逸分析”(Partial Escape Analysis),也擁有比C2更容易使用激進預測性優化(Aggressive Speculative Optimization)的策略,支持自定義的預測性假設等。
今天的Graal編譯器尚且年幼,還未經過足夠多的實踐驗證,所以仍然帶著“實驗狀態”的標簽,需要用開關參數去激活,這讓筆者不禁聯想起JDK 1.3時代,HotSpot虛擬機剛剛橫空出世時的場景,同樣也是需要用開關激活,也是作為Classic虛擬機的替代品的一段歷史。
Graal編譯器未來的前途可期,作為Java虛擬機執行代碼的最新引擎,它的持續改進,會同時為HotSpot與Graal VM注入更快更強的驅動力。
1.5.3 向Native邁進
對不需要長時間運行的,或者小型化的應用而言,Java(而不是指Java ME)天生就帶有一些劣勢,這里并不只是指跑個HelloWorld也需要百多兆的JRE之類的問題,更重要的是指近幾年在從大型單體應用架構向小型微服務應用架構發展的技術潮流下,Java表現出來的不適應。
在微服務架構的視角下,應用拆分后,單個微服務很可能就不再需要面對數十、數百GB乃至TB的內存,有了高可用的服務集群,也無須追求單個服務要7×24小時不間斷地運行,它們隨時可以中斷和更新;但相應地,Java的啟動時間相對較長,需要預熱才能達到最高性能等特點就顯得相悖于這樣的應用場景。在無服務架構中,矛盾則可能會更加突出,比起服務,一個函數的規模通常會更小,執行時間會更短,當前最熱門的無服務運行環境AWS Lambda所允許的最長運行時間僅有15分鐘。
一直把軟件服務作為重點領域的Java自然不可能對此視而不見,在最新的幾個JDK版本的功能清單中,已經陸續推出了跨進程的、可以面向用戶程序的類型信息共享(Application Class Data Sharing,AppCDS,允許把加載解析后的類型信息緩存起來,從而提升下次啟動速度,原本CDS只支持Java標準庫,在JDK 10時的AppCDS開始支持用戶的程序代碼)、無操作的垃圾收集器(Epsilon,只做內存分配而不做回收的收集器,對于運行完就退出的應用十分合適)等改善措施。而醞釀中的一個更徹底的解決方案,是逐步開始對提前編譯(Ahead of Time Compilation,AOT)提供支持。
提前編譯是相對于即時編譯的概念,提前編譯能帶來的最大好處是Java虛擬機加載這些已經預編譯成二進制庫之后就能夠直接調用,而無須再等待即時編譯器在運行時將其編譯成二進制機器碼。理論上,提前編譯可以減少即時編譯帶來的預熱時間,減少Java應用長期給人帶來的“第一次運行慢”的不良體驗,可以放心地進行很多全程序的分析行為,可以使用時間壓力更大的優化措施。
但是提前編譯的壞處也很明顯,它破壞了Java“一次編寫,到處運行”的承諾,必須為每個不同的硬件、操作系統去編譯對應的發行包;也顯著降低了Java鏈接過程的動態性,必須要求加載的代碼在編譯期就是全部已知的,而不能在運行期才確定,否則就只能舍棄掉已經提前編譯好的版本,退回到原來的即時編譯執行狀態。
早在JDK 9時期,Java就提供了實驗性的Jaotc命令來進行提前編譯,不過多數人試用過后都頗感失望,大家原本期望的是類似于Excelsior JET那樣的編譯過后能生成本地代碼完全脫離Java虛擬機運行的解決方案,但Jaotc其實僅僅是代替即時編譯的一部分作用而已,仍需要運行于HotSpot之上。
直到Substrate VM出現,才算是滿足了人們心中對Java提前編譯的全部期待。Substrate VM是在Graal VM 0.20版本里新出現的一個極小型的運行時環境,包括了獨立的異常處理、同步調度、線程管理、內存管理(垃圾收集)和JNI訪問等組件,目標是代替HotSpot用來支持提前編譯后的程序執行。它還包含了一個本地鏡像的構造器(Native Image Generator),用于為用戶程序建立基于Substrate VM的本地運行時鏡像。這個構造器采用指針分析(Points-To Analysis)技術,從用戶提供的程序入口出發,搜索所有可達的代碼。在搜索的同時,它還將執行初始化代碼,并在最終生成可執行文件時,將已初始化的堆保存至一個堆快照之中。這樣一來,Substrate VM就可以直接從目標程序開始運行,而無須重復進行Java虛擬機的初始化過程。但相應地,原理上也決定了Substrate VM必須要求目標程序是完全封閉的,即不能動態加載其他編譯器不可知的代碼和類庫。基于這個假設,Substrate VM才能探索整個編譯空間,并通過靜態分析推算出所有虛方法調用的目標方法。
Substrate VM帶來的好處是能顯著降低內存占用及啟動時間,由于HotSpot本身就會有一定的內存消耗(通常約幾十MB),這對最低也從幾GB內存起步的大型單體應用來說并不算什么,但在微服務下就是一筆不可忽視的成本。根據Oracle官方給出的測試數據,運行在Substrate VM上的小規模應用,其內存占用和啟動時間與運行在HotSpot上相比有5倍到50倍的下降,具體結果如圖1-5和圖1-6所示。

圖1-5 內存占用對比
Substrate VM補全了Graal VM“Run Programs Faster Anywhere”愿景藍圖里的最后一塊拼圖,讓Graal VM支持其他語言時不會有重量級的運行負擔。譬如運行JavaScript代碼,Node.js的V8引擎執行效率非常高,但即使是最簡單的HelloWorld,它也要使用約20MB的內存,而運行在Substrate VM上的Graal.js,跑一個HelloWorld則只需要4.2MB內存,且運行速度與V8持平。Substrate VM的輕量特性,使得它十分適合嵌入其他系統,譬如Oracle自家的數據庫就已經開始使用這種方式支持用不同的語言代替PL/SQL來編寫存儲過程。在本書第11章還會再詳細討論提前編譯的相關內容。

圖1-6 啟動時間對比
1.5.4 靈活的胖子
即使HotSpot最初設計時考慮得再長遠,大概也不會想到這個虛擬機將在未來的二十年內一直保持長盛不衰。這二十年間有無數改進和功能被不斷地添加到HotSpot的源代碼上,致使它成長為今天這樣的龐然大物。
HotSpot的定位是面向各種不同應用場景的全功能Java虛擬機,這是一個極高的要求,仿佛是讓一個胖子能擁有敏捷的身手一樣的矛盾。如果是持續跟蹤近幾年OpenJDK的代碼變化的人,相信都感覺到了HotSpot開發團隊正在持續地重構著HotSpot的架構,讓它具有模塊化的能力和足夠的開放性。模塊化
方面原本是HotSpot的弱項,監控、執行、編譯、內存管理等多個子系統的代碼相互糾纏。而IBM的J9就一直做得就非常好,面向Java ME的J9虛擬機與面向Java EE的J9虛擬機可以是完全由同一套代碼庫編譯出來的產品,只有編譯時選擇的模塊配置有所差別。
現在,HotSpot虛擬機也有了與J9類似的能力,能夠在編譯時指定一系列特性開關,讓編譯輸出的HotSpot虛擬機可以裁剪成不同的功能,譬如支持哪些編譯器,支持哪些收集器,是否支持JFR、AOT、CDS、NMT等都可以選擇。能夠實現這些功能特性的組合拆分,反映到源代碼不僅僅是條件編譯,更關鍵的是接口與實現的分離。
早期(JDK 1.4時代及之前)的HotSpot虛擬機為了提供監控、調試等不會在《Java虛擬機規范》中約定的內部功能和數據,就曾開放過Java虛擬機信息監控接口(Java Virtual Machine Profiler Interface,JVMPI)與Java虛擬機調試接口(Java Virtual Machine Debug Interface,JVMDI)供運維和性能監控、IDE等外部工具使用。到了JDK 5時期,又抽象出了層次更高的Java虛擬機工具接口(Java Virtual Machine Tool Interface,JVMTI)來為所有Java虛擬機相關的工具提供本地編程接口集合,到JDK 6時JVMTI就完全整合代替了JVMPI和JVMDI的作用。
在JDK 9時期,HotSpot虛擬機開放了Java語言級別的編譯器接口(Java Virtual Machine Compiler Interface,JVMCI),使得在Java虛擬機外部增加、替換即時編譯器成為可能,這個改進實現起來并不費勁,但比起之前JVMPI、JVMDI和JVMTI卻是更深層次的開放,它為不侵入HotSpot代碼而增加或修改HotSpot虛擬機的固有功能邏輯提供了可行性。Graal編譯器就是通過這個接口植入到HotSpot之中。
到了JDK 10,HotSpot又重構了Java虛擬機的垃圾收集器接口(Java Virtual Machine Compiler Interface),統一了其內部各款垃圾收集器的公共行為。有了這個接口,才可能存在日后(今天尚未)某個版本中的CMS收集器退役,和JDK 12中Shenandoah這樣由Oracle以外其他廠商領導開發的垃圾收集器進入HotSpot中的事情。如果未來這個接口完全開放的話,甚至有可能會出現其他獨立于HotSpot的垃圾收集器實現。
經過一系列的重構與開放,HotSpot虛擬機逐漸從時間的侵蝕中掙脫出來,雖然代碼復雜度還在增長,體積仍在變大,但其架構并未老朽,而是擁有了越來越多的開放性和擴展性,使得HotSpot成為一個能夠聯動外部功能,能夠應對各種場景,能夠學會十八般武藝的身手靈活敏捷的“胖子”。
1.5.5 語言語法持續增強
筆者將語言的功能特性和語法放到最后來講,因為它是相對最不重要的改進點,畢竟連JavaScript這種“反人類”的語法都能獲得如此巨大的成功,而比Java語法先進優雅得多的挑戰者C#現在已經“江湖日下”,成了末路英雄。
但一門語言的功能、語法又是影響語言生產力和效率的重要因素,很多語言特性和語法糖不論有沒有,程序也照樣能寫,但即使只是可有可無的語法糖,也是直接影響語言使用者的幸福感程度的關鍵指標。JDK 7的Coins項目結束以后,Java社區又創建了另外一個新的語言特性改進項目Amber,JDK 10至13里面提供的新語法改進基本都來自于這個項目,譬如:
·JEP 286:Local-Variable Type Inference,在JDK 10中提供,本地類型變量推斷。
·JEP 323:Local-Variable Syntax for Lambda Parameters,在JDK 11中提供,JEP 286的加強,使它可以用在Lambda中。
·JEP 325:Switch Expressions,在JDK 13中提供,實現switch語句的表達式支持。
·JEP 335:Text Blocks,在JDK 13中提供,支持文本塊功能,可以節省拼接HTML、SQL等場景里大量的“+”操作。
還有一些是仍然處于草稿狀態或者暫未列入發布范圍的JEP,可供我們窺探未來Java語法的變化,譬如:
·JEP 301:Enhanced Enums,允許常量類綁定數據類型,攜帶額外的信息。
·JEP 302:Lambda Leftovers,用下劃線來表示Lambda中的匿名參數。
·JEP 305:Pattern Matching for instanceof,用instanceof判斷過的類型,在條件分支里面可以不需要做強類型轉換就能直接使用。
除語法糖以外,語言的功能也在持續改進之中,以下幾個項目是目前比較明確的,也是受到較多關注的功能改進計劃:
·Project Loom:現在的Java做并發處理的最小調度單位是線程,Java線程的調度是直接由操作系統內核提供的(這方面的內容可見本書第12章),會有核心態、用戶態的切換開銷。而很多其他語言都提供了更加輕量級的、由軟件自身進行調度的用戶線程(曾經非常早期的Java也有綠色線程),譬如Golang的Groutine、D語言的Fiber等。Loom項目就準備提供一套與目前Thread類API非常接近的Fiber實現。
·Project Valhalla:提供值類型和基本類型的泛型支持,并提供明確的不可變類型和非引用類型的聲明。值類型的作用和價值在本書第10章會專門討論,而不可變類型在并發編程中能帶來很多好處,沒有數據競爭風險帶來了更好的性能。一些語言(如Scala)就有明確的不可變類型聲明,而Java中只能在定義類時將全部字段聲明為final來間接實現。基本類型的范型支持是指在泛型中引用基本數據類型不需要自動裝箱和拆箱,避免性能損耗。
·Project Panama:目的是消弭Java虛擬機與本地代碼之間的界線。現在Java代碼可以通過JNI來調用本地代碼,這點在與硬件交互頻繁的場合尤其常用(譬如Android)。但是JNI的調用方式充其量只能說是達到能用的標準而已,使用起來仍相當煩瑣,頻繁執行的性能開銷也非常高昂,Panama項目的目標就是提供更好的方式讓Java代碼與本地代碼進行調用和傳輸數據。
隨著Java每半年更新一次的節奏,新版本的Java中會出現越來越多其他語言里已有的優秀特性,相信博采眾長的Java,還能繼續保持現在的勃勃生機相當長時間。
- Java多線程編程實戰指南:設計模式篇(第2版)
- 算法精粹:經典計算機科學問題的Java實現
- Learning SAP Analytics Cloud
- NativeScript for Angular Mobile Development
- Hands-On Functional Programming with TypeScript
- BIM概論及Revit精講
- Flutter跨平臺開發入門與實戰
- Android程序設計基礎
- Instant Nancy Web Development
- Mastering Linux Security and Hardening
- UNIX Linux程序設計教程
- Geospatial Development By Example with Python
- CodeIgniter Web Application Blueprints
- Java程序設計教程
- Elasticsearch搜索引擎構建入門與實戰