一個隱藏比較深的問題爵赵。項目使用grpc完成遠程調(diào)用扛或,但是grpc定義的pd文件,由于message中參數(shù)過多瓢颅,達到190多個恩尾,在pb的該message某次新增一個字段后。項目的CPU使用率直接由10%飆升到70%挽懦。
1. 基礎(chǔ)知識
1.1 什么叫做JIT
在計算機技術(shù)中翰意,即時編譯(just-in-time,縮寫JIT;又譯及時編譯冀偶、實時編譯)醒第,也被稱為動態(tài)編譯或者運行時編譯,是一種執(zhí)行計算機代碼的方法进鸠,這種方法涉及在程序執(zhí)行過程中(在執(zhí)行期)而不是執(zhí)行之前進行編譯稠曼。
1.2 JIT工作原理
在字節(jié)碼編譯的系統(tǒng)中,源代碼被轉(zhuǎn)換為稱為字節(jié)碼的中間表示形式客年。字節(jié)碼不是任何特定計算機的機器代碼霞幅,可以在計算機體系結(jié)構(gòu)之間移植(即JAVA的跨平臺性)。然后可以在虛擬機上解釋或者運行字節(jié)碼量瓜、JIT編譯器在許多(或全部司恳、很少)部分讀取字節(jié)碼,并將他們動態(tài)的編譯成機器代碼榔至,以便程序更快速運行抵赢。這可以針對每個文件、每個函數(shù)甚至任何任意代碼片段進行編譯唧取;代碼可以在即將執(zhí)行時進行編譯(因此稱為“即時”)铅鲤,然后緩存并在以后重用,無需重新編譯枫弟。
缺省情況下邢享,啟用JIT編譯器。在編譯方法時淡诗,JVM直接調(diào)用該方法的已編譯代碼骇塘,而不是對代碼進行解釋。理論上韩容,如果編譯不需要占用處理器時間和內(nèi)存款违,那么編譯每個方法都可能使JAVA程序速度接近于本機應(yīng)用程序的速度。
圖片來源:如何通俗易懂地介紹「即時編譯」(JIT)
疑問群凶?為什么JVM里既有compiler插爹,也有interpreter(解釋器)?
JVM在解釋器之外引入了即時(Just In Time)編譯器:當(dāng)程序運行時请梢,解釋器首先發(fā)揮作用赠尾,代碼可以直接執(zhí)行。隨著時間推移毅弧,即時編譯器逐漸發(fā)揮作用气嫁,把越來越多的代碼編譯優(yōu)化成本地代碼,來獲取更高的執(zhí)行效率够坐。解釋器這時可以作為編譯運行的降級手段寸宵,在一些不可靠的編譯優(yōu)化出現(xiàn)問題時崖面,再切換回解釋執(zhí)行,保證程序可以正常運行邓馒。
1.3 JIT編譯是在項目啟動時編譯的嗎嘶朱?
不是!JIT編譯不需要占用處理器時間和內(nèi)存光酣。在JVM首次啟動時疏遏,將調(diào)用數(shù)千種方法。即使程序最終實現(xiàn)了較高的峰值性能救军,編譯所有這些方法也會對啟動時間產(chǎn)生顯著影響财异。實際上,第一次調(diào)用方法時不會對方法進行編譯唱遭。對于每個方法戳寸,JVM都會保留一個調(diào)用計數(shù),以預(yù)定義的編譯閾值開始拷泽,并在每次調(diào)用方法時遞減疫鹊。在調(diào)用計數(shù)達到零時,將觸發(fā)方法的即時編譯司致。因此拆吆,在JVM啟動后將立即編譯常用方法,而較長時間(或者根本不編譯)不常用的方法脂矫。JIT編譯閾值幫助JVM快速啟動并且還可以提高性能枣耀。選擇閾值以在啟動時與長期性能之間實現(xiàn)最佳平衡。
為了提升執(zhí)行速度庭再,Hotspot JVM采用了JIT compile技術(shù)捞奕,JIT編譯器將運行頻率很高的字節(jié)碼(熱點代碼)直接編譯為機器碼執(zhí)行以提高性能。
1.4 JIT編譯與靜態(tài)編譯
常見的編譯型語言如C++拄轻,通常會把代碼直接編譯成CPU所能理解的機器碼來運行颅围。而Java為了實現(xiàn)“一次編譯,處處運行”的特性恨搓,把編譯的過程分成兩部分谷浅,首先它會先由javac編譯成通用的中間形式——字節(jié)碼,然后再由解釋器逐條將字節(jié)碼解釋為機器碼來執(zhí)行奶卓。所以在性能上,Java通常不如C++這類編譯型語言撼玄。
為了優(yōu)化Java的性能 夺姑,JVM在解釋器之外引入了即時(Just In Time)編譯器:當(dāng)程序運行時,解釋器首先發(fā)揮作用掌猛,代碼可以直接執(zhí)行盏浙。隨著時間推移眉睹,即時編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯優(yōu)化成本地代碼废膘,來獲取更高的執(zhí)行效率竹海。解釋器這時可以作為編譯運行的降級手段蚕礼,在一些不可靠的編譯優(yōu)化出現(xiàn)問題時霎匈,再切換回解釋執(zhí)行,保證程序可以正常運行寄悯。
即時編譯器極大地提高了Java程序的運行速度灌闺,而且跟靜態(tài)編譯相比艰争,JIT(即時編譯器)可以選擇性地編譯熱點代碼,省去了很多編譯時間桂对,也節(jié)省很多的空間甩卓。目前,即時編譯器已經(jīng)非常成熟了蕉斜,在性能層面甚至可以和編譯型語言相比逾柿。不過在這個領(lǐng)域,大家依然在不斷探索如何結(jié)合不同的編譯方式宅此,使用更加智能的手段來提升程序的運行速度机错。
2. JIT編譯與CPU使用率飆升
上面說到JIT編譯,那么他和CPU使用率飆升有什么關(guān)系呢诽凌?
上面說到毡熏,熱點代碼會被直接編譯成機器碼執(zhí)行來提高性能,但是考慮這樣一個場景:熱點代碼每次調(diào)用時均逐條解釋(Interpreter)侣诵,是不是需要消耗CPU資源痢法?
那么您又得好奇了,JVM不是采用了JIT技術(shù)了嗎杜顺,為啥熱點代碼不會直接編譯成機器碼執(zhí)行财搁?
2.1 大方法默認關(guān)閉JIT編譯
上面說到,JVM是默認開啟JIT編譯器躬络,但是存在一個參數(shù)尖奔,若一個方法中字節(jié)碼大小超過8000字節(jié),那么就不允許被JIT編譯穷当,而8000這個閾值在產(chǎn)品版的HotSpot里無法被調(diào)整提茁。
關(guān)鍵詞:
-XX:+DontCompileHugeMethods
-XX:HugeMethodLimit=8000
解決方案:可以通過JVM參數(shù):-XX:-DontCompileHugeMethods
來允許大方法被JIT編譯。
但是這種方式存在風(fēng)險馁菜,會導(dǎo)致所有的方法都有可能編譯成字節(jié)碼茴扁,但是一旦CodeCache滿了,后續(xù)的方法都無法編譯成字節(jié)碼汪疮,這種方法是存在一定風(fēng)險的峭火。
2.2 查看方法占用的字節(jié)
可以下載:jclasslib Bytecode viewer插件毁习。
使用方法:IDEA字節(jié)碼學(xué)習(xí)查看神器jclasslib bytecode viewer介紹
2.3 觸發(fā)場景以及解決方案
測試案例——Java中一個方法字節(jié)碼的長度為什么會影響程序并發(fā)下的性能?
在實際工作中卖丸,一個方法很少能超過8000字節(jié)纺且,但是自動生成的方法便存在這種風(fēng)險。例如本次事故稍浆,就是grpc的pb文件中载碌,一個message{}中的屬性字段太多導(dǎo)致的。
針對grpc的這種場景:可以增加一個嵌套的message字段粹湃,新增的字段都放在這個嵌套結(jié)構(gòu)下恐仑。可以解決這種業(yè)務(wù)場景为鳄。
參考資料
IDEA字節(jié)碼學(xué)習(xí)查看神器jclasslib bytecode viewer介紹