1. 程式人生 > 程式設計 >深入理解Java虛擬機器器鎖優化&逃逸分析技術

深入理解Java虛擬機器器鎖優化&逃逸分析技術

引言

HotSpot虛擬機器器團隊在1.5 -> 1.6版本演進中,進行了大量的鎖優化技術,相應的jdk6併發包也推出了很多併發容器&API,所以JDK6是高效併發大放異彩的一個關鍵版本。本文主要介紹一下java虛擬機器器中對於鎖的優化技術、逃逸分析技術。

鎖優化:適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等

逃逸分析:棧上分配、同步消除、標量替換等

理論基礎

在進行鎖優化介紹&逃逸分析介紹之前,先回顧一下以下基礎概念,是有必要的。

·synchronized同步方法基於ACC_SYNCHRONIZED關鍵字隱式對方法進行加鎖,同步程式碼塊基於monitorenter&monitorexit進行加鎖與釋放鎖。在鎖優化技術還未成熟之前,synchronized實現是直接通過ObjectMonitor呼叫enter&exit進行“重量級鎖”操作。

·物件頭中主要包含了GC分代年齡、鎖狀態標記、雜湊碼、epoch等資訊。物件的狀態一共有五種,分別是無鎖態、輕量級鎖、重量級鎖、GC標記和偏向鎖

·在HotSpot虛擬機器器中,使用oop-klass模型來表示物件


圖轉自https://blog.csdn.net/linxdcn/article/details/73287490

·執行緒五種基本狀態,鎖與執行緒&執行緒狀態的切換息息相關。

對於執行緒來說,一共有五種狀態,分別為:初始狀態(New) 、就緒狀態(Runnable) 、執行狀態(Running) 、阻塞狀態(Blocked) 和死亡狀態(Dead) 。


圖轉自https://www.cnblogs.com/aspirant/p/8900276.html

·java多執行緒模型

·使用核心執行緒 1:1

·使用使用者執行緒 1:n

·使用使用者執行緒加輕量級程式混合實現 m&n


鎖優化

自旋鎖

基於java多執行緒模型、執行緒狀態切換與互斥原理,可以得知對效能最大的影響是阻塞的實現,掛起和恢復執行緒需要對映到OS核心態中完成,同時,虛擬機器器團隊發現在許多應用上,共享資料的鎖定狀態只會持續很短一段時間,為了這段時間去掛起和恢復執行緒很沒有價效比。如果物理機有一個以上處理器,可以實現並行操作,那麼我們就可以讓後面請求鎖的那個執行緒'稍等一下',但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。這個“稍微等一下”的過程就是自旋。

自旋鎖在JDK1.4已經引入進來,但是預設關閉,JDK6預設開啟。

在JDK1.4版本,可以通過引數-XX:UseSpinning選擇開啟自旋,-XX:PreBlockSpin來更改自旋的時間,預設值是10次。

自適應自旋鎖

JDK6引入了自適應的自旋鎖,於是我們不需要再固定指定一個自旋時間,虛擬機器器會有演演算法策略智慧的選擇時間,隨著程式執行和效能監控資訊不斷完善,虛擬機器器的預測會越來越精準,越來越'聰明'。

自旋與阻塞的區別在於是否放棄處理器的執行時間,自旋比較適合競爭不激烈,並且保持鎖的時間短的場景。


鎖消除

鎖消除是JIT編譯器優化功能之一,也是逃逸分析下面要講到的。

顧名思義,就是JIT編譯同步程式碼塊的時候,會使用逃逸分析技術來判斷當前程式碼塊,是否是區域性變數等,只會被一個執行緒訪問到。打個比方



在上面程式碼塊中,StringBuffer內部是synchronized修飾的,但是它在程式碼塊中屬於區域性變數,是執行緒私有的。還有例子2,也是區域性變數,所以這種情況,JIT會幫我們進行優化,進行鎖消除,加鎖完全沒有意義。

由於synchronized是基於moniotor指令實現的,可能有人就想javap試一下是不是真的鎖消除了,這裡需要提一下,JIT編譯階段的優化,javap無法檢視具體結果,如果讀者感興趣,還是可以看的,只是會複雜一點,首先你要自己build一個fasttest版本的jdk,然後在使用java命令對.class檔案進行執行的時候加上-XX:+PrintEliminateLocks引數。而且jdk的模式還必須是server模式。


鎖粗化

鎖的細化老生常談的問題了,在使用鎖的時候,控制粒度有利於提升效能,在真正競態條件發生的地區才用鎖。

那為什麼會有鎖粗化這一說呢,很簡單,同樣的道理我們一定有經驗且碰到過。

在你寫一段try catch異常處理的時候,如果程式碼塊中有迴圈操作,你會把try catch寫在迴圈裡面還是迴圈外面?



所以這就是鎖粗化關注的點了,當JIT發現一系列連續的操作都對同一個物件反覆加鎖和解鎖,甚至加鎖操作出現在迴圈體中的時候,會將加鎖同步的範圍擴散(粗化)到整個操作序列的外部。


輕量級鎖

輕量級鎖是JDK 1.6之中加入的新型鎖機制,它名字中的“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。 首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。

在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為“01”狀態),虛擬機器器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),這時候執行緒堆疊與物件頭的狀態如下圖所示:



然後,虛擬機器器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變為“00”,即表示此物件處於輕量級鎖定狀態,這時候執行緒堆疊與物件頭的狀態如下圖所示:



如果這個更新操作失敗了,虛擬機器器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果只說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖物件已經被其他執行緒搶佔了。 如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。

上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進行的,如果物件的Mark Word仍然指向著執行緒的鎖記錄,那就用CAS操作把物件當前的Mark Word和執行緒中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。 如果替換失敗,說明有其他執行緒嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的執行緒。

輕量級鎖能提升程式同步效能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料。 如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。


偏向鎖

偏向鎖也是JDK 1.6中引入的一項鎖優化,它的目的是消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。 如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。

偏向鎖的“偏”,就是偏心的“偏”、 偏袒的“偏”,它的意思是這個鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。

如果讀懂了前面輕量級鎖中關於物件頭Mark Word與執行緒之間的操作過程,那偏向鎖的原理理解起來就會很簡單。 假設當前虛擬機器器啟用了偏向鎖(啟用引數-XX:+UseBiasedLocking,這是JDK 1.6的預設值),那麼,當鎖物件第一次被執行緒獲取的時候,虛擬機器器將會把物件頭中的標誌位設為“01”,即偏向模式。 同時使用CAS操作把獲取到這個鎖的執行緒的ID記錄在物件的Mark Word之中,如果CAS操作成功,持有偏向鎖的執行緒以後每次進入這個鎖相關的同步塊時,虛擬機器器都可以不再進行任何同步操作(例如Locking、 Unlocking及對Mark Word的Update等)。

當有另外一個執行緒去嘗試獲取這個鎖時,偏向模式就宣告結束。 根據鎖物件目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位為“01”)或輕量級鎖定(標誌位為“00”)的狀態,後續的同步操作就如上面介紹的輕量級鎖那樣執行。 偏向鎖、 輕量級鎖的狀態轉化及物件Mark Word的關係如下圖所示:



偏向鎖可以提高帶有同步但無競爭的程式效能。 它同樣是一個帶有效益權衡(Trade Off)性質的優化,也就是說,它並不一定總是對程式執行有利,如果程式中大多數的鎖總是被多個不同的執行緒訪問,那偏向模式就是多餘的。 在具體問題具體分析的前提下,有時候使用引數-XX:-UseBiasedLocking來禁止偏向鎖優化反而可以提升效能。


逃逸分析

棧上分配

顧名思義,當虛擬機器器確定一個物件不會逃逸出方法之外,那麼讓物件在棧上分配記憶體,物件所佔用的記憶體空間就可以隨著棧幀的出棧而銷燬

我們先來看一段程式碼的測試結果



我們使用上述jvm引數(開啟逃逸分析,關閉TLAB優化,分配1g堆記憶體,開啟gc日誌列印),執行以上程式碼


再來調整一下jvm引數(僅關閉逃逸分析),重新執行一次



可以看到,逃逸分析在判定區域性變數testObject不會逃逸出當前方法作用域以後,會進行棧上分配優化,但是由於我jdk使用的是混合模式,所以還是有gc日誌列印。


mixed mode代表混合模式

在Hotspot中採用的是直譯器和編譯器並行的架構,所謂的混合模式就是直譯器和編譯器搭配使用,當程式啟動初期,採用直譯器執行(同時會記錄相關的資料,比如函式的呼叫次數,迴圈語句執行次數),節省編譯的時間。在使用直譯器執行期間,記錄的函式執行的資料,通過這些資料發現某些程式碼是熱點程式碼,採用編譯器對熱點程式碼進行編譯,以及優化(逃逸分析就是其中一種優化技術)。


標量替換

標量(Scalar)是指一個無法再分解成更小的資料的資料。Java中的原始資料型別就是標量。相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中的物件就是聚合量,因為他可以分解成其他聚合量和標量。

在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那麼經過JIT優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來代替。這個過程就是標量替換。

還是上面的例子,我們再調整一下jvm引數(開啟逃逸分析、關閉TLAB、關閉標量替換)



上面的例子說明,棧上分配是通過標量替換實現的。

鎖消除

同虛擬機器器鎖優化中的鎖消除。


總結

Java虛擬機器器遮蔽了與具體作業系統平臺相關的資訊之外,還為程式設計師做了很多優化,除了本文提到的優化之外,還有公共子表示式消除、陣列邊界檢查消除、方法內聯、記憶體及程式碼位置變換優化、TLAB、PLAB等優化技術,讀者有興趣可深入研究


參考:

<深入理解java虛擬機器器>

<Java虛擬機器器規範第2版>

<Hotspot實戰>

https://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%A4%9A%E7%BA%BF%E7%A8%8B

www.jianshu.com/p/04fcd0ea5…

blog.csdn.net/hollis_chua…