JVM Client 模式和 Server模式的區(qū)別
通過 java -version 可查看 JVM 所處的模式夹纫,并可以通過修改配置文件進(jìn)行配置逗柴,那它們有什么區(qū)別呢?
Server:-Server 模式啟動時(shí)视译,速度較慢院水,但是啟動之后,性能更高屋摔,適合運(yùn)行服務(wù)器后臺程序
Client:-Client 模式啟動時(shí)烁设,速度較快,啟動之后不如 Server钓试,適合用于桌面等有界面的程序
熱點(diǎn)代碼
理解
當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí)装黑,就會把這些代碼認(rèn)定為“熱點(diǎn)代碼”。
熱點(diǎn)代碼的分類
被多次調(diào)用的方法
一個(gè)方法被調(diào)用得多了弓熏,方法體內(nèi)代碼執(zhí)行的次數(shù)自然就多恋谭,成為“熱點(diǎn)代碼”是理所當(dāng)然的。
被多次執(zhí)行的循環(huán)體
一個(gè)方法只被調(diào)用過一次或少量的幾次挽鞠,但是方法體內(nèi)部存在循環(huán)次數(shù)較多的循環(huán)體疚颊,這樣循環(huán)體的代碼也被重復(fù)執(zhí)行多次,因此這些代碼也應(yīng)該認(rèn)為是“熱點(diǎn)代碼”信认。
上面提到的多次是一個(gè)不具體的詞語材义,那到底是多少次才能成為熱點(diǎn)代碼呢?
如何檢測熱點(diǎn)代碼
判斷一段代碼是否是熱點(diǎn)代碼狮杨,是否需要觸發(fā)即使編譯母截,這樣的行為稱為熱點(diǎn)探測,熱點(diǎn)探測并不一定知道方法具體被調(diào)用了多少次橄教,目前主要的熱點(diǎn)探測判定方式有兩種:
基于采樣的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會周期性地檢查各個(gè)線程的棧頂如果發(fā)現(xiàn)某個(gè)(或某些)方法經(jīng)常出現(xiàn)在棧頂清寇,那這個(gè)方法就是“熱點(diǎn)方法”
優(yōu)點(diǎn):實(shí)現(xiàn)簡單高效,容易獲取方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可)
缺點(diǎn):不精確护蝶,容易因?yàn)橐驗(yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測
基于計(jì)數(shù)器的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器华烟,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果次數(shù)超過一定的閾值就認(rèn)為它是“熱點(diǎn)方法”
優(yōu)點(diǎn):統(tǒng)計(jì)結(jié)果精確嚴(yán)謹(jǐn)
缺點(diǎn):實(shí)現(xiàn)麻煩持灰,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器盔夜,不能直接獲取到方法的調(diào)用關(guān)系
HotSpot使用第二種 - 基于計(jì)數(shù)器的熱點(diǎn)探測方法。
確定了檢測熱點(diǎn)代碼的方式堤魁,如何計(jì)算具體的次數(shù)呢喂链?
計(jì)數(shù)器的種類(兩種共同協(xié)作)
方法調(diào)用計(jì)數(shù)器:這個(gè)計(jì)數(shù)器用于統(tǒng)計(jì)方法被調(diào)用的次數(shù)。默認(rèn)閾值在 Client 模式下是 1500 次妥泉,在 Server 模式下是 10000 次
回邊計(jì)數(shù)器:統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù)
了解了熱點(diǎn)代碼和計(jì)數(shù)器有什么用呢椭微?達(dá)到計(jì)數(shù)器的閾值會觸發(fā)后文講解的即時(shí)編譯,也就是說即時(shí)編譯是需要達(dá)到某種條件才會觸發(fā)的盲链,先寫結(jié)論蝇率,后文講解什么是即時(shí)編譯器迟杂。
兩個(gè)計(jì)數(shù)器的協(xié)作(這里討論的是方法調(diào)用計(jì)數(shù)器的情況):當(dāng)一個(gè)方法被調(diào)用時(shí),會先檢查該方法是否存在被 JIT(后文講解) 編譯過的版本本慕,如果存在排拷,則優(yōu)先使用編譯后的本地代碼來執(zhí)行。如果不存在已被編譯過的版本锅尘,則將此方法的調(diào)用計(jì)數(shù)器加 1监氢,然后判斷方法調(diào)用計(jì)數(shù)器與回邊計(jì)數(shù)器之和是否超過方法調(diào)用計(jì)數(shù)器的閾值。如果已經(jīng)超過閾值藤违,那么將會向即時(shí)編譯器提交一個(gè)該方法的代碼編譯請求忙菠。
當(dāng)編譯工作完成之后,這個(gè)方法的調(diào)用入口地址就會被系統(tǒng)自動改成新的纺弊,下一次調(diào)用該方法時(shí)就會使用已編譯的版本。
什么是字節(jié)碼骡男、機(jī)器碼淆游、本地代碼?
字節(jié)碼是指平常所了解的 .class 文件隔盛,Java 代碼通過 javac 命令編譯成字節(jié)碼
機(jī)器碼和本地代碼都是指機(jī)器可以直接識別運(yùn)行的代碼犹菱,也就是機(jī)器指令
字節(jié)碼是不能直接運(yùn)行的,需要經(jīng)過 JVM 解釋或編譯成機(jī)器碼才能運(yùn)行
此時(shí)你要問了吮炕,為什么 Java 不直接編譯成機(jī)器碼腊脱,這樣不是更快嗎?
1. 機(jī)器碼是與平臺相關(guān)的龙亲,也就是操作系統(tǒng)相關(guān)陕凹,不同操作系統(tǒng)能識別的機(jī)器碼不同,如果編譯成機(jī)器碼那豈不是和 C鳄炉、C++差不多了杜耙,不能跨平臺,Java 就沒有那響亮的口號 “一次編譯拂盯,到處運(yùn)行”佑女;
2.之所以不一次性全部編譯,是因?yàn)橛幸恍┐a只運(yùn)行一次谈竿,沒必要編譯团驱,直接解釋運(yùn)行就可以。而那些“熱點(diǎn)”代碼空凸,反復(fù)解釋執(zhí)行肯定很慢嚎花,JVM 在運(yùn)行程序的過程中不斷優(yōu)化,用JIT編譯器編譯那些熱點(diǎn)代碼劫恒,讓他們不用每次都逐句解釋執(zhí)行贩幻;
3.還有一方面的原因是后文講解的解釋器與編譯器共存的原因轿腺。
什么是 JIT ?
為了提高熱點(diǎn)代碼的執(zhí)行效率丛楚,在運(yùn)行時(shí)族壳,虛擬機(jī)將會把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各種層次的優(yōu)化趣些,完成這個(gè)任務(wù)的編譯器稱為即時(shí)編譯器(Just In Time Compiler)仿荆,簡稱 JIT 編譯器
什么是編譯和解釋?
編譯器:把源程序的每一條語句都編譯成機(jī)器語言,并保存成二進(jìn)制文件,這樣運(yùn)行時(shí)計(jì)算機(jī)可以直接以機(jī)器語言來運(yùn)行此程序,速度很快;
解釋器:只在執(zhí)行程序時(shí),才一條一條的解釋成機(jī)器語言給計(jì)算機(jī)來執(zhí)行,所以運(yùn)行速度是不如編譯后的程序運(yùn)行的快的坏平;
通過javac命令將 Java 程序的源代碼編譯成 Java 字節(jié)碼拢操,即我們常說的 class 文件。這是我們通常意義上理解的編譯舶替。
字節(jié)碼并不是機(jī)器語言令境,要想讓機(jī)器能夠執(zhí)行,還需要把字節(jié)碼翻譯成機(jī)器指令顾瞪。這個(gè)過程是Java 虛擬機(jī)做的舔庶,這個(gè)過程也叫編譯。是更深層次的編譯陈醒。(實(shí)際上就是解釋惕橙,引入 JIT 之后也存在編譯)
此時(shí)又有疑惑了,Java 不是解釋執(zhí)行的嗎钉跷?
沒錯(cuò)弥鹦,Java 需要將字節(jié)碼逐條翻譯成對應(yīng)的機(jī)器指令并且執(zhí)行,這就是傳統(tǒng)的 JVM 的解釋器的功能爷辙,正是由于解釋器逐條翻譯并執(zhí)行這個(gè)過程的效率低彬坏,引入了 JIT 即時(shí)編譯技術(shù)。
必須指出的是膝晾,不管是解釋執(zhí)行苍鲜,還是編譯執(zhí)行,最終執(zhí)行的代碼單元都是可直接在真實(shí)機(jī)器上運(yùn)行的機(jī)器碼玷犹,或稱為本地代碼
附一張圖來理解
為何 HotSpot 虛擬機(jī)要使用解釋器與編譯器并存的架構(gòu)混滔?
解釋器與編譯器兩者各有優(yōu)勢
解釋器:當(dāng)程序需要迅速啟動和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用歹颓,省去編譯的時(shí)間坯屿,立即執(zhí)行。
編譯器:在程序運(yùn)行后巍扛,隨著時(shí)間的推移领跛,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后撤奸,可以獲取更高的執(zhí)行效率吠昭。
兩者的協(xié)作:在程序運(yùn)行環(huán)境中內(nèi)存資源限制較大時(shí)喊括,可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯執(zhí)行來提升效率矢棚。當(dāng)通過編譯器優(yōu)化時(shí)郑什,發(fā)現(xiàn)并沒有起到優(yōu)化作用,蒲肋,可以通過逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行蘑拯。
即時(shí)編譯器與 Java 虛擬機(jī)的關(guān)系
即時(shí)編譯器并不是虛擬機(jī)必需的部分,Java 虛擬機(jī)規(guī)范并沒有規(guī)定 Java 虛擬機(jī)內(nèi)必須要有即時(shí)編譯器的存在兜粘,更沒有限定或指導(dǎo)即時(shí)編譯器應(yīng)該如何去實(shí)現(xiàn)申窘。
但是,即時(shí)編譯器編譯性能的好壞孔轴、代碼優(yōu)化程度的高低卻是衡量一款商用虛擬機(jī)優(yōu)秀與否的最關(guān)鍵的指標(biāo)之一剃法。它也是虛擬機(jī)中最核心且最能體現(xiàn)虛擬機(jī)技術(shù)水平的部分。
即時(shí)編譯器的分類
Client Compiler - C1編譯器
Server Compiler - C2編譯器
目前主流的 HotSpot 虛擬機(jī)(JDK1.7 及之前版本的虛擬機(jī))默認(rèn)采用一個(gè)解釋器和其中一個(gè)編譯器直接配合的方式工作路鹰,程序使用哪個(gè)編譯器玄窝,取決于虛擬機(jī)運(yùn)行的模式,就是文章開頭提到的兩種模式悍引。
在 HotSpot 中,解釋器和 JIT 即時(shí)編譯器是同時(shí)存在的帽氓,他們是 JVM 的兩個(gè)組件趣斤。對于不同類型的應(yīng)用程序,用戶可以根據(jù)自身的特點(diǎn)和需求黎休,靈活選擇是基于解釋器運(yùn)行還是基于 JIT 編譯器運(yùn)行浓领。HotSpot 為用戶提供了幾種運(yùn)行模式供選擇,可通過參數(shù)設(shè)定势腮,分別為:解釋模式联贩、編譯模式、混合模式捎拯,HotSpot 默認(rèn)是混合模式泪幌,需要注意的是編譯模式并不是完全通過 JIT 進(jìn)行編譯,只是優(yōu)先采用編譯方式執(zhí)行程序署照,但是解釋器仍然要在編譯無法進(jìn)行的情況下介入執(zhí)行過程祸泪。
分層編譯
產(chǎn)生的原因:由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,要編譯出優(yōu)化程度更高的代碼建芙,所花費(fèi)的時(shí)間可能更長没隘;而且要想編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息禁荸,這對解釋執(zhí)行的速度也有影響右蒲。為了在程序啟動響應(yīng)速度與運(yùn)行效率之間達(dá)到最佳平衡阀湿,HotSpot 虛擬機(jī)啟用分層編譯的策略
分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時(shí)瑰妄,劃分出不同的編譯層次:
第 0 層:程序解釋執(zhí)行陷嘴,解釋器不開啟性能監(jiān)控功能,可觸發(fā)第 1 層編譯翰撑。
第 1 層:也稱為 C1 編譯罩旋,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單眶诈,可靠的優(yōu)化涨醋,如有必要將加入性能監(jiān)控的邏輯。
第 2 層(或 2 層以上):也稱為 C2 編譯逝撬,也是將字節(jié)碼編譯為本地代碼浴骂,但是會啟用一些編譯耗時(shí)較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化宪潮。
實(shí)施分層編譯后溯警,Client Compiler 和 Server Compiler 將會同時(shí)工作,許多代碼都可能會被多次編譯看狡相,用 Client Compiler 獲取更高的編譯速度梯轻,用 Server Compiler 獲取更好的編譯質(zhì)量,在解釋執(zhí)行的時(shí)候也無須再承擔(dān)收集性能監(jiān)控信息的任務(wù)尽棕。
編譯優(yōu)化技術(shù)
Java 程序員有一個(gè)共識喳挑,以編譯方式執(zhí)行本地代碼比解釋執(zhí)行方式更快,之所以有這樣的共識滔悉,除去虛擬機(jī)解釋執(zhí)行字節(jié)碼時(shí)額外消耗時(shí)間的原因外伊诵,還有一個(gè)重要的原因就是虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)幾乎把對代碼的所有優(yōu)化措施都集中在了即時(shí)編譯器中,因此一般來說回官,即時(shí)編譯器產(chǎn)生的本地代碼會比 javac 產(chǎn)生的字節(jié)碼更優(yōu)秀曹宴。以下是具有代表性的 HotSpot 虛擬機(jī)的即時(shí)編譯器在生成代碼時(shí)采用的代碼優(yōu)化技術(shù):
- 語言無關(guān)的經(jīng)典優(yōu)化技術(shù)之一:公共子表達(dá)式消除
如果一個(gè)表達(dá)式 E 已經(jīng)計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在 E 中所有變量的值都沒有發(fā)生變化歉提,那么 E 的這次出現(xiàn)就成為了公共子表達(dá)式笛坦。對于這種表達(dá)式,沒必要花時(shí)間再對它進(jìn)行計(jì)算苔巨,只需要直接使用前面計(jì)算過的表達(dá)式結(jié)果代替 E 就可以了弯屈。例子:int d = (c*b) * 12 + a + (a+ b * c) ->?int d = E * 12 + a + (a+ E)
- 語言相關(guān)的經(jīng)典優(yōu)化技術(shù)之一:數(shù)組范圍檢查消除
在 Java 語言中訪問數(shù)組元素的時(shí)候系統(tǒng)將會自動進(jìn)行上下界的范圍檢查,超出邊界會拋出異常恋拷。對于虛擬機(jī)的執(zhí)行子系統(tǒng)來說资厉,每次數(shù)組元素的讀寫都帶有一次隱含的條件判定操作,對于擁有大量數(shù)組訪問的程序代碼蔬顾,這無疑是一種性能負(fù)擔(dān)宴偿。Java 在編譯期根據(jù)數(shù)據(jù)流分析可以判定范圍進(jìn)而消除上下界檢查湘捎,節(jié)省多次的條件判斷操作。
- 最重要的優(yōu)化技術(shù)之一:方法內(nèi)聯(lián)
簡單的理解為把目標(biāo)方法的代碼“復(fù)制”到發(fā)起調(diào)用的方法中窄刘,消除一些無用的代碼窥妇。只是實(shí)際的 JVM 中的內(nèi)聯(lián)過程很復(fù)雜,在此不分析娩践。
- 最前沿的優(yōu)化技術(shù)之一:逃逸分析
逃逸分析的基本行為就是分析對象動態(tài)作用域:當(dāng)一個(gè)對象在方法中杯定義后活翩,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他方法中翻伺,稱為方法逃逸材泄。甚至可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實(shí)例變量吨岭,稱為線程逃逸拉宗。
如果能證明一個(gè)對象不會逃逸到方法或線程之外,也就是別的方法或線程無法通過任何途徑訪問到這個(gè)對象辣辫,則可以為這個(gè)變量進(jìn)行一些高效的優(yōu)化:
- 棧上分配:將不會逃逸的局部對象分配到棧上旦事,那對象就會隨著方法的結(jié)束而自動銷毀,減少垃圾收集系統(tǒng)的壓力急灭。
- 同步消除:如果該變量不會發(fā)生線程逃逸姐浮,也就是無法被其他線程訪問,那么對這個(gè)變量的讀寫就不存在競爭葬馋,可以將同步措施消除掉(同步是需要付出代價(jià)的)
- 標(biāo)量替換:標(biāo)量是指無法在分解的數(shù)據(jù)類型卖鲤,比如原始數(shù)據(jù)類型以及reference類型。而聚合量就是可繼續(xù)分解的点楼,比如 Java 中的對象。標(biāo)量替換如果一個(gè)對象不會被外部訪問白对,并且對象可以被拆散的話掠廓,真正執(zhí)行時(shí)可能不創(chuàng)建這個(gè)對象,而是直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來代替甩恼。這種方式不僅可以讓對象的成員變量在棧上分配和讀寫蟀瞧,還可以為后后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)建條件。