Java 虛擬機器總結給面試的你(下)

Hugo_Gao發表於2019-02-23

本篇部落格主要針對Java虛擬機器的晚期編譯優化,Java記憶體模型與執行緒,執行緒安全與鎖優化進行總結,其餘部分總結請點選Java虛擬總結上篇Java虛擬機器總結中篇

一.晚期執行期優化

即時編譯器JIT

即時編譯器JIT的作用就是熱點程式碼轉換為平臺相關的機器碼,並進行優化,它並不是一個虛擬機器所必須的部分,只能說有它是錦上添花。

熱點程式碼

熱點程式碼分類

  • 被多次呼叫的方法
  • 被多次呼叫的迴圈體

熱點探測判定方法

  • 基於取樣的熱點探測,虛擬機器週期性地檢查棧頂,發現某個方法經常出現在棧頂,那麼這個方法就是熱點方法,簡單高效但不精確
  • 基於計數器熱點探測,為每個方法建立計數器來統計執行次數,超過閾值就是熱點方法,Hotpot就是採用這種方法。分為方法計數器(統計方法),回邊計數器(統計迴圈)

編譯過程(Client Complier)

  • 第一階段
    • 將位元組碼構造成高階中間程式碼表示(HIR)
  • 第二階段
    • 將HIR變為LIR
  • 第三階段
    • 使用線性掃描演算法,在LIR上分配暫存器,產生機器程式碼

優化方法

公共子表示式優化

當一個表示式A的結果已經計算過了,且A中的所有變數都沒有發生過變化,那麼下一次要用到A時就不用計算了,而是直接取之前A的結果。

陣列邊界檢查消除

方法內聯

逃逸分析

逃逸的定義:一個在方法裡定義的變數,作為引數傳遞給其他方法(方法逃逸),或者賦值給類變數(執行緒逃逸)。

優化方法:

  • 棧上分配:不會逃逸的物件就不在堆上分配了,就在棧上分配,那麼物件所佔的空間就可以隨棧幀的出棧而銷燬,減少垃圾收集系統的壓力。
  • 同步消除:如果一個變數肯定不會逃逸出執行緒,那麼關於這個變數的同步措施就可以去掉。

二.Java記憶體模型與執行緒

記憶體模型

說了這麼多的記憶體模型,到底什麼是記憶體模型呢?

特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的過程抽象。

它的作用是定義程式中各個共享的變數的訪問規則,即如何將變數寫入記憶體和從記憶體中取出變數。Java記憶體模型有主記憶體與工作記憶體之分,所有變數存在主記憶體中,執行緒則是擁有自己的工作記憶體,它是主記憶體的副本拷貝,執行緒只能讀寫工作記憶體。

8種原子操作

  1. lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
  2. unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  3. read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的 load 動作使用。
  4. load(載入):作用於工作記憶體的變數,它把 read 操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  5. use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。
  6. assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  7. store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的 write 操作使用。
  8. write(寫入):作用於主記憶體的變數,它把 store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中。

volatile變數的特殊規則

volatile的特性是保證此變數對所有執行緒的可見性,即當變數的值修改後,其他執行緒可以立即知道發生的變化。普通變數則是修改完值後,需要寫回主記憶體,然後其他執行緒再從主記憶體讀取該資料。volatile還可以通過記憶體屏障來禁止指令的重排序。綜合來講它的讀操作和普通變數差不多,寫操作慢一點。

long和double變數的特殊規則

8種操作一般都是原子性的,但是對於64位的資料,記憶體模型允許將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作進行—->非原子協定但一般我們不需要將long和double宣告為volatile。

先行發生原則

  • 程式次序規則
  • 管程鎖定規則
  • volatile變數規則
  • 執行緒啟動規則
  • 執行緒終止規則
  • 執行緒中斷規則
  • 物件終結規則
  • 傳遞性

Java與執行緒

Java的Thread類大多API都是Native方法,是與平臺相關的。

實現執行緒的三種方式

  • 使用核心執行緒實現:核心執行緒即直接由作業系統核心支援的執行緒,由核心來完成執行緒切換,程式使用輕量級程式介面與核心執行緒一對一的關係,核心執行緒再經由執行緒排程器分派給CPU。
  • 使用使用者執行緒實現:使用者執行緒的建立同步銷燬排程完全在使用者態中完成,不需切換到核心態,一對多的關係。
  • 使用者執行緒+輕量級程式:多對多的關係。

執行緒的排程

  • 協同式排程
    • 執行緒的執行時間由執行緒自己控制,執行完後再主動通知系統切換執行緒,可能會導致一個執行緒長時間地阻塞
  • 搶佔式排程
    • 由系統分配時間,執行緒可以主動讓出時間但是不能主動獲得時間,通過設定優先順序確定順序

執行緒的狀態

  • 新建:剛剛建立還未啟動
  • 執行:正在執行或者等待分配時間
  • 無限等待:不會被CPU分配時間,需要其他執行緒顯式喚醒
  • 有限等待:在一段時間後由系統自動喚醒
  • 阻塞:等待一個排他鎖
  • 結束

三.執行緒安全與鎖優化

執行緒安全的程度,依次減弱

  • 不可變,將物件中帶狀態的變數都置為final
  • 絕對執行緒安全,完全符合執行緒安全定義
  • 相對執行緒安全,對這個物件的單獨的操作是執行緒安全的,如Vector,HashTable等
  • 執行緒相容,物件本身不是執行緒安全的,但是可以在呼叫端正確地使用同步手段才能保證在併發環境下正常使用。
  • 執行緒對立,無論呼叫端如何努力,都不可能實現執行緒安全

執行緒安全的實現方法

  1. 互斥同步

    synchronized關鍵字會在程式碼塊的前後分別形成monitorenter和monitorexit指令,這兩個指令需要一個reference物件引數,該鎖有一個計數器以實現同步,進入時將計數器+1,退出時-1,本執行緒可重入,其他執行緒需阻塞等待。synchronized的缺點是由於Java執行緒是對映到作業系統的,所以喚醒阻塞一個執行緒都需要系統幫忙,需要從使用者態轉到核心態,耗費很多處理器時間。

    ReentrantLock對synchronized的優勢:

    • 等待可中斷
    • 公平鎖:必須按照申請鎖的時間順序來一次獲得鎖
    • 鎖繫結多個條件
  2. 非阻塞同步

    為了解決執行緒阻塞和喚醒所帶來的效能問題,先對共享資料進行操作,如果沒有競爭就成功了,否則就補償(不斷重試直到成功)

  3. 無同步方案

    • 可重入程式碼
    • 執行緒本地儲存,把共享資料的範圍限制到執行緒內,ThreadLocalMap以ThreadLocalHashMap為鍵,以本地執行緒變數為值的K-V對

鎖優化

鎖優化的方案有以下幾種:

  • 自旋鎖:為了減少執行緒阻塞與喚醒的消耗,執行緒在被阻塞時可以執行一個忙迴圈(自旋)
  • 鎖消除:對不存在共享資料競爭的鎖進行消除
  • 鎖粗化:在一個程式碼塊內對一個物件連續的地加鎖解鎖,就對整個程式碼塊一次性加鎖減少效能損耗
  • 輕量級鎖:無競爭地情況下使用CAS操作去消除同步使用地互斥量
  • 偏向鎖:鎖會偏向於第一個獲得它地執行緒

相關文章