(學習總結)JDK源碼解析

目錄

一、Jdk源碼解析過程

二、java虛擬機運行時數據區

1、Java虛擬機的五大分區

三、OutOfMemory異常實踐(OOM)

 1、Java堆溢出

2、虛擬機棧和本地方法棧溢出

3、方法區和運行時常量池溢出

4、本機直接內存溢出

四、垃圾回收

(1)堆的回收

(2)方法區的回收

(3)垃圾回收算法

(4)如何回收 

(5)垃圾收集器(七種)

(6)內存分配回收策略

五、類(class)文件結構

1、Class文件的結構

六、java虛擬機類加載機制

1、虛擬機的啓動

2、類加載的時機(類加載的過程)

3、類加載的過程

4、類加載器

七、早期優化(編譯器的優化)

1、解析與填充符號表

2、註解處理器:對註解處理,JDK1.5之後。

3、語義分析和字節碼生成

4、語法糖的應用

八、晚期優化(運行期的優化)

1、Hotspot的即時編譯器

2、編譯對象和觸發條件

3、編譯過程

九、Java內存模型

1、主內存和工作內存

2、Java與線程

3、Java線程調度

4、Java線程狀態轉化

十、線程安全與鎖優化

1、Java語言中的線程安全

2、線程安全的實現方式

3、鎖優化


一、Jdk源碼解析過程

  1. Jdk(java開發工具包)-》jre(java運行時環境)-》jvm(java虛擬機)

二、java虛擬機運行時數據區

1、Java虛擬機的五大分區

首先來看一個圖:

Java虛擬機的五大分區爲:

  1. 方法區
  2. 虛擬機棧
  3. 本地方法棧
  4. 程序計數器

下面來一一介紹

程序計數器(PC寄存器)

可以理解爲當前線程所執行的字節碼的指示器。

是線程私有的,每條jvm線程都有自己的程序計數器,各條線程互不影響,獨立存儲,是“線程私有”內存。

Java虛擬機棧

線程私有的,每個jvm線程都有自己的java虛擬機棧,與線程同時創建,生命週期和線程相同。

虛擬機描述的是java方法執行的內存模型

每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

本地方法棧

和虛擬機棧作用相似,虛擬機棧爲虛擬機執行的java方法服務,本地方法棧爲虛擬機用到的Native方法服務。

線程私有的

HotSpot直接把java虛擬機棧和本地方法棧合二爲一。

Java堆

是虛擬機內存中最大的一部分

用來存儲對象實例,所有對象技術組都要在這裏分配內存

線程共享

虛擬機創建的時候創建

由於這塊區域是線程共享的,裏面存的數據不能隨線程消亡而刪除,所以這裏的存儲的對象實例以及數組要在這裏被自動管理,也就是垃圾回收(GC)(Garbage Collector)。

方法區

方法區也是一塊被各個線程共享的區域。

存儲被虛擬機加載的類信息,常量,靜態變量。

在虛擬機啓動的時候被創建

運行時常量池

是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。

直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常出現。

類似於一個緩存區,避免了在Java堆和Native堆中來回複製數據。

三、OutOfMemory異常實踐(OOM)

 1、Java堆溢出

當出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟着進一步提示“Java heap space”。

內存泄露:是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

2、虛擬機棧和本地方法棧溢出

(1)單線程:

①如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常 

②如果虛擬機在擴展時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常

(2)如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程了。如果沒有這方面的處理經驗,這種通過“減少內存”的手段來解決內存溢出的方式會比較難以想到。

3、方法區和運行時常量池溢出

運行時常量池溢出,在OutOfMemoryError後面跟隨的提示信息是“PermGen space”

4、本機直接內存溢出

由DirectMemory導致的內存溢出,一個明顯的特徵是在Heap Dump文件中不會看見明顯的異常,如果發現OOM之後文件很小,而程序中有直接或簡介使用了NIO,那就可以考慮一下是不是這方面的原因。

四、垃圾回收

由於程序計數器、虛擬機棧、本地方法棧三個區域隨線程而生,隨線程而滅,棧幀隨方法進行入棧出棧操作。所以的這些區域的內存分配和回收具有確定性,不考慮垃圾回收問題。內存會隨線程的結束而自動回收。

堆和方法區不一樣,我們只有在程序運行期間才知道會創建哪些對象,這部分的內存分配和回收是動態的,垃圾回收關注的是這部分內存。

(1)堆的回收

  1. 死掉的對象所佔內存需要回收
  2. 判斷對象是否死掉

1)引用計數法

  1. 每當有一個地方需要使用這個對象時,計數器加一,當引用失效的時候,計數器減一,任何時刻,計數器爲0的對象都不會再被使用。
  2. Java中沒有使用引用計數法,因爲很難解決對象之間循環引用的問題。

2)可達性分析算法或者根搜索算法

  1. 即判定對象是否存活,即從“GC Roots”對象作爲起始點,從這些節點向下搜索,搜索走過的路徑叫做“引用鏈”,當一個對象到GC Roots 沒有任何引用鏈,證明此對象是不可用的。
  2. 在Java語言中,可作爲GC Roots的對象包括下面幾種:
  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

3)對象的引用

  1. JDK1.2之前,認爲如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表着一個引用,這種太過狹隘。
  2. 在JDK 1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,這四種引用強度依次逐漸減弱。
    1. 強引用:類似於“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
    2. 軟引用:軟引用用來描述一些還有用,但並非必需的對象。表示在java堆裏面沒有數據,但是在棧和方法區中有數據,比如:Object object= null;
    3. 弱引用:弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。所以弱引用的對象無論內存是否足夠,都會被回收。例如:局部變量,返回值,參數。
    4. 虛引用:虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到一個系統通知。比如:反射獲取的對象,註解。

(2)方法區的回收

  • 永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。
  • 很多人認爲方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
  • 無用的類是指:
    1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
    2. 加載該類的ClassLoader已經被回收。
    3. 該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

(3)垃圾回收算法

  • 標記-清除算法(Mark-Sweep)
  1. 首先標記出所有需要清除的對象,標記完了統一回收被標記的對象。
  2. 主要缺點:
    1. 效率問題:要遍歷兩次
    2. 空間問題:產生大量不連續的內存碎片,使得程序在需要分配比較大的對象的時候無法找到連續的內存而不得不提前觸發一次垃圾回收。

  • 複製算法
  1. 爲了解決效率問題
  2. 將內存分爲大小相同的兩塊,每次只使用其中的的一塊內存,當這一塊內存使用完了,將這塊內存上有用的對象複製到另一塊上,然後把這塊內存所有的數據都清空。
  3. 優點:實現簡單,運行高效
  4. 缺點:內存縮小爲原來的一半,代價高。
  5. 用來回收新生代,伊甸園區和兩個倖存者區是8:1:1的關係。

  • 標記-整理算法
  1. 標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
  2. 老年代使用標記整理算法。
  • 分代收集算法
  1. 一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
  2. 新生代--複製算法。老年代--標記-整理算法。

(4)如何回收 

枚舉根節點:一次枚舉根節點,數量龐大,逐個檢查,會消耗很多時間。

安全點:由於GC必須停止進程,安全點就是,可以進行GC的點

安全區域:可以隨時進行GC的區域

(5)垃圾收集器(七種)

  • Serial收集器(在GC日誌中新生代的名稱是DefNew)
  • ParNew收集器(在GC日誌中新生代的名稱是ParNew)
  • Parallel Scavenge收集器(在GC日誌中新生代的名稱是PSYongGen)
  • Serial Old(MSC)收集器
  • Parallel Old收集器
  • CMS收集器
  • G1收集器

(6)內存分配回收策略

  • Java內存回收機制吧java堆分爲新生代和老年代。
  • 新生代分爲伊甸園區(Eden),兩個倖存者區(server),大小爲8:1:1,對象優先分配在新生代的伊甸園區,當內存空間不夠的時候,將在新生代發生一次GC(Minior GC),此時將伊甸園區的對象移動至倖存者一區,然後在兩個倖存者去進行復制算法,將倖存者一區存活下來的對象賦值到倖存者二區中,再將倖存者一區清空,完成一次minior GC,當倖存者二區滿了後,再將倖存者二區中存活的對象複製到倖存者一區中,將倖存者二區清空。如此往復循環,不斷進行GC。
  • 進入老年帶的對象
    1. 大對象直接進入老年代
    2. 長期存活的對象將進入老年代(默認年齡是15)
    3. 動態對象年齡判定:爲了能更好地適應不同程序的內存狀況,虛擬機並不總是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
    4. 空間分配擔保:在發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於,則改爲直接進行一次Full GC。如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗;如果允許,那隻會進行Minor GC;如果不允許,則也要改爲進行一次Full GC。

五、類(class)文件結構

1、Class文件的結構

  1. Class文件是一個由二進制字節組成的文件。
  2. 他的文件結構是:
  3. 下面詳細介紹結構體各部分詳細介紹
  • Magic
    • 固定值,確定這個文件是否爲一個能被虛擬機所接受的 Class 文件。
  • minor_version、major_version
    • 不同版本的java虛擬機支持的版本不一樣。
    • 高版本jvm向下兼容低版本的class文件。
  • 常量池計數器
    • 有幾個常量池,就是幾。
    • constant_pool 表的索引值只有在大於 0 且小於 constant_pool_count 時纔會被認爲是有效的。
    • 使用索引 0 來表示“不引用任何一個常量池項”的意思。
  • constant_pool[ ]
    • 常量池:它包含 Class 文件結構及其子結構中引用的所有字符串常量、類或接口名、字段名和其它常量。
  • access_flags
    • 用於表示某個類或者接口的訪問權限及基礎屬性
  •  this_class
    • 表示這個 Class 文件所定義的類或接口
  • super_class
    • 表示這個class文件的父類或父接口
  • interfaces_count
    • 表示當前類或接口的直接父接口數量。
  •  interfaces[]
    • 表示這個類實現的接口(順序:從左到右)。
  • fields_count
    • 類或接口的成員的個數
  • fields[]
    • 字段表,fields[]數組描述當前類或接口聲明的所有字段,但不包括從父類或父接口繼承的部分。
  • methods_count
    • 方法計數器,方法的個數。
  • methods[]
    • 方法表,methods[]數組,只描述當前類或接口中聲明的方法,不包括從父類或父接口繼承的方法。
  • attributes_count
    • 屬性計數器,attributes_count 的值表示當前 Class 文件 attributes 表的成員個數。
  • attributes[]
    • 屬性表

六、java虛擬機類加載機制

虛擬機的類加載機制,虛擬機吧描述類的文件從class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終形成可以被java虛擬機直接使用的java類。

說白了,就是把編譯好的class文件加載到方法區中的各個部分,並且進行校驗。

1、虛擬機的啓動

Java虛擬機的啓動是有一個引導類加載器創建一個初始類來完成。

2、類加載的時機(類加載的過程)

(1)類從被加載到虛擬機內存中到卸載出內存,整個生命週期:加載、連接、初始化、使用、卸載。其中連接分爲驗證、準備、解析。解析的順序不一定,有可能按照上述順序,也有可能在初始化階段之後纔開始,這是爲了支持Java的運行時綁定(動態綁定)。如下圖:

(2)對類進行初始化的時機:

  • 使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  • 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化。則需要先觸發其初始化

3、類加載的過程

(1)創建和加載

①Java 虛擬機支持兩種類加載器:Java 虛擬機提供的引導類加載器(Bootstrap Class Loader)和用戶自定義類加載器(User-Defined Class Loader)。

②加載需要完成:

1、通過類的完全限定名獲取類的二進制字節流文件

也就是根據類名,獲取class文件

2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構

也就是將class文件轉化爲方法區中jvm可以識別的數據結構

3、在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

也就是說,生成了一個類的訪問接口,class對象,記錄了類成員,接口等信息,那麼可以推測,是不是可以通過反射機制獲取類信息了。

(2)連接和驗證

①驗證保證類或接口的二進制表示結構上是正確的。

②驗證過程可能會導致某些額外的類和接口被加載進來(§5.3),但不應該會導致它們也需要驗證或準備。

③驗證階段大體會進行如下階段的檢查:

  • 文件格式驗證:class文件是否符合規範
  • 元數據驗證:是否存在不符合java語言規範的元數據信息
  • 字節碼驗證:保證驗證類不會做出危害虛擬機安全的事件
  • 符號引用驗證:確保後續解析階段符號引用正確

(3)連接和準備

  • 正式爲類分配內存和變量初始化階段,這裏進行分配內存的僅僅包括類變量

(4)連接和解析

  • 將符號引用解析爲直接引用。

符號引用是指用符號來描述引用目標,符號可以是任何字面量。

直接引用,可以理解爲引用他的內存地址

  1. 類與接口解析:jvm解析一個類
  2. 字段解析:jvm解析一個類的字段
  3. 類方法解析:jvm解析一個類方法:拋出異常:java.lang.IncompatibleClassChangeError,java.lang.AbstractMethodError,java.lang.NoSuchMethodError.的時機。
  4. 接口方法解析:

(5)初始化:初始一個類。

  • 在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃其餘初始化變量和其他資源,或者從另一個角度來表達,初始化階段是執行類構造器<clinit>()方法的過程:

(6)使用

(7)退出

  • 某些線程調用 Runtime 類或 System 類的 exit 方法,或是 Runtime 類的 halt 方法,並且 Java 安全管理器也允許這些 exit 或 halt 操作。

4、類加載器

  1. 雙親委派機制,要加載一個類,優先於扔給他的父類的類加載器來加載,如果父類無法完成,纔會使用子加載器來自己加載。
  2. 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載器請求最終都是應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需要的類)時,子加載器纔會嘗試自己去加載。
  3. “雙親委派機制”是一種設計模式(代理模式)

七、早期優化(編譯器的優化)

編譯器的優化是在生成class文件的編譯過程中的優化,發生在編譯器上,生成class文件的時候。

Javac編譯器的編譯過程,基本可以按照下圖來表示。

1、解析與填充符號表

(1)詞法,語法分析

  • 語法分析是根據 Token 序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序代碼中的一個語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等都可以是一個語法結構。

(2)填充符號表

  • 符號表中所登記的信息在編譯的不同階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

2、註解處理器:對註解處理,JDK1.5之後。

3、語義分析和字節碼生成

  1. 標註檢查:檢查的內容包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。常量摺疊。
  2. 數據及控制流分析:對程序上下文邏輯更進一步的驗證,它可以檢測出諸如程序局部變量是在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。
  3. 解語法糖:
    • 指在計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用。通常來說,使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。
    • 其實自動拆箱,裝箱,泛型,都是語法糖。
  4. 字節碼生成:
    • 在 Javac 源碼裏面由com.sun.tools.javac.jvm.Gen 類來完成
    • 生成class文件

4、語法糖的應用

  1. 泛型和泛型擦除,
  2. 自動裝箱,自動拆箱,循環遍歷
  3. 條件編譯

八、晚期優化(運行期的優化)

Java程序最初是解釋執行的,後來,發現某個方法使用的非常頻繁,就會認爲這段代碼是熱點代碼,運行時,會把這些代碼編譯成平臺相關的代碼,並進行各種層次的優化,完成這個任務的編譯器爲即時編譯器(JIT)。

1、Hotspot的即時編譯器

(1)hotspot同時存在解釋器和編譯器,如下圖所示。

(2)HotSpot虛擬機中內置了兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,或者簡稱爲C1編譯器–Client 和C2編譯器– Server(也叫Opto編譯器)

2、編譯對象和觸發條件

(1)編譯對象是“熱點代碼”

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

(2)判斷一段代碼是不是“熱點代碼”的方法:

  • 基於採樣的熱點探測:就是每隔一段時間查看棧頂的方法(處於棧頂的方法就是當前正在執行的方法),如果一個方法經常出現在棧頂,那麼,就認爲這個方法是熱點方法。不過這個方法容易受到線程阻塞或其他的外界因素影響。
  • 基於計數器的熱點檢測:即爲每一個方法,或方法快,建立一個計數器,方法執行一次,計數器+1,超過一定的閾值就認爲這個方法是熱點方法。Hotspot是基於計數器的熱點檢測,爲每個方法準備了兩個計數器:
    1. 方法調用計數器
      1. 要執行一個方法,首先需要判斷他有沒有已編譯的版本,如果有,直接執行已編譯的代碼,如果沒有,方法計數器+1,如果兩個計數器之和超過了閾值,那麼,向編譯器提交編譯請求,並以解釋的方式完成本次的代碼執行。
      2. 方法調用計數器可以設置半衰週期,如果在一定的時間內,沒有達到閾值,就將方法調用計數器的值減半,這樣就保證了,只有在一定之間之內,調用夠一定次數的方法才能進行編譯執行。
    2. 回邊計數器
      1. 統計一個方法體重代碼的執行次數,遇到跳轉代碼,例如:continue,叫做回邊。
      2. 遇到回邊指令,首先需要判斷他有沒有已編譯的版本,如果有,直接執行已編譯的代碼,如果沒有,回邊計數器+1,如果兩個計數器之和超過了閾值,那麼,向編譯器提交編譯請求,並以解釋的方式完成本次的代碼執行。
      3. 回邊計數器沒有半衰週期。

 

 

 

3、編譯過程

  1. 默認設置下,無論是方法調用產生的即時編譯請求,還是OSR編譯請求,虛擬機在代碼編譯器還未完成之前,都仍然將按照解釋方式繼續執行,而編譯動作則在後臺的編譯線程中進行。
  2. 編譯過程中有各種編譯優化技術,這裏只說幾種:
    • 公共子表達式消除
      1. 如果一個表達式E已經計算過了,並且從先前的計算到現在E中所有變量的值都沒有發生變化,那麼E的這次出現就成爲了公共子表達式。
      2. 優化僅限於程序的基本塊內, 局部公共子表達式消除
      3. 優化的範圍涵蓋了多個基本塊, 全局公共子表達式消除
    • 數組邊界檢查消除
      1. 如果編譯器只要通過數據流分析就可以判定循環變量的取值範圍永遠在區間[0,foo.length)之內,那在整個循環中就可以把數組的上下界檢查消除,這可以節省很多次的條件判斷操作。
    • 方法內聯
      1. 方法內聯的優化行爲只是把目標方法的代碼“複製”到發起調用的方法之中,避免發生真實的方法調用而已。 (避免方法頻繁的出棧和入棧)
      2. 爲了解決虛方法的內聯問題,(虛方法:有方法體,但是裏面沒有東西;抽象方法:沒有方法體)
    • 逃逸分析
      1. 逃逸分析與類型繼承關係分析一樣,並不是直接優化代碼的手段,而是爲其他優化手段提供依據的分析技術。分爲:
        1. 方法逃逸:在一個線程內,方法的變量被外部引用了,發生了傳參。相當於參數的作用域大了,叫方法逃逸。
        2. 線程逃逸:一個線程調用另一個線程的一個帶參方法,那麼這個對象發生了線程逃逸。
      2. 基於逃逸分析的一些優化
        1. 棧上分配:不逃逸的對象,完全可以將他放在棧中,使之隨線程消失,減少GC壓力
        2. 同步消除:不逃逸的對象,在一個線程之內,不會出現不同步的問題,所以不逃逸的對象可以吧他涉及到的同步措施消除掉。
        3. 標量替換: 把一個聚合量(例如:對象),拆分爲不可拆分的標量(例如:int等),程序創建對象變爲創建一系列標量,可以讓對象在棧上分配空間,還可以爲進一步優化創建條件。

九、Java內存模型

Java虛擬機規範中試圖定義一種Java內存模型來屏蔽掉各種硬件和操作系統的內存差異,來實現Java程序在各種平臺下都能達到一致的內存訪問效果。即:一套完整的java內存使用規則,內存的規則。

保證了:環境一致 內存一致 訪問方式一致

1、主內存和工作內存

(1)這裏所講的主內存、工作內存與Java內存區域中的Java堆、棧、方法區等並不是同一個層次的內存劃分。

主內存主要對應java堆中的對象實例數據部分,而工作內存則對應於虛擬機棧中的部分數據。從更低層次上說,主內存就直接對應於物理硬件的內存。
爲了獲取更好的運行速度,虛擬機可能會讓工作內存優先存儲於寄存器和高速緩存中,因爲程序運行時主要訪問讀寫的是工作內存。

(2)內存之間的交互操作

①即,將一個數據在jvm中和硬盤中存入,取出的過程。見下圖:

②分析

  1. lock(鎖定):作用於主內存的變量,它把一個變量標誌爲一條線程獨佔的狀態。
  2. unlock(解鎖):作用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  3. read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用。
  4. load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  5. use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎。
  6. assign(賦值):作用於工作內存的變量,它把一個從執行引擎接受到的值賦給工作內存的變量。
  7. store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。
  8. write(寫入):作用於主內存中的變量,它把store操作從主內存中得到的變量值放入主內存的變量中。

③volatile(關鍵字)型變量的特殊規則

  1. 當一個變量定義爲volatile之後,它將具備兩種屬性,
  2. 第一種是保證此變量對所有線程的可見性,這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其它線程來說是可以立即得知的。
  3. 禁止指令重排序優化:
    1. 指令重排序優化:吧一塊代碼,切分爲幾個部分,分別在多個線程中執行,那麼,如果一個線程中的指令需要另一個線程中的值,那麼,需要等待那個線程先執行,然後再調用,最後在根據一定規則吧代碼塊從新組合。叫做指令重排序
    2. valotile關鍵字是如何實現禁止指令重排序呢?
      1. 有volatile關鍵字修飾的變量,賦值後會多執行一個操作,這個操作會把修改同步到內存,意味着所有之前的操作都執行完畢了,這個操作相當於內存屏障(Memory Barrier),這樣一來指令重排序的時候就不能把後面的指令重排序到內存屏障之前的位置。只有一個CPU訪問內存時,不需要內存屏障。

2、Java與線程

(1)內核線程

  • 內核線程(Kernel-Level Thread,KLT)就是直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操作系統調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。
  • 程序一般不直接使用內核線程,而是用他的高級接口,輕量級線程。
  • 這種輕量級進程(LMP)和內核線程(KLT)之間1:1的關係稱爲一對一線程模型。
  • 耗費資源,需要經常進行用戶態和內核態的轉化。

(2)用戶線程

  • 從廣義上來講,一個線程只要不是內核線程,就可以認爲是用戶線程,因此,從這個定義上來講,輕量級進程也屬於用戶線程,但輕量級進程的實現始終是建立在內核之上的,許多操作都進行系統調用,效率會受到限制。
  • 這種進程與用戶線程之間1:N的關係稱爲一對多的線程模型。
  • 不耗費資源,但是使用方便,線程的創建、切換和調度都是需要考慮的問題。

(3)用戶線程和輕量級線程混合使用

(4)Java線程的實現

Java線程在JDK 1.2之前是基於用戶線程實現的,而JDK 1.2中,線程模型替換爲基於操作系統原生線程來實現。後來變爲混合實現。

3、Java線程調度

搶佔式調度,但是可以設置優先級,因爲Java線程是通過映射到原生線程上來實現的,所以線程調度最終還是取決於操作系統。

4、Java線程狀態轉化

  • 新建(New):創建後尚未啓動的線程處於這種狀態。
  • 運行(Runable):Runable包括了操作系統線程狀態中的Running和Ready,也就是說處於此種狀態的線程可能正在執行,也可能正在等待CPU爲它分配執行時間。
  • 無限期等待(Waiting):處於這種狀態下的線程不會被分配CPU執行時間,他們要等待被其他線程顯示喚醒。
  • 限期等待(Timed Waiting):處於這種狀態下的線程也不會被分配CPU執行時間,不過無須等待被其他線程顯示喚醒,在一定時間之後它們由系統自動喚醒。
  • 阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待着獲取一個排他鎖,這個事件將在另外一個線程放棄這個鎖的時候發生。
  • 結束(Terminate):已經終止的線程的線程狀態,線程已經結束執行。

阻塞還在佔用資源,等待不佔用資源。

十、線程安全與鎖優化

當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。

1、Java語言中的線程安全

(1)不可變

  • 使用final修飾的常量是不可能發生線程不安全的情況的。

(2)絕對線程安全

  • 絕對的線程安全完全滿足 Brian Goetz 給出的線程安全的定義,這個定義其實是很嚴格的,一個類要達到 “不管運行是環境如何,調用者都不需要任何額外的同步措施” 通常需要付出很大的,甚至有時候是不切實際的代價。

(3)相對線程安全

  • 它需要保證對這個對象單獨的操作是線程安全,我們在調用的時候不需要做額外的保障措施,但是對於一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。
  • 例如: Vector、HashTable

(4)線程兼容

  • 線程兼容是指對象本身並不是線程安全的,但是可以通過在調用端正確地使用同步手段來保證對象在併發環境中可以安全地使用,我們平常說一個類不是線程安全的,絕大多數時候指的是這一種情況

(5)線程對立

  • 線程對立是指無論調用端是否採取了同步措施,都無法在多線程環境中併發使用的代碼。
  • 例如:System.setIn()、System.setOut() 

*我們一般說的線程安全指的是:相對線程安全和線程兼容。

2、線程安全的實現方式

(1)互斥同步

  • 最基本的互斥同步手段就是 synchronized 關鍵字,他是悲觀的認爲任何時刻都會發生線程不安全的情況。所以每次都要進行鎖操作。
  • 重入鎖:高級功能:
    1. 等待可中斷
    2. 公平鎖
    3. 鎖綁定多個條件
  • JDK1.6中對synchronized 做了優化,使他的效率和互斥鎖基本持平,所以我們在不是必須使用重入鎖的情況,優先使用synchronized 。

(2)非阻塞同步

  • 基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他線程爭用共享數據,那操作就成功了;如果共享數據有爭用,產生了衝突,那就再採取其他的補償措施(最常見的補償措施就是不斷地重試,知道成功爲止),這種樂觀的併發策略的許多實現都不需要把線程掛起,因此這種同步操作稱爲非阻塞同步(Non-Blocking Synchronization)。
  • 他是一個樂觀鎖

(3)無同步方案

  • 要保證線程安全,並不是一定就要進行同步,兩者沒有因果關係。
  • 不需要同步的代碼有兩類:
    1. 可重入代碼:在代碼任何時間中斷,然後在重新調用這個代碼,結果都不會出錯。那他就是線程安全的。
    2. 線程本地存儲:在一個線程中,無需同步

3、鎖優化

(1)自旋鎖和自適應自旋

  • 如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,我們就可以讓後面請求鎖的那個線程 “稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
  • JDK 1.6 中引入了自適應的自旋鎖。自適應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如 100 個循環。另外,如果對於某個鎖,自旋很少成功獲得過,那在以後要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。有了自適應自旋,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測就會越來越準確,虛擬機就會變得越來越 “聰明” 了。

(2)鎖消除

  • 有些代碼,雖然這裏有鎖,但是可以被安全地消除掉,在即時編譯之後,這段代碼就會忽略掉所有的同步而直接執行了。

(3)鎖粗化

  • 如果虛擬機探測到由這樣的一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部,只需要加一次鎖。

(4)輕量級鎖

  •  JDK 1.6 之中加入的新型鎖機制,它名字中的 “輕量級” 是相對於使用操作系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱爲 “重量級” 鎖。首先需要強調一點的是,輕量級鎖並不是用來代替重要級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
  • 他是一個樂觀鎖
  • 先進行CAS操作,如果CAS失敗,則說明這個鎖被其他線程搶佔了,進入重量級鎖。如果CAS成功了,那麼,不進行鎖操作,直接進入代碼塊。
  • 如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

(5)偏向鎖

  • 他也是一個樂觀鎖
  • 偏向鎖也是 JDK 1.6 中引入的一項鎖優化,它的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。
  • 它的意思是這個鎖會偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。
  • 如果被其他線程獲取了,其他線程像執行輕量級鎖那樣繼續執行。輕量級鎖CAS失敗,執行重量鎖,否則,直接進入代碼塊。
  • 偏向鎖可以提高帶有同步但無競爭的程序性能。它同樣是一個帶有效益權衡(Trade Off)性質的優化,也就是說,它並不一定總是對程序運行有利,如果程序中大多數的鎖總是被多個不同的線程訪問,那偏向模式就是多餘的。在具體問題具體分析的前提下,有時候使用參數 -XX:-UseBiasedLocking 來禁止偏向鎖優化反而可以提升性能。

總結:輕量級鎖和偏向鎖,並不是總是對程序運行有利的,當在經常出現衝突的程序中,反而增加了CAS操作和偏向操作,降低了運行效率。


參考資料:https://me.csdn.net/sinat_38259539敬業的小碼哥


真正的平靜,不是避開車馬喧囂,而是在心中修籬種菊。  ——白落梅 《你若安好 便是晴天》

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章