《深刻理解Java虛擬機》筆記四

大部份內容都是《深刻理解Java虛擬機上的內容》的總結,少部份內容是來自於網上或者本身的理解。讀完應該會把沒筆記的markdown文件放在 github上。

本部分筆記對應的是《深刻理解Java虛擬機》最後幾章。git

早期優化

編譯器

  1. 解析

    生成抽象語法樹的階段github

  2. 填充符號

    符號表是由一組符號地址和符號信息構成的表格編程

  3. 註解處理器

    提供一組插入式註解處理的標準API在編譯期間對註解進行處理,咱們能夠把它看作一組編譯器的插件,在這些插件裏面,能夠讀取,修改,添加抽象語法樹中的任意元素。若是這些插件在註解期間對語法書進行了修改,編譯器將會到解析及符號表填充的過程從新處理,知道全部插入式註解處理器都沒有再對語法樹進行修改成止,每一次循環稱爲一個Round。api

  4. 標註檢查
  5. 數據及控制流分析
  6. 解語法糖
  7. 生成字節碼

語法糖

泛型

public class GenericTypes{  
 public static void method(List<String> list){  
   
 }  
   
 public static void method(List<Integer> list){  
   
 }  
}

這段代碼不能被編譯,由於類型擦除後都變成原生類型List<E>。數組

public class GenericTypes{  
 public static int method(List<String> list){  
   
 }  
   
 public static String method(List<Integer> list){  
   
 }  
}

這段代碼是能夠被編譯運行的。方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名之中,因此返回值不參與重載選擇,可是在Class文件格式之中,只要描述符不是徹底一致的兩個方法就能夠共存。安全

晚期優化

HotSpot中有兩個編譯器,分別稱爲Client Compiler和Server Compiler。markdown

解釋器與編譯器

熱點代碼

  • 被屢次調用的方法
  • 被屢次執行的循環體

編譯器都是以整個方法做爲編譯對象多線程

對於方法調用的熱點數據斷定oop

  • 基於採樣的熱點數據探測(Sample Based Hot Spot Detection),採用這種方式的虛擬機會週期性的檢查各個線程的棧頂,若是發現某個線程方法常常出如今棧頂,就編譯。
  • 基於計數器的熱點探測(Count Based Hot Spot Detection),採用這種方法的虛擬機會爲每一個方法創建一個計數器,統計方法的執行次數,若是執行次數超過必定的闕值,就編譯。優化

    • 方法計數器,記錄方法的相對頻率,一段時間以內方法被調用的次數。當超過必定的時間限度,若是方法的調用仍然不足以讓它提交給編譯器,這個方法的調用計數器就會被減小一半,這個過程被稱爲方法調用計數器熱度的衰減(Count Decay),這段時間就被稱爲半衰週期(Counter Half Life Time)
    • 回邊計數器,統計一個方法中循環體代碼的執行次數,當初過必定闕值就會觸發編譯。

逃逸分析

逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞到其餘方法中稱爲方法逃逸。若是被外部線程訪問,稱爲線程逃逸。

  • 棧上分配(Stack Allocation),Java堆中的對象對於各個線程都是共享可見的,只要持有這個線程的引用,就能夠訪問堆中存儲的對象數據。若是肯定一個對象不會逃逸出一個方法以外,那就讓這個對象在棧上分配,減小垃圾回收的壓力
  • 同步消除(Synchronization Elimination)若是一個變量不會逃逸出線程,那麼同步措施就能夠取消
  • 標量替換(Scalar Repalace)若是逃逸分析證實一個對象不會被外部訪問,而且這個對象,能夠被拆散的話,那麼程序真正執行的時候將可能不建立這個對象,而改成直接建立這個若干個被這個方法使用到的成員變量來代替。

Java內存模型與線程

Java內存模型

其實屏蔽的是操做系統的多級內存模型

主內存和工做內存

Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中,每一個線程還有本身的工做內存(Working Memory),線程的工做內存保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取,賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。

內存間交互操做

Java的內存模型中定義了8個操做來完成,虛擬機實現時必需要保證下面說起的每一種操做都是原子的。

  • lock(鎖定)做用於主內存的變量,它把一個變量標誌爲一條線程獨佔的狀態。
  • unlock(解鎖)做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放以後才能被其餘的線程鎖定。
  • read(讀取)做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
  • load(載入)做用於工做內存的變量,它把read操做從主內存中獲取獲得的變量值放入工做內存的副本中
  • use(使用)做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令將會執行這個操做。
  • assign(賦值)做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
  • store(存儲)做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
  • wirte(寫入)做用於主內存的變量,它把store操做從工做內存中獲得的變量值放入主內存的變量中。

基本規則

  • read和load,store和write必須順序執行,可是沒有保證連續執行
  • 變量在工做內存中改變了以後必須把該變化同步回主內存中
  • 線程不能無緣由(無assgin操做)把數據從線程的工做內存同步回主內存中
  • 一個變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化的變量。(必需要先申明?)
  • 一個變量在同一個時刻只容許被一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行好屢次,可是必需要執行相同次數unlock
  • 對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量
  • 若是沒有被lock操做鎖定,就不容許對它執行unlock操做
  • 對一個變量執行unlock操做以前,必需要此變量同步回主內存中

volatile

volatile,依然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來若是直接在主內存中讀寫訪問通常。

符合如下兩個規則,則能夠不加鎖

  • 運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值
  • 變量不須要與其餘的狀態變量共同參與不變的約束

可見性

當一個線程修改了這個變量的值,新值對於其餘線程是能夠當即獲得的。普通變量在線程間傳遞均須要經過主內存來完成。

禁止指令重排

先行原則

  1. 程序次序原則(應該是控制流順序)
  2. 管程鎖定原則
  3. volatile變量規則
  4. 線程啓動規則
  5. 線程終止規則
  6. 線程中斷規則
  7. 對象終結規則
  8. 傳遞性

Java與線程

線程的實現

內核線程實現

內核線程(Kernel-Level Thread)就是直接由操做系統內核支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器對線程進行調度,並負責將線程的任務映射各個處理器上。支持多線程的內核叫作多線程內核

輕量級進程(Light Weight Thread),輕量級進程就是一般意義上所講的線程,因爲每一個輕量級進程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。這種輕量級進程與內核之間1:1的關係稱爲一對一的線程模型。

會常常在用戶態和內核臺切換。

用戶線程實現

用戶線程指的是徹底創建用戶空間的線程庫上,系統內核不能感知線程的存在。

用戶線程加輕量級進程實現

內核線程和用戶線程一塊兒使用的方式

線程調度

協同式線程調度(Cooperative Threads-Scheduling)

線程的執行時間由線程自己控制,線程把本身的工做執行完成以後,要主動通知操做系統切換到另外一個線程

搶佔式線程調度(Preemptive Threads-Scheduling)

每一個線程將由操做系統來分配執行時間,線程切換不有線程自己來決定。Java使用的線程調度方式就是搶佔式調度。Java的線程是經過調用操做系統的api來實現的。因此Java提供的線程優先級也是要依賴於系統。

狀態轉換

就緒,運行,等待,阻塞,結束

線程安全與鎖優化

線程安全的級別

不可變

絕對線程安全

無論運行時環境如何,調用者都不須要任何額外的同步措施

相對線程安全

實現

互斥

synchronize的是可重入鎖

ReentrantLock

  1. 等待可中斷,當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待。
  2. 公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖
  3. 鎖能夠綁定多個條件

非阻塞同步

CAS

無同步方案

  • 可重入代碼(Reentrant Code),能夠在代碼執行的任什麼時候刻中斷,轉而去執行另一段代碼,而在控制權返回後,原來的程序不會出現任何錯誤。
  • 線程本地存儲(Thread Local Storage),ThreadLocal類

鎖優化

自旋鎖和自適應自旋

共享數據的鎖定狀態只會持續很短的一段時間,爲了這段時間去掛起和恢復線程並不值得。爲了讓線程等待,只須要讓線程執行一個忙循環體,這就是共享自旋鎖。

鎖消除

鎖消除是指在虛擬機即便編譯在運行時,對一些代碼上要求同步,可是檢測到不可能存在共享數據競爭的鎖進行消除。有的時候會編譯優化產生鎖

鎖粗化

對於一段代碼,在不一樣代碼塊反覆加鎖,不如直接給這段代碼加鎖。

輕量級鎖

HotSpot虛擬機的對象頭(Object Header)分爲兩部分信息,第一部分用於存儲對象自身的運行時數據,如HashCode,GC分代年齡(Generational GC age),這部分數據的長度在32位和64位的虛擬機分別爲32bit,64bit,官方稱爲「Mark Word」。另一部分用於存儲指向方法區對象類型數據的指針,若是是數組對象,還會有一個額外的部分存儲數組長度。

加鎖

在代碼進入同步塊的時候,若是此同步對象沒有被鎖定(鎖標誌位爲「01」),虛擬機首先將當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)空間,用於存儲對象目前的Mark Work的拷貝。

虛擬機將使用CAS才作嘗試將對象的Mark Word更新爲指向Lock Record的指針。若是更新成功,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌編程 「00」,表示此對象處於輕量級鎖定狀態

若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是,則說明當前線程已經擁有鎖了。若是不是那輕量級線鎖就再也不有效,要膨脹爲重量級鎖。鎖的標誌變爲「10」。

解鎖

經過CAS操做進行,若是對象的Mark Word仍然指向該線程的鎖記錄,那麼就用CAS操做把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,若是替換成功,同步過程就完成了。若是失敗,說明有其餘線程嘗試獲取該鎖,那就釋放該鎖的同時,喚醒被掛起的線程。

先用CAS看看能不能修改對象鎖的狀態,若是不能就用掛起和阻塞的方式去修改,以代表該對象是被鎖定的。從輕量級鎖膨脹爲重量級鎖。

偏向鎖

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲「01」,即爲偏向模式。同時使用CAS操做把獲取到這個鎖的ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每一次進入這個鎖相關的同步塊,都不須要進行任何同步操做。

當另一個線程去嘗試獲取這個鎖的時候,偏向模式結束,後續按照輕量級鎖的方式進行處理。

若是隻有一個線程進入臨界區,那麼就是偏向鎖。若是有第二線程想要進入,那麼就會鎖膨脹爲輕量級鎖。
相關文章
相關標籤/搜索