參照周志明老師的《深入理解Java虛擬機(jī)》做的摘要
Java 內(nèi)存區(qū)域
運(yùn)行時(shí)數(shù)據(jù)區(qū)域
程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器
Java 虛擬機(jī)棧:線程私有肩狂。每個方法被執(zhí)行的時(shí)候谈飒,Java 虛擬機(jī)會同步創(chuàng)建一個棧幀用于存儲局部變量表手素、操作數(shù)棧、動態(tài)連接呢灶、方法出口等信息。方法的從調(diào)用到執(zhí)行完畢對應(yīng)一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程陋守。
Java 堆:線程共享。對象和數(shù)組實(shí)例都是在這分配內(nèi)存,是 GC 重點(diǎn)照顧的對象拿霉。以前很多虛擬機(jī)的垃圾收集器是基于分代收集理論設(shè)計(jì)的吟秩,所以也經(jīng)常分為新生代、老年代绽淘、Eden 空間涵防、From Survivor 空間、To Survivor 空間等∈栈郑現(xiàn)在很多垃圾收集器設(shè)計(jì)理念已經(jīng)發(fā)生了變化武学,所以以后基于分代思維可能需要改變了祭往。
方法區(qū):線程共享伦意。用于存儲已被虛擬機(jī)加載的類型信息、常量硼补、靜態(tài)變量驮肉、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。
運(yùn)行時(shí)常量池:方法區(qū)的一部分已骇、存放編譯器生產(chǎn)的各種字面量和符號引用离钝,也能在運(yùn)行期將新的常量放入池中。
-
對象創(chuàng)建:
- 分配內(nèi)存
- 執(zhí)行構(gòu)造函數(shù)和 <init> 方法將對象初始化
- 將內(nèi)存地址指向引用的指針褪储;
-
對象內(nèi)存布局:
- 對象頭:GC 分代年齡卵渴、哈希嗎、鎖狀態(tài)標(biāo)志鲤竹、線程持有的鎖浪读、偏向線程 ID、偏向時(shí)間戳辛藻。
- 實(shí)例數(shù)據(jù)
- 對齊填充
垃圾收集器與內(nèi)存分配
判斷對象是否存活(標(biāo)記垃圾)
引用計(jì)數(shù)算法:虛擬機(jī)為每一個對象加一個引用計(jì)數(shù)器碘橘,當(dāng)有一個地方引用該對象時(shí),計(jì)數(shù)器加一吱肌;當(dāng)引用失效時(shí)痘拆,計(jì)數(shù)器減一。
引用計(jì)數(shù)算法雖然占用了一些額外內(nèi)存空間來計(jì)數(shù)氮墨,但它的原理簡單纺蛆,判斷效率也很高,在大多數(shù)情況下都是一個不錯的算法规揪。
缺點(diǎn):必須要配合大量額外處理才能保證正確工作桥氏,而且對于對象之間相互循環(huán)引用也很難解決。可達(dá)性分析算法:通過一系列“GC Roots”的根對象作為起始節(jié)點(diǎn)集粒褒,從這些節(jié)點(diǎn)開始根據(jù)引用關(guān)系向下搜索识颊,搜索過程所走過的路徑稱為“引用鏈”,如果某個對象到 GC Roots 間沒有任何引用鏈相連,則證明此對象不可達(dá)祥款,可進(jìn)行垃圾標(biāo)志等待回收清笨。
固定可作為 GC Roots 的對象包括以下幾種:
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調(diào)用的方法堆棧中使用到的參數(shù)刃跛、局部變量抠艾、臨時(shí)變量等。
- 在方法區(qū)中類靜態(tài)屬性引用的對象桨昙,譬如 Java 類的引用類型靜態(tài)變量检号;
- 在方法區(qū)中常量引用的對象,如字符串常量池里的引用蛙酪;
- 在本地方法棧中 JNI(即常所說的 Native 方法)引用的對象齐苛。
- Java 虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對應(yīng)的 Class 對象桂塞,一些常駐的異常對象 (比如 NullPointExcepiton)等凹蜂,還有系統(tǒng)類加載器;
- 所有同步鎖持有的對象阁危;
- 反映 Java 虛擬機(jī)內(nèi)部情況的 JMXBean玛痊、JVMTI 中注冊的回調(diào)、本地代碼緩存等
垃圾收集算法:
-
標(biāo)記-清除算法:首先標(biāo)記出所有需要回收的對象狂打,在標(biāo)記完成后擂煞,統(tǒng)一回收掉所有被標(biāo)記的對象,也可以反過來趴乡,標(biāo)記存活的對象对省,統(tǒng)一回收所有未被標(biāo)記的對象;
缺點(diǎn):一個是執(zhí)行效率不穩(wěn)定, 如果 Java 堆中包含大量對象浙宜,而且其中大部分是需要被回收的官辽,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動作,導(dǎo)致標(biāo)記和清除兩個過程的執(zhí)行效率都隨對象數(shù)量增長而降低粟瞬;第二個是內(nèi)存空間的碎片化問題同仆,標(biāo)記、清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片裙品,空間碎片太多可能會導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對象時(shí)俗批,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作; -
標(biāo)記-復(fù)制算法:將內(nèi)存區(qū)域按比例劃分為不同的區(qū)域市怎,當(dāng)一塊內(nèi)存用完了岁忘,就進(jìn)行 GC 然后把存活的對象復(fù)制到另一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉区匠,這樣能有效避免內(nèi)存空間碎片的問題干像。
缺點(diǎn):可用內(nèi)存變小帅腌,無法完全利用內(nèi)存造成空間浪費(fèi)。
主流商用 Java 虛擬機(jī)用于新生代的收集算法麻汰,因?yàn)樾律械膶ο笥?98% 熬不過第一輪收集速客,所以需要復(fù)制的對象不多。同時(shí) Eden 和 Survivor 的比例是8:1五鲫,浪費(fèi)的內(nèi)存空間相對較少溺职。 -
標(biāo)記-整理算法: 標(biāo)記清除后讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi)存位喂。相比標(biāo)記清除算法浪耘,解決了內(nèi)存空間碎片的問題,但相對的引入了整理的過程塑崖,提升了復(fù)雜度和性能花銷辜膝。
缺點(diǎn):如果每次回收后存活的對象都很多痴怨,就會給系統(tǒng)帶來極大的負(fù)重操作凸主。
根節(jié)點(diǎn)枚舉:所有收集器在這一步驟都必須暫停用戶線程
安全點(diǎn):收到“Stop The World”后祝旷,用戶程序需要到達(dá)安全點(diǎn)才能暫停处窥,不能隨意暫停菩收;
并發(fā)的可達(dá)性分析:
- 增量更新:把新插入的引用記錄下來诡壁,掃描結(jié)束后針對新插入的重新掃描一次触机;
- 原始快照:將要刪除的引用對象記錄下來颠区,掃描結(jié)束后以這些對象作為根再掃描一遍削锰;
垃圾收集器:
- Serial:新生代收集器,標(biāo)記-復(fù)制毕莱,會暫停其它所有線程器贩,簡單高效;
- ParNew: Serial 的多線程版本朋截,可與 CMS 收集器配合工作蛹稍;
- Parallel Scavenge:新生代收集器,跟 ParNew 非常相似部服,但是更在意吞吐量唆姐;
- Serial Old:老年代收集器,標(biāo)記-整理廓八,會暫停其它所有線程奉芦,簡單高效,更多的是客戶端模式下使用
- Parallel Old:老年代收集器剧蹂,標(biāo)記-整理声功,多線程并發(fā)收集;
- CMS: 老年代收集器,以獲取最短回收停頓時(shí)間為目標(biāo)宠叼,常跟 ParNew 配合使用先巴,從JDK 9開始不推薦,推薦 G1 取代。
- Garbage First(G1):面向服務(wù)端的垃圾收集器伸蚯,不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分醋闭,而是把連續(xù)的 Java 堆劃分為多個大小相等的獨(dú)立區(qū)域(Region),每一個 Region 都可以根據(jù)需要朝卒,扮演新生代的 Eden 空間或 Survivor 空間或老年代空間证逻,還會為大對象劃分專屬的 Humongous 區(qū)域。允許用戶選擇期望的停頓時(shí)間
G1運(yùn)作過程:
- 初始標(biāo)記:標(biāo)記 GC Roots 能直接關(guān)聯(lián)的對象抗斤,需要停頓所有線程囚企,但耗時(shí)很短;
- 并發(fā)標(biāo)記:從 GC Roots 開始對對象進(jìn)行可達(dá)性分析瑞眼,耗時(shí)較長龙宏,但可與用戶程序并發(fā)執(zhí)行;
- 最終標(biāo)記:通過原始快照算法處理并發(fā)階段遺留的記錄伤疙;
- 帥選標(biāo)記:更新 Region 的統(tǒng)計(jì)數(shù)據(jù)银酗,對各個 Region 的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的最短時(shí)間來制定回收計(jì)劃徒像。將選擇的回收 Region 中的存活對象復(fù)制到空 Region 中黍特,然后清理掉整個舊的 Region 的全部空間。
低延遲垃圾收集器:
- Shenandoah:RedHat 公司開發(fā)的低延遲收集器
- ZGC:尚在實(shí)驗(yàn)中的低延遲垃圾收集器锯蛀,由 Oracle 公司研發(fā)灭衷。使用了染色指針的新技術(shù)。
虛擬機(jī)類加載機(jī)制
一個類型的生命周期有加載旁涤、連接(驗(yàn)證翔曲、準(zhǔn)備、解析)劈愚、初始化瞳遍、使用、卸載菌羽。其中加載掠械、驗(yàn)證、準(zhǔn)備算凿、初始化和卸載這五個階段的開始順序是確定的份蝴,而解析則不一定,因?yàn)橛行┓栆眯枰\(yùn)行時(shí)才能確定調(diào)用者類型然后進(jìn)行解析氓轰。
注意:是開始順序確定婚夫,結(jié)束順序則不一定,比如類加載還沒結(jié)束就會開始驗(yàn)證
Java虛擬機(jī)規(guī)范規(guī)定了有且只有六種情況在類沒有初始化時(shí)必須對類進(jìn)行初始化(加載署鸡、驗(yàn)證案糙、準(zhǔn)備自然需要在此之前開始):
- 遇到 new限嫌、getstatic、putstatic时捌、invokestatic 這四條字節(jié)碼指令時(shí)怒医,也就是實(shí)例化對象、讀取或設(shè)置一個靜態(tài)字段(被 final 修飾奢讨、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)稚叹、調(diào)用一個類型的靜態(tài)方法的時(shí)候;(使用靜態(tài)字段或者方法的時(shí)候拿诸,只會初始化定義靜態(tài)字段或方法的類扒袖,比如通過子類調(diào)用父類的靜態(tài)字段,只會初始化父類)
- 使用 java.lang.reflect 包的方法對類型進(jìn)行反射調(diào)用的時(shí)候亩码;
- 當(dāng)初始化類時(shí)季率,父類沒有初始化的時(shí)候需要對父類進(jìn)行初始化;
- 虛擬機(jī)啟動時(shí)描沟,用戶需要指定一個要執(zhí)行的類(包含 main 方法的類)飒泻,虛擬機(jī)會先初始化這個類;
- 使用 java.lang.invoke.MethodHandle 實(shí)例解析方法句柄的時(shí)候吏廉;
- 定義了默認(rèn)方法(被 default 關(guān)鍵字修飾的方法)的接口泞遗,如果這個接口的實(shí)例初始化,該接口也要在此之前被初始化迟蜜;
類加載的過程
加載:
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流(沒有規(guī)定從哪里獲取字節(jié)流刹孔,所以可以是文件,也可以是網(wǎng)絡(luò)娜睛,動態(tài)加載的基礎(chǔ)支持);
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)卦睹;
- 在內(nèi)存中生成一個代表該類的 java.lang.Class 對象畦戒,作為方法區(qū)這個類的各種數(shù)據(jù)訪問入口;
加載階段結(jié)束后结序,Java虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所設(shè)定的格式存儲在方法區(qū)中了障斋。
驗(yàn)證:
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合 Class 文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理徐鹤。只有保證輸入的字節(jié)流能夠正確地解析并存儲于方法區(qū)之內(nèi)垃环,這個段字節(jié)流才被允許進(jìn)入Java虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲。所以這個階段會于加載過程中就開始返敬。
- 元數(shù)據(jù)驗(yàn)證:對字節(jié)碼描述的信息(即類的元數(shù)據(jù)信息)進(jìn)行語義分析遂庄,以保證其描述的信息符合 Java 語義規(guī)范的要求;
- 字節(jié)碼驗(yàn)證:對類的方法體進(jìn)行校驗(yàn)分析劲赠,包括數(shù)據(jù)流分析和控制流分析等復(fù)雜流程涛目,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會做出危害虛擬機(jī)安全的行為秸谢,是驗(yàn)證過程中最復(fù)雜的階段;
- 符號引用驗(yàn)證:通過對符號引用校驗(yàn)霹肝,檢測是否存在對應(yīng)的類估蹄、方法或字段,以及對該類沫换、方法或字段是否具有訪問權(quán)限臭蚁。該驗(yàn)證發(fā)生在解析階段,目的是確保解析行為能正常執(zhí)行讯赏;
準(zhǔn)備:
準(zhǔn)備階段是正式為類中定義的變量(即靜態(tài)變量)分配內(nèi)存并設(shè)置變量初始值刊棕,用戶設(shè)置的值需要在初始化階段才會設(shè)置。
解析:
解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程待逞,直接引用是可以直接指向目標(biāo)的指針甥角、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。解析動作主要針對類或接口识樱、字段嗤无、類方法、接口方法怜庸、方法類型当犯、方法句柄和調(diào)用限定符7類符號引用進(jìn)行割疾。
初始化:
初始化階段就是執(zhí)行類構(gòu)造器<clinit>() 方法的過程嚎卫,該方法是由編譯器自動收集類中所有類變量(靜態(tài)變量)的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的;靜態(tài)語句是按順序收集的宏榕,所以只能訪問到定義在靜態(tài)語句塊之前的變量拓诸。父類方法的初始化先于子類方法的初始化。
類加載器
“通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流”這個動作就是通過類加載器實(shí)現(xiàn)的(通過loadClass 方法)麻昼。同一個類比較是否相等必須在同一個類加載器加載奠支,否則無意義。
三層類加載器
- 啟動類加載器:Bootstrap Class Loader抚芦。使用 C++ 語言實(shí)現(xiàn)倍谜,無法被 Java 程序直接引用,負(fù)責(zé)加載存放<JAVA_HOME>\lib目錄叉抡,如果需要把加載器請求委派給啟動類加載器尔崔,那直接使用 null 代替即可。
- 擴(kuò)展類加載器:Extension Class Loader(ExtClassLoader)褥民。java 實(shí)現(xiàn)季春,開發(fā)者可以直接在程序中使用,負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中轴捎,或者被 java.ext.dirs 系統(tǒng)變量所指定的路徑中所有的類庫鹤盒。
- 應(yīng)用程序類加載器:Application Class Loader(AppClassLoader)蚕脏。負(fù)責(zé)加載用戶類路徑上所有的類庫,開發(fā)者可以直接在程序中使用侦锯,如果應(yīng)用程序中沒有定義過自己的類加載器驼鞭,一般情況下這個就是程序中的默認(rèn)類加載器。
雙親委派模式要求除了頂層的啟動類加載器外尺碰,其余的類加載器都應(yīng)有自己的父類加載器挣棕。
雙親委派模式
雙親委派模式工作過程:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類亲桥,而是把這個請求委派給父類加載器去完成洛心,每一個層次的類加載器都是如此,因此所有的加載請求都應(yīng)該傳送到最頂層的啟動類加載器中题篷,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個加載請求(搜索范圍內(nèi)沒有找到該類)時(shí)词身,子加載器才會嘗試自己去完成加載。
虛擬機(jī)字節(jié)碼執(zhí)行引擎
運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行背后的數(shù)據(jù)接口番枚,它也是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧的棧元素法严。棧幀存儲了方法的局部變量表、操作數(shù)棧葫笼、動態(tài)連接和方法返回地址等信息深啤。方法的從調(diào)用到執(zhí)行完畢對應(yīng)一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程。在編譯 Java 程序源碼的時(shí)候路星,棧幀需要多大的局部變量表溯街,需要多深的操作數(shù)棧就已經(jīng)被分析計(jì)算出來,并寫入到方法表(每個類都有一個方法表記錄定義的方法)的 Code 屬性之中洋丐,即棧幀需要的內(nèi)存在編譯時(shí)已經(jīng)確定了的呈昔。
局部變量表
局部變量表是一組變量值的存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量垫挨。如果執(zhí)行的是實(shí)例方法,那局部變量表中第0位索引的變量槽默認(rèn)是用于傳遞方法所屬對象實(shí)例的引用。
操作數(shù)棧
后入先出的棧哲泊,方法執(zhí)行中參數(shù)的調(diào)用和運(yùn)算都要依賴于操作數(shù)棧。
動態(tài)連接
每個棧幀都包含一個指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用催蝗,只有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接丙号。
方法返回地址
方法退出會將棧幀出棧先朦,恢復(fù)上層方法的局部變量表和操作數(shù)棧缰冤,把返回值(有的話)壓入調(diào)用者棧幀的操作數(shù)棧中棉浸,調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令迷郑。
方法調(diào)用
一切方法在 class 文件里面存儲的都只是符號引用创倔,而不是方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址畦攘。
解析
在類加載解析階段知押,會將其中一部分的符號引用轉(zhuǎn)化為直接引用,這一類解析前提是方法在程序運(yùn)行前就有一個可確定的調(diào)用版本首妖,并且這個版本在運(yùn)行期不會改變有缆。
分派
有些方法的符號引用在解析階段并不能確定它的直接引用棚壁,所以會在運(yùn)行時(shí)進(jìn)行分派,通常是虛方法才需要分派袖外,即除靜態(tài)方法曼验、私有方法鬓照、實(shí)例構(gòu)造器豺裆、父類方法和 final 修飾的方法之外的方法臭猜。
基于棧的字節(jié)碼解釋執(zhí)行引擎
javac 編譯器完成了程序代碼經(jīng)過詞法分析蔑歌、語法分析到抽象語法樹丐膝,再遍歷抽象語法樹生成線性的字節(jié)碼指令流的過程
解釋執(zhí)行
動態(tài)產(chǎn)生每條字節(jié)碼對應(yīng)的匯編代碼來運(yùn)行帅矗。
基于棧的解釋執(zhí)行
Javac 編譯器輸出的字節(jié)碼指令流浑此,基本上是一種基于棧的指令集架構(gòu)凛俱,字節(jié)碼指令流里面的指令大部分都是零地址指令蒲犬,它們依賴操作數(shù)棧進(jìn)行工作
前端編譯與優(yōu)化
前端編譯器:JDK 的 Javac
即時(shí)編譯器:HotSpot 虛擬機(jī)的 C1原叮、C2 即時(shí)編譯器,Graal 編譯器
提前編譯器:JDK 的 Jaotc
前端編譯器做的優(yōu)化措施更多是為了降低程序員的編碼復(fù)雜度擂送、提高編碼效率嘹吨;后端編譯器則是對程序執(zhí)行性能進(jìn)行優(yōu)化蟀拷。
Javac 編譯器
Javac編譯過程:
- 準(zhǔn)備過程:初始化插入式注解處理器;
- 解析與填充符號表過程,包括:
- 詞法她按、語法分析。將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合匕累,構(gòu)造出抽象語法樹欢嘿。
- 填充符號表炼蹦。產(chǎn)生符地址和符號信息掐隐。
- 插入式注解處理器的注解處理過程:插入式注解處理器的執(zhí)行階段虑省,如果產(chǎn)生新的符號就需要回到之前的過程2重新處理探颈。
- 分析與字節(jié)碼生成過程伪节。包括:
- 標(biāo)注檢查架馋。對語法的靜態(tài)信息進(jìn)行檢查叉寂。
- 數(shù)據(jù)流及控制流分析屏鳍。對程序動態(tài)執(zhí)行過程進(jìn)行檢查钓瞭。
- 解語法糖山涡。將簡化代碼編寫的語法糖還原為原有的形式鸭丛。
- 字節(jié)碼生成過程鳞溉。將前面各個步驟所生成的信息轉(zhuǎn)化為字節(jié)碼熟菲。
Java 語法糖
- 泛型:只在源碼中存在抄罕,經(jīng)過編譯后的字節(jié)碼中厉萝,全部泛型都被替換為原來的裸類型谴垫,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)換代碼翩剪,即會類型檫除前弯。
- 自動裝箱恕出、拆箱:對基本數(shù)據(jù)類型轉(zhuǎn)化為對應(yīng)的包裝類型浙巫,或者相反操作的畴。
- 遍歷循環(huán):將 for-each 替換為迭代器遍歷
- 條件編譯器:根據(jù)布爾常量值的真假將分支中不成立的代碼塊消除掉丧裁。
后端編譯與優(yōu)化
后端編譯:將 Class 文件轉(zhuǎn)換成與本地基礎(chǔ)設(shè)施(硬件指令集煎娇、操作系統(tǒng))相關(guān)的二進(jìn)制機(jī)器碼眨猎。
即時(shí)編譯器
即時(shí)編譯器:當(dāng)虛擬機(jī)發(fā)現(xiàn)某個方法或代碼塊運(yùn)行特別頻繁,就會把這些代碼認(rèn)定為“熱點(diǎn)代碼”匿情,為了提高熱點(diǎn)代碼的執(zhí)行效率炬称,在運(yùn)行時(shí)玲躯,虛擬機(jī)將會把這些代碼編譯成本地機(jī)器碼跷车,并以各種手段進(jìn)行代碼優(yōu)化朽缴,運(yùn)行時(shí)完成這個任務(wù)的后端編譯器被稱為即時(shí)編譯器密强。
解釋器:當(dāng)程序需要迅速啟動和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用薪鹦,省去編譯的時(shí)間距芬,立即運(yùn)行框仔。同時(shí)在運(yùn)行時(shí)收集程序性能監(jiān)控信息离斩,為即時(shí)編譯器的優(yōu)化提供數(shù)據(jù)參考跛梗,解釋器還作為即時(shí)編譯器激進(jìn)優(yōu)化時(shí)的后備逃生門(去優(yōu)化)核偿。
HotSpot 虛擬機(jī)內(nèi)置了兩個編譯器(或三個轰绵,Graal 仍處于實(shí)驗(yàn)中)左腔,分別被稱為“客戶端編譯器” - C1 編譯器和“服務(wù)端編譯器” - C2 編譯器液样。
通常優(yōu)化程度越高的代碼鞭莽,所需的編譯時(shí)間便會越長撮抓,解釋器收集的信息要求也更全,這會對程序的執(zhí)行性能有一定的干擾乖酬,所以 Hotspot 虛擬機(jī)采用了分層編譯的功能:
- 第0層咬像。程序純解釋執(zhí)行县昂,并且解釋器不開啟性能監(jiān)控功能倒彰。
- 第 1 層芒澜。使用客戶端編譯器將字節(jié)碼編譯為本地代碼來運(yùn)行痴晦,進(jìn)行簡單可靠的穩(wěn)定優(yōu)化誊酌,不開啟性能監(jiān)控功能术辐。
- 第 2 層。仍然使用客戶端編譯器執(zhí)行猾骡,僅開啟方法及回邊次數(shù)統(tǒng)計(jì)等有限性能監(jiān)控功能兴想。
- 第 3 層。仍然使用客戶端編譯器執(zhí)行毙替,開啟全部性能監(jiān)控功能厂画,除了第 2 層的統(tǒng)計(jì)信息外袱院,還會收集如分支跳轉(zhuǎn)、虛方法調(diào)用版本等全部統(tǒng)計(jì)信息欲虚。
- 第 4 層苍在。使用服務(wù)端編譯器將字節(jié)碼編譯為本地代碼寂恬,相比起客戶端編譯器酷鸦,服務(wù)端編譯器會啟用更多編譯耗時(shí)更長的優(yōu)化臼隔,還會根據(jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
熱點(diǎn)代碼主要有兩類:
- 被多次調(diào)用的方法氨淌。
- 被多次執(zhí)行的循環(huán)體
對于這兩種情況,編譯的目標(biāo)對象都是整個方法體豪筝,這也是虛擬機(jī)中標(biāo)準(zhǔn)的即時(shí)編譯方式续崖。第一種是直接以整個方法作為編譯對象;第二種是在第一種的基礎(chǔ)上傳入執(zhí)行入口點(diǎn)字節(jié)碼序號(從第幾條字節(jié)碼指令開始執(zhí)行編譯)著蟹,這種編譯因?yàn)榘l(fā)生在方法執(zhí)行的過程中,被稱為棧上替換涮雷,即方法的棧幀還在棧上样刷,方法就被替換了蜓竹。
判斷某段代碼是不是熱點(diǎn)代碼是通過“熱點(diǎn)探測”來進(jìn)行的俱济,主流熱點(diǎn)探測方式有以下兩種:
- 基于采樣的熱點(diǎn)探測聂喇。即周期的檢測各個線程的調(diào)用棧頂授帕,如果發(fā)現(xiàn)某個方法經(jīng)常出現(xiàn)在棧頂彤路,就判斷為熱點(diǎn)方法洲尊。這種實(shí)現(xiàn)方式簡單高效躯护,還可以很容易的獲取方法的調(diào)用關(guān)系(將調(diào)用棧頂展開即可)棺滞,缺點(diǎn)是很難精確的確認(rèn)一個方法的熱度,容易受到線程阻塞或者別的外界因素干擾。
- 基于計(jì)數(shù)器的熱點(diǎn)探測崔泵。虛擬機(jī)為每個方法(或者是回邊代碼塊-統(tǒng)計(jì)回邊次數(shù))建立計(jì)數(shù)器憎瘸,統(tǒng)計(jì)方法的執(zhí)行次數(shù)含思,執(zhí)行次數(shù)超過一定閾值就認(rèn)為是熱點(diǎn)方法崎弃。這種方式雖然實(shí)現(xiàn)復(fù)雜點(diǎn),還要為每個方法建立并維護(hù)計(jì)數(shù)器含潘,而且不能直接獲取方法的調(diào)用關(guān)系饲做,但它的統(tǒng)計(jì)結(jié)果相對來說更加嚴(yán)謹(jǐn)。
提前編譯器
- 在程序運(yùn)行之前把程序代碼直接編譯成機(jī)器碼遏弱。
- 把原本即時(shí)編譯器在運(yùn)行時(shí)要做的編譯工作提前做好并保存下來,下次運(yùn)行到這些代碼(譬如公共代碼在被同一臺機(jī)器其它 Java 進(jìn)程使用)時(shí)直接把它加載進(jìn)來使用漱逸。
提前編譯能將編譯過程中最耗時(shí)的優(yōu)化措施如“過程間分析”等以及一些其它耗時(shí)的優(yōu)化措施提前進(jìn)行泪姨,避免運(yùn)行時(shí)對用戶程序的干擾。
即時(shí)編譯器要占用程序運(yùn)行時(shí)間和運(yùn)算資源饰抒,而且達(dá)到全速運(yùn)行狀態(tài)需要一定的時(shí)間肮砾。但是由于運(yùn)行時(shí)數(shù)據(jù)監(jiān)控的功能,能夠進(jìn)行熱點(diǎn)代碼分析并制定合適的優(yōu)化方案袋坑,而且一些激進(jìn)預(yù)測性優(yōu)化也無法脫離運(yùn)行時(shí)的數(shù)據(jù)參考仗处。
編譯器優(yōu)化技術(shù)
方法內(nèi)聯(lián)
將目標(biāo)方法的代碼原封不動地“復(fù)制”到發(fā)起調(diào)用的方法之中,避免發(fā)生真實(shí)的方法調(diào)用枣宫。方法內(nèi)聯(lián)能夠去除方法調(diào)用的成本(查找方法版本婆誓、建立棧幀等),為其它優(yōu)化建立良好的基礎(chǔ)也颤,多數(shù)其他優(yōu)化都是基于方法內(nèi)聯(lián)的基礎(chǔ)上的洋幻。
逃逸分析
分析對象動態(tài)作用域,當(dāng)一個對象在方法里面被定義后翅娶,它可能被外部方法引用文留,例如作為參數(shù)傳遞到其它方法中,這種稱為方法逃逸故觅;甚至還可能被外部線程訪問到厂庇,譬如賦值給可以在其他線程中訪問的實(shí)例變量,這種稱為線程逃逸输吏;從不逃逸权旷、方法逃逸到線程逃逸,稱為對象由低到高的不同逃逸程度。
針對不逃逸或者逃逸程度低的對象實(shí)例拄氯,可采取一下優(yōu)化措施:
- 棧上分配:由于垃圾回收需要消耗大量資源躲查,如果確定一個對象不會逃逸出線程之外,那讓這個對象在棧上分配內(nèi)存將會是一個不錯的主意译柏,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀镣煮。支持方法逃逸不支持線程逃逸。
- 標(biāo)量替換:若一個數(shù)據(jù)無法再分解成更小的數(shù)據(jù)來表示鄙麦,如基本的數(shù)據(jù)類型典唇,那它就可以稱為標(biāo)量。如果把一個 Java 對象拆散胯府,根據(jù)程序訪問的情況介衔,將其用到的成員變量恢復(fù)為原始類型直接在方法中定義來訪問,這個過程稱為標(biāo)量替換骂因。由于去掉對象實(shí)例的創(chuàng)建炎咖,所以需要對象完全不逃逸。
- 同步消除:由于線程同步本身是一個相對耗時(shí)的過程寒波,如果逃逸分析能夠確定一個變量不會逃逸出線程乘盼,無法被其他線程訪問,那么這個實(shí)例對象的讀寫就不會有競爭俄烁,因此針對這個對象的同步措施就可以安全的消除掉绸栅。
公共子表達(dá)式消除
如果一個表達(dá)式 E 之前已經(jīng)被計(jì)算過了,并且從之前的計(jì)算到現(xiàn)在 E 中所有變量的值都沒有發(fā)生變化页屠,那么 E 的這次出現(xiàn)就稱為公共子表達(dá)式阴幌。對于這種公共子表達(dá)式,就沒必要花時(shí)間重新對其進(jìn)行計(jì)算卷中,只需要直接使用前面計(jì)算過的表達(dá)式結(jié)果代替 E。
數(shù)組邊界檢查消除
對于虛擬機(jī)的執(zhí)行子系統(tǒng)來說渊抽,每次數(shù)組元素的讀寫都帶有一次隱含的條件判斷操作蟆豫,當(dāng)數(shù)組越界時(shí)拋出一個運(yùn)行時(shí)異常以避免溢出攻擊(像 C 語言數(shù)組越界會產(chǎn)生不可控的結(jié)果),但對于含有大量數(shù)組訪問操作的程序代碼懒闷,這必定是一種性能負(fù)擔(dān)十减。如果編譯器能夠通過數(shù)據(jù)流分析能夠確定數(shù)組的訪問不會越界,如循環(huán)讀取數(shù)組數(shù)據(jù)時(shí)愤估,那么編譯器就會把數(shù)組的上下界檢查消除掉帮辟,這可以節(jié)省很多次的條件判斷操作。
Java 內(nèi)存模型與線程
Java 內(nèi)存模型
主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型的主要目的是定義程序中各種變量的訪問規(guī)則玩焰,即關(guān)注在虛擬機(jī)中把變量值存儲到內(nèi)存和從內(nèi)存中取出變量值的細(xì)節(jié)由驹。此處的變量包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組的對象的元素昔园,但不包括局部變量與方法參數(shù)蔓榄,因?yàn)楹笳呤蔷€程私有的并炮,不會被共享。
Java 內(nèi)存模型規(guī)定了所有變量都存儲在主內(nèi)存中甥郑,每條線程還有自己的工作內(nèi)存逃魄,線程的工作中保存了被該線程使用的變量的主內(nèi)存副本。線程對變量的所有操作(讀取澜搅、賦值等)都必須在工作內(nèi)存中進(jìn)行伍俘,而不能直接讀寫主內(nèi)存中的數(shù)據(jù)。不同線程之前也無法訪問對方的工作內(nèi)存中的變量勉躺,線程間變量值的傳遞需要通過主內(nèi)存來完成癌瘾。
內(nèi)存間交互操作
關(guān)于主內(nèi)存與工作內(nèi)存之前具體的交互協(xié)議柳弄,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存這一類細(xì)節(jié)概说,Java 內(nèi)存模型定義了 8 種操作碧注,每一種都是原子的,不可再分的糖赔。
- lock(鎖定):作用于主內(nèi)存的變量萍丐,它把一個變量識別為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,它把-個處于鎖定狀態(tài)的變量釋放出來放典,釋放后的變量才可以被其他線程鎖定逝变。
- read(讀取):作用于主內(nèi)存的變量奋构,他把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中壳影,以便隨后的 load 動作使用。
- load(載入):作用于工作內(nèi)存的變量弥臼,他把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中宴咧。
- use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎. 每當(dāng)虛擬機(jī)遇到一個需要使用變量的值的字節(jié)碼指令時(shí)將會執(zhí)行這個動作径缅。
- assign(賦值):作用于工作內(nèi)存的變量掺栅,他把一個從執(zhí)行引擎接收的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個操作。
- store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中救拉,以便隨后的 write 操作使用。
- write(寫入):作用于主內(nèi)存的變量沙绝,他把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
對于這 8 種操作,虛擬機(jī)也規(guī)定了一系列規(guī)則宿饱,在執(zhí)行這 8 種操作的時(shí)候必須遵循如下的規(guī)則:
- 不允許 read 和 load熏瞄、store 和 write 操作之一單獨(dú)出現(xiàn),也就是不允許從主內(nèi)存讀取了變量的值但是工作內(nèi)存不接收的情況谬以,或者不允許從工作內(nèi)存將變量的值回寫到主內(nèi)存但是主內(nèi)存不接收的情況强饮。
- 不允許一個線程丟棄最近的 assign 操作,也就是不允許線程在自己的工作線程中修改了變量的值卻不同步/回寫到主內(nèi)存为黎。
- 不允許一個線程回寫沒有修改的變量到主內(nèi)存邮丰,也就是如果線程工作內(nèi)存中變量沒有發(fā)生過任何assign操作,是不允許將該變量的值回寫到主內(nèi)存變量只能在主內(nèi)存中產(chǎn)生铭乾。
- 不允許在工作內(nèi)存中直接使用一個未被初始化的變量剪廉,也就是沒有執(zhí)行 load 或者 assign 操作。也就是說在執(zhí)行 use炕檩、store 之前必須對相同的變量執(zhí)行了 load斗蒋、assign 操作。
- 一個變量在同一時(shí)刻只能被一個線程對其進(jìn)行 lock 操作笛质,也就是說一個線程一旦對一個變量加鎖后泉沾,在該線程沒有釋放掉鎖之前,其他線程是不能對其加鎖的妇押,但是同一個線程對一個變量加鎖后跷究,可以繼續(xù)加鎖,同時(shí)在釋放鎖的時(shí)候釋放鎖次數(shù)必須和加鎖次數(shù)相同敲霍。
- 對變量執(zhí)行 lock 操作俊马,就會清空工作空間該變量的值,執(zhí)行引擎使用這個變量之前肩杈,需要重新 load 或者 assign 操作初始化變量的值柴我。
- 不允許對沒有l(wèi)ock的變量執(zhí)行unlock操作,如果一個變量沒有被 lock 操作扩然,那也不能對其執(zhí)行unlock操作屯换,當(dāng)然一個線程也不能對被其他線程 lock 的變量執(zhí)行 unlock 操作。
- 對一個變量執(zhí)行 unlock 之前与学,必須先把變量同步回主內(nèi)存中,也就是執(zhí)行 store 和 write 操作嘉抓。
當(dāng)然索守,最重要的還是如開始所說,這8個動作必須是原子的抑片,不可分割的卵佛。
Volatile
volatile 可以說是 Java 虛擬機(jī)提供的最輕量的同步機(jī)制,它修飾得變量具有兩項(xiàng)特性:
- 保證此變量對所有線程的可見性,一個線程修改了這個變量的值截汪,新值對于其他線程來說可以立即得知疾牲,即修改變量后需要同步到主內(nèi)存,同時(shí)使用的時(shí)候也需要從主內(nèi)存刷新變量的值衙解。
- 禁止指令重排序阳柔。由于虛擬機(jī)會在保證運(yùn)算結(jié)果跟代碼順序執(zhí)行的結(jié)果一致的情況向進(jìn)行指令重排序優(yōu)化。volatile 修飾的變量會要求不被指令重排序優(yōu)化蚓峦,保證代碼執(zhí)行順序跟程序的順序相同舌剂。
volatile 缺點(diǎn)是無法保證原子性,這導(dǎo)致 volatile 變量的運(yùn)算在并發(fā)下一樣是不安全的暑椰。比如自增運(yùn)算霍转,將變量取到操作數(shù)棧時(shí),會跟主內(nèi)存同步一汽,保證變量的正確性避消,但是當(dāng)執(zhí)行加一指令時(shí),其他線程可能已經(jīng)把變量的值改變了召夹,而操作數(shù)棧頂?shù)闹稻妥兂闪诉^期的數(shù)據(jù)岩喷,執(zhí)行加一后就可能把不正確的值同步回主內(nèi)存。
volatile 變量適合以下兩條規(guī)則的運(yùn)算場景:
- 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值戳鹅,或者能夠確保只有單一線程修改變量的值均驶。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束。
原子性枫虏、可見性與有序性
原子性
原子性即操作無法被分解妇穴,執(zhí)行開始到結(jié)束不會插入其它指令×フ基本數(shù)據(jù)類型的訪問腾它、讀寫都具備原子性。如果要保證大范圍的原子性死讹,需要依賴同步操作瞒滴。
可見性
可見性就是指當(dāng)一個線程修改了共享變量的值時(shí),其它線程能夠立刻得知這個變量的修改赞警。volatile妓忍、synchronize 和 final 都能實(shí)現(xiàn)可見性。
有序性
如果在本線程內(nèi)觀察愧旦,所有操作都是有序的;如果在一個線程中觀察另一個線程笤虫,所有的操作都是無序的旁瘫。volatile 和 synchronize 能夠保證線程的有序性祖凫。
先行發(fā)生規(guī)則(Happens-Before)
先行發(fā)生原則是 Java 內(nèi)存模型中定義的兩個操作之間的偏序關(guān)系。比如說操作 A 先行發(fā)生于操作B酬凳,那么在 B 操作發(fā)生之前惠况,A 操作產(chǎn)生的“影響”都會被操作 B 感知到。這里的影響是指修改了內(nèi)存中的共享變量宁仔、發(fā)送了消息稠屠、調(diào)用了方法等。它是判斷數(shù)據(jù)是否存在競爭台诗,線程是否安全的非常有用的手段完箩。
Java內(nèi)存模型自帶先行發(fā)生原則有哪些
- 程序次序原則:在一個線程內(nèi)部,按照代碼的順序拉队,書寫在前面的先行發(fā)生于后邊的弊知。或者更準(zhǔn)確的說是在控制流順序前面的先行發(fā)生于控制流后面的粱快,而不是代碼順序秩彤,因?yàn)闀蟹种А⑻D(zhuǎn)事哭、循環(huán)等漫雷。
- 管程鎖定規(guī)則:一個 unlock 操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須注意的是對同一個鎖鳍咱,后面是指時(shí)間上的后面
- volatile變量規(guī)則:對一個 volatile 變量的寫操作先行發(fā)生于后面對這個變量的讀操作降盹,這里的后面是指時(shí)間上的先后順序
- 線程啟動規(guī)則:Thread對象的 start() 方法先行發(fā)生于該線程的每個動作。當(dāng)然如果你錯誤的使用了線程谤辜,創(chuàng)建線程后沒有執(zhí)行 start 方法蓄坏,而是執(zhí)行 run 方法,那此句話是不成立的丑念,但是如果這樣其實(shí)也不是線程了
- 線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測涡戳,可以通過 Thread.join()和 Thread.isAlive() 的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行
- 線程中斷規(guī)則:對線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過 Thread.interrupted() 方法檢測到是否有中斷發(fā)生脯倚。
- 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的 finalize 方法的執(zhí)行渔彰,也就是初始化方法先行發(fā)生于 finalize 方法
- 傳遞性:如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C推正,那么操作 A 先行發(fā)生于操作 C恍涂。
Java 線程
Java 線程是與內(nèi)核線程 1:1 對應(yīng)的,所有各個線程的操作如創(chuàng)建植榕、析構(gòu)和同步再沧,都需要進(jìn)行系統(tǒng)調(diào)用,而系統(tǒng)調(diào)用需要在用戶態(tài)和內(nèi)核態(tài)中來回切換内贮,花銷較大产园。
Java 線程的狀態(tài):
- 新建(New)
- 運(yùn)行(Runnable)
- 無限期等待(Waiting)
- 限期等待(Timed Waiting)
- 阻塞(Blocked)
- 結(jié)束(Terminated)
線程安全與鎖優(yōu)化
線程安全
線程安全:當(dāng)多個線程同時(shí)訪問一個對象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行夜郁,也不需要進(jìn)行額外的同步什燕,或者在調(diào)用方進(jìn)行其他額外的協(xié)調(diào)操作,調(diào)用這個對象的行為都可以獲得正確的結(jié)果竞端,那就稱這個對象是線程安全的屎即。
Java中各種操作共享的數(shù)據(jù)按線程安全的由強(qiáng)到弱分為以下五類:不可變、絕對線程安全事富、相對線程安全技俐、線程兼容和線程對立。
線程安全的實(shí)現(xiàn)方法
1. 互斥同步(阻塞同步)
同步是指在多個線程并發(fā)訪問共享數(shù)據(jù)時(shí)统台,保證共享數(shù)據(jù)在同一時(shí)刻只被一條(或者是一些雕擂,當(dāng)使用信號量的時(shí)候)線程使用。
Synchronize 是最基本的互斥同步手段贱勃,Javac 編譯后由會在同步塊的前后形成 monitorenter 和 monitorexit 兩個字節(jié)指令井赌,這兩個字節(jié)碼指令都需要一個 reference 類型的參數(shù)來指明要鎖定和解鎖的對象。
在執(zhí)行 monitorenter 指令時(shí)贵扰,會先嘗試獲取對象的鎖仇穗,對象沒有鎖或者當(dāng)前線程已經(jīng)持有這個對象鎖,就把鎖的計(jì)數(shù)器的值加一戚绕,執(zhí)行 monitorexit 指令時(shí)會將計(jì)數(shù)減一纹坐。當(dāng)計(jì)數(shù)器的值為零,鎖隨即就被釋放了舞丛。如果鎖被其他線程持有耘子,當(dāng)前線程就會被阻塞直到鎖被釋放。
Synchronize 的使用需要注意:
- 被 Synchronize 修飾的同步塊對同一線程是可重入的瓷马,只是計(jì)數(shù)器的值加一或者減一而已拴还,不會阻塞自己。
- 被 Synchronize 修飾的同步塊在持有鎖的線程執(zhí)行完畢釋放之前欧聘,會無條件阻塞其他線程的進(jìn)入片林,同時(shí)獲取鎖的線程無法被強(qiáng)制釋放鎖,阻塞等待的線程也無法被強(qiáng)制中斷等待或者超時(shí)退出怀骤。
Java 線程是映射到操作系統(tǒng)的原生內(nèi)核線程之上的费封,如果阻塞或喚醒一條線程,需要操作系統(tǒng)來完成蒋伦,這就不可避免地陷入用戶態(tài)到核心態(tài)的轉(zhuǎn)換中弓摘,進(jìn)行這種狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時(shí)間。所以 Synchronize 在 Java 的一個重量級操作痕届。
重入鎖(ReentrantLook)是 Lock 接口的常見實(shí)現(xiàn)韧献,相比 Synchronize 增加了一些高級功能:
- 等待可中斷:當(dāng)持有鎖的線程長時(shí)間不釋放鎖的時(shí)候末患,等待的線程可以選擇放棄等待,改為處理其他事情锤窑。
- 公平鎖:多個線程在等待鎖的時(shí)候璧针,會按照申請鎖的時(shí)間順序來依次獲取鎖,即公平鎖渊啰。Synchronize 中的鎖是非公平的探橱,ReentrantLook 默認(rèn)也是非公平的,不過可以通過構(gòu)造函數(shù)進(jìn)行設(shè)置绘证,公平鎖容易導(dǎo)致 ReentrantLook 的性能急劇下降隧膏,會明顯影響吞吐量。
- 鎖綁定多個條件: 一個 ReentrantLook 對象可以綁定多個 Condition 對象嚷那。
JDK 6之前胞枕,ReentrantLook 的性能是優(yōu)于 Synchronize 的,不過隨著 Synchronize 的鎖優(yōu)化车酣,現(xiàn)在兩種性能以及基本無差曲稼,而卻 Java 虛擬機(jī)更容易針對 Synchronize 進(jìn)行優(yōu)化。
2. 非阻塞同步
互斥同步屬于一種悲觀策略湖员,總認(rèn)為會存在數(shù)據(jù)競爭贫悄,需要進(jìn)行加鎖,這會導(dǎo)致用戶態(tài)到核心態(tài)轉(zhuǎn)換娘摔、維護(hù)鎖計(jì)數(shù)器和檢查是否有被阻塞的線程需要被喚醒等開銷窄坦。
非阻塞同步是基于沖突檢測的樂觀并發(fā)策略,先進(jìn)行數(shù)據(jù)操作凳寺,如果沒有出現(xiàn)其他線程爭用共享數(shù)據(jù)鸭津,那操作直接成功了;如果共享數(shù)據(jù)的確被爭用肠缨,產(chǎn)生了沖突逆趋,再進(jìn)行其他補(bǔ)救措施。
樂觀并發(fā)策略需要硬件指令的支持晒奕,因?yàn)槲覀冃枰僮骱蜎_突檢測這兩個步驟具備原子性闻书,這類指令常用的有:
- 測試并設(shè)置(Test-and-Set)
- 獲取并增加(Fetch-and-Increment)
- 交換(Swap)
- 比較并交換(Compare-and-Swap,即 CAS)
- 加載鏈接/條件儲存(Load-Linked / Store-Conditional脑慧,即 LL/SC)
上述指令的處理過程都是一個原子操作魄眉,執(zhí)行期間不會被其他線程中斷。
注意:CAS 無法確認(rèn)變量被改了之后又被改回來的問題
3. 無同步方案-線程本地存儲
每一個線程 Thread 對象都有一個 ThreadLocalMap 對象闷袒,可以通過它把數(shù)據(jù)跟線程綁定坑律,則線程之間的數(shù)據(jù)就不會存在競爭了。
鎖優(yōu)化
自旋鎖與自適應(yīng)自旋鎖
由于阻塞導(dǎo)致用戶態(tài)到核心態(tài)的性能開銷囊骤,和統(tǒng)計(jì)上發(fā)現(xiàn)在許多應(yīng)用中共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的時(shí)間晃择,為了不在這短暫的時(shí)間去阻塞和恢復(fù)線程冀值,我們可以讓本來需要阻塞的線程改為執(zhí)行一個忙循環(huán)(自旋),以等待持有鎖的線程處理完宫屠,這就是自旋鎖池摧。本質(zhì)也是基于認(rèn)為等待時(shí)間會很短,屬于樂觀策略的一種激况。
自旋鎖的缺點(diǎn)是會占用處理器的時(shí)間,同時(shí)如果持有鎖的線程遲遲不釋放膘魄,就會造成自旋時(shí)間過長乌逐,白白消耗處理器資源。自旋鎖默認(rèn)自旋的次數(shù)是十次创葡,超過次數(shù)就會走傳統(tǒng)的阻塞方式掛起線程浙踢。
自適應(yīng)自旋鎖是對自旋鎖的優(yōu)化,自旋的時(shí)間不再固定灿渴,會根據(jù)性能監(jiān)控信息以及上一次自旋等待是否成功獲得鎖等進(jìn)行自旋時(shí)間的優(yōu)化洛波,會隨著程序的運(yùn)行進(jìn)行自適應(yīng)。
鎖消除
即時(shí)編譯器會對不存在數(shù)據(jù)競爭的同步代碼的鎖進(jìn)行消除骚露。
鎖粗化
如果虛擬機(jī)探測到有一串零碎的操作都對同一個對象加鎖蹬挤,將會把鎖同步的范圍擴(kuò)展到整個操作序列的外部,比如循環(huán)內(nèi)提到循環(huán)外棘幸。
輕量鎖
輕量鎖并不是用來代替重量級鎖的焰扳,他設(shè)計(jì)的初衷是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗误续。
輕量級鎖的工作過程:在代碼即將進(jìn)入同步塊的時(shí)候吨悍,如果此對象沒有被鎖定(鎖標(biāo)志為“01”狀態(tài)),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間蹋嵌,用于存儲鎖對象目前的 Mark Word 的拷貝(官方稱為 Displaced Mark Word)育瓜,這時(shí)候線程堆棧與對象頭的狀態(tài)如圖
然后,虛擬機(jī)將使用 CAS 操作嘗試把對象的的 Mark Word 更新為指向 Lock Record 的指針栽烂。如果這個更新動作成功了躏仇,即代表改線程擁有了這個對象的鎖,并且對象 Mark Word 的鎖標(biāo)志位將轉(zhuǎn)變?yōu)椤?0”愕鼓,表示此對象處于輕量級鎖定狀態(tài)钙态。
如果這個更新操作失敗了,就意味著至少存在一條線程與當(dāng)前線程競爭獲取該對象的鎖菇晃。當(dāng)出現(xiàn)兩條線程以上爭用同一個鎖的情況册倒,那輕量級鎖就不在有效,必須要膨脹為重量級鎖磺送,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”驻子。
輕量級鎖解鎖過程也同樣是通過 CAS 操作來進(jìn)行的灿意,如果對象的 Mark Word 仍然指向線程的鎖記錄,那就用 CAS 操作把對象當(dāng)前的 Mark Word 和線程中復(fù)制的 Displaced Mark Word 替換回來崇呵。假如能夠替換成功缤剧,那即解鎖成功,如果替換失敗域慷,則說明有其他線程嘗試過獲取該鎖荒辕,就要在釋放鎖的同時(shí),喚醒被掛起的線程犹褒。
輕量級鎖能夠提升程序同步性能的依據(jù)是“對于絕大部分的鎖抵窒,在整個同步周期內(nèi)都是不存在競爭的”這一經(jīng)驗(yàn)法則,也是一種樂觀策略叠骑。如果沒有競爭李皇,通過 CAS 操作成功避免了互斥同步的開銷,如果確實(shí)存在競爭宙枷,除了互斥同步本身開銷外掉房,還額外發(fā)生了 CAS 操作的開銷,所以競爭頻繁的時(shí)候開銷比重量級鎖還大慰丛。
偏向鎖
輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量卓囚,偏向鎖是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不去做了诅病。
當(dāng)鎖對象第一次被線程獲取的時(shí)候捍岳,虛擬機(jī)將會把對象頭中的標(biāo)志位設(shè)置為“01”、把偏向模式設(shè)置為“1”睬隶,表示進(jìn)入偏向模式锣夹。同時(shí)使用 CAS 操作把獲取到這個鎖的線程的 ID 記錄在對象的 Mark Word 之中。如果 CAS 操作成功苏潜,持有偏向鎖的線程以后每次進(jìn)入這個鎖相關(guān)的同步塊時(shí)银萍,虛擬機(jī)都可以不再進(jìn)行任何同步操作(例如加鎖、解鎖及對 Mark Word 的更新操作等)恤左。
一旦出現(xiàn)另外一個線程去嘗試獲取這個鎖的情況贴唇,偏向模式就馬上宣告結(jié)束。根據(jù)鎖對象目前是否處于被鎖定的狀態(tài)決定是否撤銷偏向(偏向模式設(shè)置為“0”)飞袋,撤銷后標(biāo)志位恢復(fù)到未鎖定(標(biāo)志位位“01”)或輕量級鎖定(標(biāo)志位“00”)的狀態(tài)戳气,后續(xù)同步操作就按照上面介紹的輕量級鎖那樣去執(zhí)行。
由于偏向鎖在對象的 Mark Word 中存儲線程 ID 的位置是用于存放哈希碼的瓶您,所以一旦對象計(jì)算過哈希碼后,就再也無法進(jìn)入偏向鎖狀態(tài)了,同時(shí)如果對象處于偏向鎖狀態(tài)呀袱,當(dāng)收到哈希碼計(jì)算請求時(shí)贸毕,偏向狀態(tài)會立即撤銷,并且鎖會膨脹為重量級鎖夜赵。
偏向鎖可以提高帶有同步但無競爭的程序性能明棍,并非總是對程序有利,如果程序中大多數(shù)的鎖都總是被多個不同的線程訪問寇僧,那偏向模式就是多余的摊腋。
偏向鎖 -> 輕量級鎖 -> 重量級鎖
每一種同步模式都有它適合的場景,需要具體來分析嘁傀,不能片面思考歌豺。