本文轉(zhuǎn)載自:https://mp.weixin.qq.com/s/fVz2A-AmgfhF0sTkz8ADNw
Java 誕生距今已有 25 年象颖,但它仍然長(zhǎng)期占據(jù)著“天下第一”編程語(yǔ)言的寶座佩厚。只是其統(tǒng)治地位并非堅(jiān)不可摧,反倒可以說(shuō)是危機(jī)四伏说订。云原生時(shí)代抄瓦,Java 技術(shù)體系的許多前提假設(shè)都受到了挑戰(zhàn),目前已經(jīng)有可預(yù)見(jiàn)的陶冷、足以威脅動(dòng)搖其根基的潛在可能性正在醞釀闺鲸。同時(shí),像 Golang埃叭、Rust 這樣的新生語(yǔ)言摸恍,以及 C、C++、C#立镶、Python 等老對(duì)手也都對(duì) Java 的市場(chǎng)份額虎視眈眈壁袄。面對(duì)危機(jī),Java 正在嘗試哪些變革媚媒?未來(lái)嗜逻,Java 是會(huì)繼續(xù)向前、再攀高峰缭召,還是由盛轉(zhuǎn)衰栈顷?在今天由極客邦科技舉辦的 QCon 全球軟件開(kāi)發(fā)大會(huì) 2020(深圳站)上,遠(yuǎn)光軟件研究院院長(zhǎng)嵌巷、《深入理解 Java 虛擬機(jī)》系列書(shū)籍作者周志明發(fā)表了主題演講《云原生時(shí)代的 Java》萄凤,以下內(nèi)容為演講整理。
今天搪哪,25歲的Java仍然是最具有統(tǒng)治力的編程語(yǔ)言靡努,長(zhǎng)期占據(jù)編程語(yǔ)言排行榜的首位,擁有一千二百萬(wàn)的龐大開(kāi)發(fā)者群體晓折,全世界有四百五十億部物理設(shè)備使用著Java技術(shù)惑朦,同時(shí),在云端數(shù)據(jù)中心的虛擬化環(huán)境里漓概,還運(yùn)行著超過(guò)兩百五十億個(gè)Java虛擬機(jī)的進(jìn)程實(shí)例 (數(shù)據(jù)來(lái)自O(shè)racle的WebCast)漾月。
以上這些數(shù)據(jù)是Java過(guò)去25年巨大成就的功勛佐證,更是Java技術(shù)體系維持自己“天下第一”編程語(yǔ)言的堅(jiān)實(shí)壁壘胃珍。Java與其他語(yǔ)言競(jìng)爭(zhēng)栅屏,底氣從來(lái)不在于語(yǔ)法、類(lèi)庫(kù)有多么先進(jìn)好用堂鲜,而是來(lái)自它龐大的用戶群和極其成熟的軟件生態(tài)栈雳,這在朝夕之間難以撼動(dòng)。然而缔莲,這個(gè)現(xiàn)在看起來(lái)仍然堅(jiān)不可摧的Java帝國(guó)哥纫,其統(tǒng)治地位的穩(wěn)固程度不僅沒(méi)有高枕無(wú)憂,反而說(shuō)是危機(jī)四伏也不為過(guò)痴奏。目前已經(jīng)有了可預(yù)見(jiàn)的蛀骇、足以威脅動(dòng)搖其根基的潛在可能性正在醞釀,并隨云原生時(shí)代而降臨读拆。
Java 的危機(jī)
Java與云原生的矛盾擅憔,來(lái)源于Java誕生之初,植入到它基因之中的一些基本的前提假設(shè)已經(jīng)逐漸開(kāi)始被動(dòng)搖檐晕,甚至已經(jīng)不再成立暑诸。
我舉個(gè)例子蚌讼,每一位Java的使用者都聽(tīng)說(shuō)過(guò)“一次編寫(xiě),到處運(yùn)行”(Write Once, Run Anywhere)這句口號(hào)个榕。20多年前篡石,Java成熟之前,開(kāi)發(fā)者如果希望程序在Linux西采、Solaris凰萨、Windows等不同平臺(tái),在x86械馆、AMD64胖眷、SPARC、MIPS霹崎、ARM等不同指令集架構(gòu)上都能正常運(yùn)行珊搀,就必須針對(duì)每種組合,編譯出對(duì)應(yīng)的二進(jìn)制發(fā)行包仿畸,或者索性直接分發(fā)源代碼,由使用者在自己的平臺(tái)上編譯朗和。
面對(duì)這個(gè)問(wèn)題错沽,Java通過(guò)語(yǔ)言層虛擬化的方式,令每一個(gè)Java應(yīng)用都自動(dòng)取得平臺(tái)無(wú)關(guān)(Platform Independent)眶拉、架構(gòu)中立(Architecture Neutral)的先天優(yōu)勢(shì)千埃,讓同一套程序格式得以在不同指令集架構(gòu)、不同操作系統(tǒng)環(huán)境下都能運(yùn)行且得到一致的結(jié)果忆植,不僅方便了程序的分發(fā)放可,還避免了各種平臺(tái)下內(nèi)存模型、線程模型朝刊、字節(jié)序等底層細(xì)節(jié)差異對(duì)程序編寫(xiě)的干擾耀里。在當(dāng)年,Java的這種設(shè)計(jì)帶有令人趨之若鶩的強(qiáng)大吸引力拾氓,直接開(kāi)啟了托管語(yǔ)言(Managed Language冯挎,如Java、.NET)的一段興盛期咙鞍。
面對(duì)相同的問(wèn)題房官,今天的云原生選擇以操作系統(tǒng)層虛擬化的方式,通過(guò)容器實(shí)現(xiàn)的不可變基礎(chǔ)設(shè)施去解決续滋。不可變基礎(chǔ)設(shè)施這個(gè)概念出現(xiàn)得比云原生要早翰守,原本是指該如何避免由于運(yùn)維人員對(duì)服務(wù)器運(yùn)行環(huán)境所做的持續(xù)的變更而導(dǎo)致的意想不到的副作用。但在云原生時(shí)代疲酌,它的內(nèi)涵已不再局限于方便運(yùn)維蜡峰、程序升級(jí)和部署的手段,而是升華一種為向應(yīng)用代碼隱藏環(huán)境復(fù)雜性的手段,是分布式服務(wù)得以成為一種可普遍推廣的普適架構(gòu)風(fēng)格的必要前提事示。
將程序連同它的運(yùn)行環(huán)境一起封裝到穩(wěn)定的鏡像里早像,現(xiàn)已是一種主流的應(yīng)用程序分發(fā)方式。Docker同樣提出過(guò)“一次構(gòu)建肖爵,到處運(yùn)行”(Build Once, Run Anywhere)的口號(hào)卢鹦,盡管它只能提供環(huán)境兼容性和有局限的平臺(tái)無(wú)關(guān)性(指系統(tǒng)內(nèi)核功能以上的ABI兼容),且完全不可能支撐架構(gòu)中立性劝堪,所以將“一次構(gòu)建冀自,到處運(yùn)行”與“一次編寫(xiě),到處運(yùn)行”對(duì)立起來(lái)并不嚴(yán)謹(jǐn)恰當(dāng)秒啦,但是無(wú)可否認(rèn)熬粗,今天Java技術(shù)“一次編譯,到處運(yùn)行”的優(yōu)勢(shì)余境,已經(jīng)被容器大幅度地削弱驻呐,不再是大多數(shù)服務(wù)端開(kāi)發(fā)者技術(shù)選型的主要考慮因素了。
如果僅僅是優(yōu)勢(shì)的削弱芳来,并不足以成為Java的直接威脅含末,充其量只是一個(gè)潛在的不利因素,但更加迫在眉睫的風(fēng)險(xiǎn)來(lái)自于那些與技術(shù)潮流直接沖突的假設(shè)即舌。譬如佣盒,Java總體上是面向大規(guī)模、長(zhǎng)時(shí)間的服務(wù)端應(yīng)用而設(shè)計(jì)的顽聂,嚴(yán)(luō)謹(jǐn)(suō)的語(yǔ)法利于約束所有人寫(xiě)出較一致的代碼肥惭;靜態(tài)類(lèi)型動(dòng)態(tài)鏈接的語(yǔ)言結(jié)構(gòu),利于多人協(xié)作開(kāi)發(fā)紊搪,讓軟件觸及更大規(guī)模蜜葱;即時(shí)編譯器、性能制導(dǎo)優(yōu)化耀石、垃圾收集子系統(tǒng)等Java最具代表性的技術(shù)特征笼沥,都是為了便于長(zhǎng)時(shí)間運(yùn)行的程序能享受到硬件規(guī)模發(fā)展的紅利。
另一方面娶牌,在微服務(wù)的背景下奔浅,提倡服務(wù)圍繞業(yè)務(wù)能力而非技術(shù)來(lái)構(gòu)建應(yīng)用,不再追求實(shí)現(xiàn)上的一致诗良,一個(gè)系統(tǒng)由不同語(yǔ)言汹桦,不同技術(shù)框架所實(shí)現(xiàn)的服務(wù)來(lái)組成是完全合理的;服務(wù)化拆分后鉴裹,很可能單個(gè)微服務(wù)不再需要再面對(duì)數(shù)十舞骆、數(shù)百GB乃至TB的內(nèi)存钥弯;有了高可用的服務(wù)集群,也無(wú)須追求單個(gè)服務(wù)要7×24小時(shí)不可間斷地運(yùn)行督禽,它們隨時(shí)可以中斷和更新脆霎。
同時(shí),微服務(wù)又對(duì)應(yīng)用的容器化親和性狈惫,譬如鏡像體積睛蛛、內(nèi)存消耗、啟動(dòng)速度胧谈,以及達(dá)到最高性能的時(shí)間等方面提出了新的要求忆肾。這兩年的網(wǎng)紅概念Serverless也進(jìn)一步增加這些因素的考慮權(quán)重,而這些卻正好都是Java的弱項(xiàng):哪怕再小的Java程序也要帶著完整的虛擬機(jī)和標(biāo)準(zhǔn)類(lèi)庫(kù)菱肖,使得鏡像拉取和容器創(chuàng)建效率降低客冈,進(jìn)而使整個(gè)容器生命周期拉長(zhǎng)∥惹浚基于Java虛擬機(jī)的執(zhí)行機(jī)制场仲,使得任何Java的程序都會(huì)有固定的基礎(chǔ)內(nèi)存開(kāi)銷(xiāo),以及固定的啟動(dòng)時(shí)間退疫,而且Java生態(tài)中廣泛采用的依賴(lài)注入進(jìn)一步將啟動(dòng)時(shí)間拉長(zhǎng)渠缕,使得容器的冷啟動(dòng)時(shí)間很難縮短。
軟件工業(yè)中已經(jīng)出現(xiàn)過(guò)不止一起因Java這些弱點(diǎn)而導(dǎo)致失敗的案例蹄咖,如JRuby編寫(xiě)的Logstash褐健,原本是同時(shí)承擔(dān)部署在節(jié)點(diǎn)上的收集端(Shipper)和專(zhuān)門(mén)轉(zhuǎn)換處理的服務(wù)端(Master)的職責(zé)付鹿,后來(lái)因?yàn)橘Y源占用的原因澜汤,被Elstaic.co用Golang的Filebeat代替了Shipper部分的職能;又如Scala語(yǔ)言編寫(xiě)的邊車(chē)代理Linkerd舵匾,作為服務(wù)網(wǎng)格概念的提出者俊抵,卻最終被Envoy所取代,其主要弱點(diǎn)之一也是由于Java虛擬機(jī)的資源消耗所帶來(lái)的劣勢(shì)坐梯。
雖然在云原生時(shí)代依然有很多適合Java發(fā)揮的領(lǐng)域徽诲,但是具備彈性與韌性、隨時(shí)可以中斷重啟的微型服務(wù)的確已經(jīng)形成了一股潮流吵血,在逐步蠶食大型系統(tǒng)的領(lǐng)地谎替。正是由于潮流趨勢(shì)的改變,新一代的語(yǔ)言與技術(shù)尤其重視輕量化和快速響應(yīng)能力蹋辅,大多又重新回歸到了原生語(yǔ)言(Native Language钱贯,如Golang、Rust)之上侦另。
Java 的變革
面對(duì)挑戰(zhàn)秩命,Java的開(kāi)發(fā)者和社區(qū)都沒(méi)有退縮尉共,它們?cè)诟髯缘念I(lǐng)域給出了很多優(yōu)秀的解決方案,涌現(xiàn)了如Quarkus弃锐、Micronaut袄友、Helidon等一大批以提升Java在云原生環(huán)境下的適應(yīng)性為賣(mài)點(diǎn)的框架。
不過(guò)霹菊,今天我們的主題將聚焦在由Java官方本身所推進(jìn)的項(xiàng)目上剧蚣。在圍繞Java 25周年的研討和布道活動(dòng)中,官方的設(shè)定是以“面向未來(lái)的變革”(Innovating for the Future)為基調(diào)浇辜,你有可能在此之前已經(jīng)聽(tīng)說(shuō)過(guò)其中某個(gè)(某些)項(xiàng)目的名字和改進(jìn)點(diǎn)券敌,但這里我們不僅關(guān)心這些項(xiàng)目改進(jìn)的是什么,還更關(guān)心它們背后的動(dòng)機(jī)與困難柳洋、帶來(lái)的收益待诅,以及要付出的代價(jià)。
Innovating for the FutureProject Leyden
對(duì)于原生語(yǔ)言的挑戰(zhàn)熊镣,最有力最徹底的反擊手段無(wú)疑是將字節(jié)碼直接編譯成可以脫離Java虛擬機(jī)的原生代碼卑雁。如果真的能夠生成脫離Java虛擬機(jī)運(yùn)行的原生程序,將意味著啟動(dòng)時(shí)間長(zhǎng)的問(wèn)題能夠徹底解決绪囱,因?yàn)榇藭r(shí)已經(jīng)不存在初始化虛擬機(jī)和類(lèi)加載的過(guò)程测蹲;也意味著程序馬上就能達(dá)到最佳的性能,因?yàn)榇藭r(shí)已經(jīng)不存在即時(shí)編譯器運(yùn)行時(shí)編譯鬼吵,所有代碼都是在編譯期編譯和優(yōu)化好的(如下圖所示)扣甲;沒(méi)有了Java虛擬機(jī)、即時(shí)編譯器這些額外的部件齿椅,也就意味著能夠省去它們?cè)鞠牡哪遣糠謨?nèi)存資源與鏡像體積琉挖。
Java Performance Matrices(圖片來(lái)源)
但同時(shí),這也是風(fēng)險(xiǎn)系數(shù)最高涣脚、實(shí)現(xiàn)難度最大的方案示辈。
Java并非沒(méi)有嘗試走過(guò)這條路,從Java 2之前的GCJ(GNU Compiler for Java)遣蚀,到后來(lái)的Excelsior JET矾麻,再到2018年Oracle Labs啟動(dòng)的GraalVM中的SubstrateVM模塊,最后到2020年中期剛建立的Leyden項(xiàng)目芭梯,都在朝著提前編譯(Ahead-of-Time Compilation险耀,AOT)生成原生程序這個(gè)目標(biāo)邁進(jìn)。
Java支持提前編譯最大的困難在于它是一門(mén)動(dòng)態(tài)鏈接的語(yǔ)言玖喘,它假設(shè)程序的代碼空間是開(kāi)放的(Open World)甩牺,允許在程序的任何時(shí)候通過(guò)類(lèi)加載器去加載新的類(lèi),作為程序的一部分運(yùn)行芒涡。要進(jìn)行提前編譯柴灯,就必須放棄這部分動(dòng)態(tài)性卖漫,假設(shè)程序的代碼空間是封閉的(Closed World),所有要運(yùn)行的代碼都必須在編譯期全部可知赠群。這一點(diǎn)不僅僅影響到了類(lèi)加載器的正常運(yùn)作羊始,除了無(wú)法再動(dòng)態(tài)加載外,反射(通過(guò)反射可以調(diào)用在編譯期不可知的方法)查描、動(dòng)態(tài)代理突委、字節(jié)碼生成庫(kù)(如CGLib)等一切會(huì)運(yùn)行時(shí)產(chǎn)生新代碼的功能都不再可用,如果將這些基礎(chǔ)能力直接抽離掉冬三,Helloworld還是能跑起來(lái)匀油,但Spring肯定跑不起來(lái),Hibernate也跑不起來(lái)勾笆,大部分的生產(chǎn)力工具都跑不起來(lái)敌蚜,整個(gè)Java生態(tài)中絕大多數(shù)上層建筑都會(huì)轟然崩塌。
要獲得有實(shí)用價(jià)值的提前編譯能力窝爪,只有依靠提前編譯器弛车、組件類(lèi)庫(kù)和開(kāi)發(fā)者三方一起協(xié)同才可能辦到。由于Leyden剛剛開(kāi)始蒲每,幾乎沒(méi)有公開(kāi)的資料纷跛,所以下面我是以SubstrateVM為目標(biāo)對(duì)象進(jìn)行的介紹:
有一些功能,像反射這樣的基礎(chǔ)特性是不可能妥協(xié)的邀杏,折衷的解決辦法是由用戶在編譯期贫奠,以配置文件或者編譯器參數(shù)的形式,明確告知編譯器程序代碼中有哪些方法是只通過(guò)反射來(lái)訪問(wèn)的望蜡,編譯器將方法的添加到靜態(tài)編譯的范疇之中唤崭。同理,所有使用到動(dòng)態(tài)代理的地方泣特,也必須在事先列明浩姥,在編譯期就將動(dòng)態(tài)代理的字節(jié)碼全部生成出來(lái)挑随。其他所有無(wú)法通過(guò)程序指針?lè)治觯≒oints-To Analysis)得到的信息状您,譬如程序中用到的資源、配置文件等等兜挨,也必須照此處理膏孟。
另一些功能,如動(dòng)態(tài)生成字節(jié)碼也十分常用拌汇,但用戶自己往往無(wú)法得知那些動(dòng)態(tài)字節(jié)碼的具體信息柒桑,就只能由用到CGLib、javassist等庫(kù)的程序去妥協(xié)放棄噪舀。在Java世界中也許最典型的場(chǎng)景就是Spring用CGLib來(lái)進(jìn)行類(lèi)增強(qiáng)魁淳,默認(rèn)情況下飘诗,每一個(gè)Spring管理的Bean都要用到CGLib。從Spring Framework 5.2開(kāi)始增加了@proxyBeanMethods注解來(lái)排除對(duì)CGLib的依賴(lài)界逛,僅使用標(biāo)準(zhǔn)的動(dòng)態(tài)代理去增強(qiáng)類(lèi)昆稿。
2019年起,Pivotal的Spring團(tuán)隊(duì)與Oracle Labs的GraalVM團(tuán)隊(duì)共同孵化了Spring GraalVM Native項(xiàng)目息拜,這個(gè)目前仍處于Experimental / Alpha狀態(tài)的項(xiàng)目溉潭,能夠讓程序先以傳統(tǒng)方式運(yùn)行(啟動(dòng))一次,自動(dòng)化地找出程序中的反射少欺、動(dòng)態(tài)代理的代碼喳瓣,代替用戶向編譯器提供絕大部分所需的信息,并能將允許啟動(dòng)時(shí)初始化的Bean在編譯期就完成初始化赞别,直接繞過(guò)Spring程序啟動(dòng)最慢的階段畏陕。這樣從啟動(dòng)到程序可以提供服務(wù),耗時(shí)竟能夠低于0.1秒仿滔。
Spring Boot Startup Time(數(shù)據(jù)來(lái)源)
以原生方式運(yùn)行后蹭秋,縮短啟動(dòng)時(shí)間的效果立竿見(jiàn)影,一般會(huì)有數(shù)十倍甚至更高的改善堤撵,程序容量和內(nèi)存消耗也有一定程度的下降仁讨。不過(guò)至少目前而言,程序的運(yùn)行效率還是要弱于傳統(tǒng)基于Java虛擬機(jī)的方式实昨,雖然即時(shí)編譯器有編譯時(shí)間的壓力洞豁,但由于可以進(jìn)行基于假設(shè)的激進(jìn)優(yōu)化和運(yùn)行時(shí)性能度量的制導(dǎo)優(yōu)化,使得即時(shí)編譯器的效果仍要優(yōu)于提前編譯器荒给,這方面需要GraalVM編譯器團(tuán)隊(duì)的進(jìn)一步努力丈挟,也需要從語(yǔ)言改進(jìn)上入手,讓Java變得更適合被編譯器優(yōu)化志电。
Project Valhalla
Java語(yǔ)言上可感知的語(yǔ)法變化曙咽,多數(shù)來(lái)自于Amber項(xiàng)目,它的項(xiàng)目目標(biāo)是持續(xù)優(yōu)化語(yǔ)言生產(chǎn)力挑辆,近期(JDK 15例朱、16)會(huì)有很多來(lái)自這個(gè)項(xiàng)目的特性,如Records鱼蝉、Sealed Class洒嗤、Pattern Matching、Raw String Literals等實(shí)裝到生產(chǎn)環(huán)境魁亦。
然而語(yǔ)法不僅與編碼效率相關(guān)渔隶,與運(yùn)行效率也有很大關(guān)系〗嗄危“程序=代碼+數(shù)據(jù)”這個(gè)提法至少在衡量運(yùn)行效率上是合適的间唉,無(wú)論是托管語(yǔ)言還是原生語(yǔ)言绞灼,最終產(chǎn)物都是處理器執(zhí)行的指令流和內(nèi)存存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)。Java呈野、.NET镀赌、C、C++际跪、Golang商佛、Rust等各種語(yǔ)言誰(shuí)更快,取決于特定場(chǎng)景下姆打,編譯器生成指令流的優(yōu)化效果良姆,以及數(shù)據(jù)在內(nèi)存中的結(jié)構(gòu)布局。
Java即時(shí)編譯器的優(yōu)化效果拔群幔戏,但是由于Java“一切皆為對(duì)象”的前提假設(shè)玛追,導(dǎo)致在處理一系列不同類(lèi)型的小對(duì)象時(shí),內(nèi)存訪問(wèn)性能非常拉垮闲延,這點(diǎn)是Java在游戲痊剖、圖形處理等領(lǐng)域一直難有建樹(shù)的重要制約因素,也是Java建立Valhalla項(xiàng)目的目標(biāo)初衷垒玲。
這里舉個(gè)例子來(lái)說(shuō)明此問(wèn)題陆馁,如果我想描述空間里面若干條線段的集合,在Java中定義的代碼會(huì)是這樣的:
public record Point(float x, float y, float z) {}
面向?qū)ο蟮膬?nèi)存布局中合愈,對(duì)象標(biāo)識(shí)符(Object Identity)存在的目的是為了允許在不暴露對(duì)象結(jié)構(gòu)的前提下叮贩,依然可以引用其屬性與行為,這是面向?qū)ο缶幊讨卸鄳B(tài)性的基礎(chǔ)佛析。在Java中堆內(nèi)存分配和回收益老、空值判斷、引用比較寸莫、同步鎖等一系列功能都會(huì)涉及到對(duì)象標(biāo)識(shí)符捺萌,內(nèi)存訪問(wèn)也是依靠對(duì)象標(biāo)識(shí)符來(lái)進(jìn)行鏈?zhǔn)教幚淼模┤缟厦娲a中的“若干條線段的集合”膘茎,在堆內(nèi)存中將構(gòu)成如下圖的引用關(guān)系:
Object Identity / Memory Layout
計(jì)算機(jī)硬件經(jīng)過(guò)25年的發(fā)展桃纯,內(nèi)存與處理器雖然都在進(jìn)步,但是內(nèi)存延遲與處理器執(zhí)行性能之間的馮諾依曼瓶頸(Von Neumann Bottleneck)不僅沒(méi)有縮減辽狈,反而還在持續(xù)加大慈参,“RAM Is the New Disk”已經(jīng)從嘲諷梗逐漸成為了現(xiàn)實(shí)呛牲。
一次內(nèi)存訪問(wèn)(將主內(nèi)存數(shù)據(jù)調(diào)入處理器Cache)大約需要耗費(fèi)數(shù)百個(gè)時(shí)鐘周期刮萌,而大部分簡(jiǎn)單指令的執(zhí)行只需要一個(gè)時(shí)鐘周期而已。因此娘扩,在程序執(zhí)行性能這個(gè)問(wèn)題上着茸,如果編譯器能減少一次內(nèi)存訪問(wèn)壮锻,可能比優(yōu)化掉幾十、幾百條其他指令都來(lái)得更有效果涮阔。
額外知識(shí):馮諾依曼瓶頸不同處理器(現(xiàn)代處理器都集成了內(nèi)存管理器猜绣,以前是在北橋芯片中)的內(nèi)存延遲大概是40-80納秒(ns,十億分之一秒)敬特,而根據(jù)不同的時(shí)鐘頻率掰邢,一個(gè)時(shí)鐘周期大概在0.2-0.4納秒之間,如此短暫的時(shí)間內(nèi)伟阔,即使真空中傳播的光辣之,也僅僅能夠行進(jìn)10厘米左右蹄衷。數(shù)據(jù)存儲(chǔ)與處理器執(zhí)行的速度矛盾是馮諾依曼架構(gòu)的主要局限性之一有序,1977年的圖靈獎(jiǎng)得主John Backus提出了“馮諾依曼瓶頸”這個(gè)概念涝影,專(zhuān)門(mén)用來(lái)描述這種局限性函卒。
編譯器的確在努力減少內(nèi)存訪問(wèn)示启,從JDK 6起贬媒,HotSpot的即時(shí)編譯器就嘗試通過(guò)逃逸分析來(lái)做標(biāo)量替換(Scalar Replacement)和棧上分配(Stack Allocations)優(yōu)化摧阅,基本原理是如果能通過(guò)分析侧甫,得知一個(gè)對(duì)象不會(huì)傳遞到方法之外灾部,那就不需要真實(shí)地在對(duì)中創(chuàng)建完整的對(duì)象布局康铭,完全可以繞過(guò)對(duì)象標(biāo)識(shí)符,將它拆散為基本的原生數(shù)據(jù)類(lèi)型來(lái)創(chuàng)建赌髓,甚至是直接在棧內(nèi)存中分配空間(HotSpot并沒(méi)有這樣做)麻削,方法執(zhí)行完畢后隨著棧幀一起銷(xiāo)毀掉。
不過(guò)春弥,逃逸分析是一種過(guò)程間優(yōu)化(Interprocedural Optimization)呛哟,非常耗時(shí),也很難處理那些理論有可能但實(shí)際不存在的情況匿沛。相同的問(wèn)題在C扫责、C++中卻并不存在,上面場(chǎng)景中逃呼,程序員只要將Point和Line都定義為struct即可鳖孤,C#中也有struct,是依靠.NET的值類(lèi)型(Value Type)來(lái)實(shí)現(xiàn)的抡笼。Valhalla項(xiàng)目的核心改進(jìn)就是提供類(lèi)似的值類(lèi)型支持苏揣,提供一個(gè)新的關(guān)鍵字(inline),讓用戶可以在不需要向方法外部暴露對(duì)象推姻、不需要多態(tài)性支持平匈、不需要將對(duì)象用作同步鎖的場(chǎng)合中,將類(lèi)標(biāo)識(shí)為值類(lèi)型,此時(shí)編譯器就能夠繞過(guò)對(duì)象標(biāo)識(shí)符增炭,以平坦的忍燥、緊湊的方式去為對(duì)象分配內(nèi)存。
有了值類(lèi)型的支持后隙姿,現(xiàn)在Java泛型中令人詬病的不支持原數(shù)據(jù)類(lèi)型(Primitive Type)梅垄、頻繁裝箱問(wèn)題也就隨之迎刃而解,現(xiàn)在Java的包裝類(lèi)输玷,理所當(dāng)然地會(huì)以代表原生類(lèi)型的值類(lèi)型來(lái)重新定義队丝,這樣Java泛型的性能會(huì)得到明顯的提升,因?yàn)榇藭r(shí)Integer與int的訪問(wèn)欲鹏,在機(jī)器層面看完全可以達(dá)到一致的效率炭玫。
Project Loom
Java語(yǔ)言抽象出來(lái)隱藏了各種操作系統(tǒng)線程差異性的統(tǒng)一線程接口,這曾經(jīng)是它區(qū)別于其他編程語(yǔ)言(C/C++表示有被冒犯到)的一大優(yōu)勢(shì)貌虾,不過(guò)吞加,**統(tǒng)一的線程模型不見(jiàn)得永遠(yuǎn)都是正確的。 **
Java目前主流的線程模型是直接映射到操作系統(tǒng)內(nèi)核上的1:1模型尽狠,這對(duì)于計(jì)算密集型任務(wù)這很合適衔憨,既不用自己去做調(diào)度,也利于一條線程跑滿整個(gè)處理器核心袄膏。但對(duì)于I/O密集型任務(wù)践图,譬如訪問(wèn)磁盤(pán)、訪問(wèn)數(shù)據(jù)庫(kù)占主要時(shí)間的任務(wù)沉馆,這種模型就顯得成本高昂码党,主要在于內(nèi)存消耗和上下文切換上:64位Linux上HotSpot的線程棧容量默認(rèn)是1MB,線程的內(nèi)核元數(shù)據(jù)(Kernel Metadata)還要額外消耗2-16KB內(nèi)存斥黑,所以單個(gè)虛擬機(jī)的最大線程數(shù)量一般只會(huì)設(shè)置到200至400條揖盘,當(dāng)程序員把數(shù)以百萬(wàn)計(jì)的請(qǐng)求往線程池里面灌時(shí),系統(tǒng)即便能處理得過(guò)來(lái)锌奴,其中的切換損耗也相當(dāng)可觀兽狭。
Loom項(xiàng)目的目標(biāo)是讓Java支持額外的N:M線程模型,請(qǐng)注意是“額外支持”鹿蜀,而不是像當(dāng)年從綠色線程過(guò)渡到內(nèi)核線程那樣的直接替換箕慧,也不是像Solaris平臺(tái)的HotSpot虛擬機(jī)那樣通過(guò)參數(shù)讓用戶二選其一。
Loom項(xiàng)目新增加一種“虛擬線程”(Virtual Thread茴恰,以前以Fiber為名進(jìn)行宣傳過(guò)颠焦,但因?yàn)橐l繁解釋啥是Fiber所以現(xiàn)在放棄了),本質(zhì)上它是一種有棧協(xié)程(Stackful Coroutine)往枣,多條虛擬線程可以映射到同一條物理線程之中伐庭,在用戶空間中自行調(diào)度粉渠,每條虛擬線程的棧容量也可由用戶自行決定。
Virtual Thread
同時(shí)似忧,Loom項(xiàng)目的另一個(gè)目標(biāo)是要盡最大可能保持原有統(tǒng)一線程模型的交互方式渣叛,通俗地說(shuō)就是原有的Thread丈秩、J.U.C盯捌、NIO、Executor蘑秽、Future饺著、ForkJoinPool等這些多線程工具都應(yīng)該能以同樣的方式支持新的虛擬線程,原來(lái)多線程中你理解的概念肠牲、編碼習(xí)慣大多數(shù)都能夠繼續(xù)沿用幼衰。
為此,虛擬線程將會(huì)與物理線程一樣使用java.lang.Thread來(lái)進(jìn)行抽象缀雳,只是在創(chuàng)建線程時(shí)用到的參數(shù)或者方法稍有不同(譬如給Thread增加一個(gè)Thread.VIRTUAL_THREAD參數(shù)渡嚣,或者增加一個(gè)startVirtualThread()方法)。這樣現(xiàn)有的多線程代碼遷移到虛擬線程中的成本就會(huì)變得很低肥印,而代價(jià)就是Loom的團(tuán)隊(duì)必須做更多的工作以保證虛擬線程在大部分涉及到多線程的標(biāo)準(zhǔn)API中都能夠兼容识椰,甚至在調(diào)試器上虛擬線程與物理線程看起來(lái)都會(huì)有一致的外觀。但很難全部都支持深碱,譬如調(diào)用JNI的本地棧幀就很難放到虛擬線程上腹鹉,所以一旦遇到本地方法,虛擬線程就會(huì)被綁定(Pinned)到一條物理線程上敷硅。
Loom的另一個(gè)重點(diǎn)改進(jìn)是支持結(jié)構(gòu)化并發(fā)(Structured Concurrency)功咒,這是2016年才提出的新的并發(fā)編程概念,但很快就被諸多編程語(yǔ)言所吸納绞蹦。它是指程序的并發(fā)行為會(huì)與代碼的結(jié)構(gòu)對(duì)齊力奋,譬如以下代碼所示,按照傳統(tǒng)的編程觀念幽七,如果沒(méi)有額外的處理(譬如無(wú)中生有地弄一個(gè)await關(guān)鍵字)刊侯,那在task1和task2提交之后,程序應(yīng)該繼續(xù)向下執(zhí)行:
ThreadFactory factory = Thread.builder().virtual().factory();
但是在結(jié)構(gòu)化并發(fā)的支持下锉走,只有兩個(gè)并行啟動(dòng)的任務(wù)線程都結(jié)束之后滨彻,程序才會(huì)繼續(xù)向下執(zhí)行,很好地以同步的編碼風(fēng)格挪蹭,來(lái)解決異步的執(zhí)行問(wèn)題亭饵。事實(shí)上,“Code like sync梁厉,Work like async”正是Loom簡(jiǎn)化并發(fā)編程的核心理念辜羊。
Project Portola
Portola項(xiàng)目的目標(biāo)是將OpenJDK向Alpine Linux移植踏兜。Alpine Linux是許多Docker容器首選的基礎(chǔ)鏡像,因?yàn)樗挥? MB大小八秃,比起其他Cent OS碱妆、Debain等動(dòng)輒一百多MB的發(fā)行版來(lái)說(shuō),更適合用于容器環(huán)境昔驱。不過(guò)Alpine Linux為了盡量瘦身疹尾,默認(rèn)是用musl作為C標(biāo)準(zhǔn)庫(kù)的,而非傳統(tǒng)的glibc(GNU C library)骤肛,因此要以Alpine Linux為基礎(chǔ)制作OpenJDK鏡像纳本,必須先安裝glibc,此時(shí)基礎(chǔ)鏡像大約有12 MB腋颠。Portola計(jì)劃將OpenJDK的上游代碼移植到musl繁成,并通過(guò)兼容性測(cè)試。使用Portola制作的標(biāo)準(zhǔn)Java SE 13鏡像僅有41 MB淑玫,不僅遠(yuǎn)低于Cent OS的OpenJDK(大約396 MB)巾腕,也要比官方的slim版(約200 MB)要小得多。
$ sudo docker build .
Java 的未來(lái)
云原生時(shí)代絮蒿,Java技術(shù)體系的許多前提假設(shè)都受到了挑戰(zhàn)尊搬,“一次編譯,到處運(yùn)行”歌径、“面向長(zhǎng)時(shí)間大規(guī)模程序而設(shè)計(jì)”毁嗦、“從開(kāi)放的代碼空間中動(dòng)態(tài)加載”、“一切皆為對(duì)象”回铛、“統(tǒng)一線程模型”狗准,等等。技術(shù)發(fā)展迭代不會(huì)停歇茵肃,沒(méi)有必要堅(jiān)持什么“永恒的真理”腔长,舊的原則被打破,只要合理验残,便是創(chuàng)新捞附。
Java語(yǔ)言意識(shí)到了挑戰(zhàn),也意識(shí)到了要面向未來(lái)而變革您没。文中提到的這些項(xiàng)目鸟召,Amber和Portola已經(jīng)明確會(huì)在2021年3月的Java 16中發(fā)布,至少也會(huì)達(dá)到Feature Preview的程度:
JEP 394:Pattern Matching for instanceof
JEP 395:Records
JEP 397:Sealed Classes
JEP 386:Alpine Linux Port
至于更受關(guān)注氨鹏,同時(shí)也是難度更高的 Valhalla 和 Loom 項(xiàng)目欧募,目前仍然沒(méi)有明確的版本計(jì)劃信息,盡管它們已經(jīng)開(kāi)發(fā)了數(shù)年時(shí)間仆抵,非常希望能夠趕在 Java 17 這個(gè) LTS 版本中面世跟继,但前路還是困難重重种冬。
至于難度最高、創(chuàng)建時(shí)間最晚的 Leyden 項(xiàng)目舔糖,目前還完全處于特性討論階段娱两,連個(gè)胚胎都算不上。對(duì)于 Java 的原生編譯金吗,我們中短期內(nèi)只可能寄希望于 Oracle 的 GraalVM十兢。
未來(lái)一段時(shí)間,是Java重要的轉(zhuǎn)型****窗口期辽聊,如果作為下一個(gè)LTS版的Java 17纪挎,能夠成功集Amber期贫、Portola跟匆、Valhalla、Loom和Panama(用于外部函數(shù)接口訪問(wèn)通砍,本文沒(méi)有提到)的新能力玛臂、新特性于一身,GraalVM也能給予足夠強(qiáng)力支持的話封孙,那Java 17 LTS大概率會(huì)是一個(gè)里程碑式的版本迹冤,帶領(lǐng)著整個(gè)Java生態(tài)從大規(guī)模服務(wù)端應(yīng)用,向新的云原生時(shí)代軟件系統(tǒng)轉(zhuǎn)型虎忌。可能成為比肩當(dāng)年從面向嵌入式設(shè)備與瀏覽器Web Applets的Java 1泡徙,到確立現(xiàn)代Java語(yǔ)言方向(Java SE/EE/ME和JavaCard)雛形的Java 2轉(zhuǎn)型那樣的里程碑。
但是膜蠢,如果Java不能加速自己的發(fā)展步伐堪藐,那由強(qiáng)大生態(tài)所構(gòu)建的護(hù)城河終究會(huì)消耗殆盡,被Golang挑围、Rust這樣的新生語(yǔ)言礁竞,以及C、C++杉辙、C#模捂、Python等老對(duì)手蠶食掉很大一部分市場(chǎng)份額,以至被迫從“天下第一”編程語(yǔ)言的寶座中退位蜘矢。
Java的未來(lái)是繼續(xù)向前狂男,再攀高峰,還是由盛轉(zhuǎn)衰品腹,鋒芒挫縮岖食,你我拭目以待。