
10266.jpg
java虛擬機
1 意義
- 屏蔽各個硬件平臺和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓java程序在各種平臺下都能達到一致的內(nèi)存訪問效果
2 運行時數(shù)據(jù)區(qū)組成
2-1 線程私有
程序計數(shù)器
- 當前線程所執(zhí)行的字節(jié)碼的行號指示器:<ol><li>如果正在執(zhí)行的是java方法,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;</li><li>如果正在執(zhí)行natvie方法,計數(shù)器值為空(undefined)</li></ol>
作用
- java虛擬機字節(jié)碼解釋器通過改變這個計數(shù)器的值來選取下一條要執(zhí)行的字節(jié)碼指令
- 沒有規(guī)定任何outofmemoryerror的情況
虛擬機棧
- 用途:為jvm執(zhí)行java方法服務(wù)
- 編譯期間完成分配
<p>結(jié)構(gòu):棧幀:<b><u>局部變量表</u></b>、操作數(shù)棧、動態(tài)鏈接、方法出口</p>
- 基本類型變量,(boolean,byte,char,short,int,float,long,double)
- 對象句柄
- 方法參數(shù)
- 方法的局部變量
- 兩種異常:stackoverflowerror、outofmemoryerror; -xss設(shè)置棧大小
本地方法棧
- 用途:為虛擬機使用到的native方法服務(wù)
- 兩種異常:stackoverflowerror、outofmemoryerror
2-2 線程共有
方法區(qū)(自用)
- 用途:用于存儲已被jvm加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)
運行時常量池
內(nèi)容:存放編譯產(chǎn)生的字面量(<b>常量final</b>)和<b>符號引用</b>
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
特點:運行時常量池相對于class文件常量池的一個重要特征是具備<b>動態(tài)性</b>
- java語言并不要求常量一定只有編譯期才能產(chǎn)生,運行期間也能將新的常量放入池中
- 異常:(無法擴展)outofmemoryerror
方法區(qū)的gc
- 參考垃圾對象的判定
gc堆(可及)
- 目的:存放對象實例和數(shù)組
分代處理
- 目的:更好的回收內(nèi)存和更快的分配內(nèi)存
新生代
- eden空間
- from survivor空間
- to survivor空間等
- 老年代
- 空間結(jié)構(gòu):邏輯連續(xù)的空間,物理可不連續(xù)
- 優(yōu)先分配tlab(thread local allocation buffer),減少加鎖,提高效率
- gc管理的主要區(qū)域
異常:如果在堆中沒有完成內(nèi)存分配,并且堆也無法擴展時,會拋出outofmemoryerror
- -xms初始堆大小 -xmx最大堆大小
3 對象的創(chuàng)建
3-1 檢查參數(shù)是否在常量池中定位到一個類的符號引用;該類是否被加載、解析、初始化過
- 若沒有做進行類加載
3-2 若有則分配內(nèi)存
內(nèi)存絕對規(guī)整
- 用“指針碰撞”來分配內(nèi)存
內(nèi)存不規(guī)整
- 用“空閑列表”來分配內(nèi)存
線程安全
對分配內(nèi)存空間的工作進行同步處理
- 采用cas+失敗重試的方式保證更新操作的原子性
每個線程分配一塊本地線程分配緩沖區(qū)
tlab
- -xx:+/-usetlab
- 3、始化已分配內(nèi)存為零值(保證類型不賦初值可以使用)
- 4、上面工作完成后,執(zhí)行init方法按照程序員意愿初始化對象
4 對象創(chuàng)建流程圖

pn_1350044_txt_1074.png
5 對象的內(nèi)存布局
5-1 對象頭
- 存儲運行時數(shù)據(jù)
- 存儲類型指針
5-2 實例數(shù)據(jù)
- 是對象真正存儲的有效信息
5-3 對齊填充
- 起占位符的作用
6 對象的訪問定位
6-1 使用句柄
- 堆中有句柄池,存儲到實例數(shù)據(jù)和類型數(shù)據(jù)的指針;棧中的引用指向?qū)ο蟮木浔刂?/li>
優(yōu)點
- <ol><li>reference中地址相對穩(wěn)定;</li><li>對象被移動(gc時)時只會改變句柄中的實例數(shù)據(jù)指針</li></ol>
6-2 直接指針
-
棧中的引用直接存儲對象地址,到方法區(qū)中類型數(shù)據(jù)的指針包含在對象實例數(shù)據(jù)中
pn_1350044_txt_1098.png
優(yōu)點
- 訪問速度快,節(jié)省了一次指針定位的開銷
7 oom異常
7-1 虛擬機棧和本地方法棧溢出
線程請求棧深度大于最大深度stackoverflowerror
- 設(shè)置-xss128k,在單線程下,通過不斷調(diào)用遞歸方法。
新線程拓展棧時無法擴展出現(xiàn)outofmemoryerror錯誤
- 不斷創(chuàng)建新線程,并讓創(chuàng)建的線程不斷運行
- -xss
7-2 方法區(qū)和運行時常量池溢出
java.lang.outofmemoryerror后會跟permgen space
- 不斷創(chuàng)建新的字符串常量,并添加到list中
- -xx:permsize和-xx:maxpermsize
7-3 堆溢出
java.lang.outofmemoryerror:java heap space
內(nèi)存泄漏
- 通過不斷創(chuàng)建新對象,并放入list中,保證gcroots到對象之間路徑可達
- 內(nèi)存溢出
- -xms -xmx
7-4 本機直接內(nèi)存溢出
- 在heap dump文件中沒有明顯異常
- -xx
8 垃圾對象的判定
8-1 對象的引用
強引用
- 存在就不回收
軟引用
將要發(fā)生內(nèi)存溢出之前
- 實現(xiàn)緩存
弱引用
下一次垃圾回收
- 回調(diào)函數(shù)中防止內(nèi)存泄露
虛引用
對對象的生存時間沒有影響
- 能在這個對象被收集器回收時收到一個系統(tǒng)通知
8-2 引用計數(shù)法
- 難以解決對象之間相互循環(huán)引用的問題
8-3 根搜索算法
- 從gc roots向下搜索建立引用鏈;一個對象如果到gc roots沒有任何引用鏈相連時,證明對象不可用
gc roots
- 虛擬機棧中引用的對象
- 本地方法棧中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
8-4 堆中垃圾回收過程
- 1、如果對象在進行可達性分析后發(fā)現(xiàn)沒有與gc roots相連接的引用鏈,那它將會被第一次標記
- 2、斷對象是否有必要執(zhí)行finalize()方法,(沒有覆蓋,被調(diào)用過,都沒有必要執(zhí)行),放入f-queue隊列
- 3、放入f-queue中,進行第二次標記
- 4、被拯救的移除隊列,被兩次標記的被回收
8-5 方法區(qū)中垃圾回收
廢棄常量
- 沒有任何一個對象引用常量池中的“abc”常量
無用的類(滿足條件可以被回收,非必然)
- 1、該類所有的實例都已經(jīng)被回收
- 2、加載該類的加載器被回收
- 3、該類對應(yīng)的javalang.class對象沒有在任何地方被引用,無法通過反射訪問該類的方法
9 垃圾回收算法
9-1 標記-清除算法
- 首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象
- <ul><li>效率問題,標記和清除兩個過程的效率都不高</li><li>空間問題,產(chǎn)生大量不連續(xù)的內(nèi)存碎片,連續(xù)內(nèi)存不足會再次觸發(fā)gc</li></ul>
9-2 復(fù)制算法
- 將內(nèi)存等分,每次用一塊,當這塊內(nèi)存用完了,就將活著的對象復(fù)制到另一塊,然后把前者清空
- <ol><li>對象存活率較高時就要進行較多的復(fù)制操作,效率將會降低 </li><li>空間利用率低</li></ol>
9-3 標記-整理算法
- 所有存活的對象移向一端,然后直接清理掉端邊界以外的內(nèi)存
9-4 分代收集算法
新生代
- 復(fù)制算法
老年代
- 標記-整理或標記-清除
9-5 hotspot算法
枚舉根結(jié)點
- 當執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏的檢查完所在執(zhí)行上下文和全局的引用位置,在hotspot的實現(xiàn)中,使用一組稱為oopmap的數(shù)據(jù)結(jié)構(gòu)來存放對象引用
安全點
- 在這些特定的位置,線程的狀態(tài)可以被確定
位置
- 方法調(diào)用指令
- 循環(huán)跳轉(zhuǎn)指令
- 異常跳轉(zhuǎn)指令
中斷方式
搶占式
- gc發(fā)生時,首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復(fù)線程,讓它跑在安全點上
主動式
- 設(shè)置一個標志,各個線程執(zhí)行時主動輪詢這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起
安全區(qū)域
- 背景:線程sleep狀態(tài)或者blocked狀態(tài)的時候,無法響應(yīng)jvm中斷,走到安全的地方,jvm也不能等他們,這樣就無法進行g(shù)c
- 安全區(qū)域是指在一段代碼中,引用關(guān)系不會發(fā)生變化,這個區(qū)域中的任何地方開始gc都是安全的。
10 垃圾收集器
10-1 serial收集器
特點
- 新生代收集器
- 采用復(fù)制算法
- 單線程收集
- 進行垃圾收集時,必須暫停所有工作線程,直到完成
應(yīng)用場景
- 是hotspot在client模式下默認的新生代收集器
- 簡單高效(與其他收集器的單線程相比)
- 對于限定單個cpu的環(huán)境來說,serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;
參數(shù)設(shè)置
- "-xx:+useserialgc":添加該參數(shù)來顯式的使用串行垃圾收集器;
10-2 parnew收集器
特點
- 新生代收集器
- 采用復(fù)制算法
- 除了多線程外,其余的行為、特點和serial收集器一樣
應(yīng)用場景
- server模式下,parnew收集器是一個非常重要的收集器
- 單個cpu環(huán)境中,不會比serail收集器有更好的效果,因為存在線程交互開銷
參數(shù)設(shè)置
- "-xx:+useconcmarksweepgc":指定使用cms后,會默認使用parnew作為新生代收集器;
- "-xx:+useparnewgc":強制指定使用parnew;
- "-xx:parallelgcthreads":指定垃圾收集的線程數(shù)量,parnew默認開啟的收集線程與cpu的數(shù)量相同;
10-3 parallel scavenge收集器
特點
- 新生代收集器
- 采用復(fù)制算法
- 多線程收集
- cms等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程的停頓時間; 而parallel scavenge收集器的目標則是達一個可控制的吞吐量(throughput)
應(yīng)用場景
- 高吞吐量為目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間
- 當應(yīng)用程序運行在具有多個cpu上,對暫停時間沒有特別高的要求時,即程序主要在后臺進行計算,而不需要與用戶進行太多交互
參數(shù)設(shè)置
- "-xx:maxgcpausemillis"控制最大垃圾收集停頓時間
- "-xx:gctimeratio" 設(shè)置垃圾收集時間占總時間的比率
- "-xx:+useadptivesizepolicy"
10-4 serial olc收集器
特點
- 針對老年代
- 采用"標記-整理"算法(還有壓縮,mark-sweep-compact)
- 單線程收集
應(yīng)用場景
- 主要用于client模式
在server模式中
- 在jdk1.5及之前,與parallel scavenge收集器搭配使用(jdk1.6有parallel old收集器可搭配)
- 作為cms收集器的后備預(yù)案,在并發(fā)收集發(fā)生concurrent mode failure時使用
10-5 parallel old收集器
特點
- 針對老年代
- 采用"標記-整理"算法
- 多線程收集
應(yīng)用場景
- jdk1.6及之后用來代替老年代的serial old收集器
- 特別是在server模式,多cpu的情況下
- 在注重吞吐量以及cpu資源敏感的場景,就有了parallel scavenge加parallel old收集器的"給力"應(yīng)用組合
參數(shù)設(shè)置
- "-xx:+useparalleloldgc":指定使用parallel old收集器
10-6 cms收集器
特點
- 針對老年代
- 基于"標記-清除"算法(不進行壓縮操作,產(chǎn)生內(nèi)存碎片)
- 以獲取最短回收停頓時間為目標
- 并發(fā)收集、低停頓
- 需要更多的內(nèi)存(看后面的缺點)
應(yīng)用場景
- 與用戶交互較多的場景
- 希望系統(tǒng)停頓時間最短,注重服務(wù)的響應(yīng)速度
- 以給用戶帶來較好的體驗
- 如常見web、b/s系統(tǒng)的服務(wù)器上的應(yīng)用
參數(shù)設(shè)置
- "-xx:+useconcmarksweepgc":指定使用cms收集器
運行過程
- 初始標記
- 并發(fā)標記
- 重新標記
- 并發(fā)清除
缺點
- 對cpu資源非常敏感
- 無法處理浮動垃圾,可能出現(xiàn)"concurrent mode failure"失敗
- 產(chǎn)生大量內(nèi)存碎片
10-7 g1收集器
特點
并行與并發(fā)
- gc收集線程并行
- 用戶線程與gc線程并發(fā)
- 分代收集,收集范圍包括新生代和老年代
- 空間整合:結(jié)合多種垃圾收集算法,空間整合,不產(chǎn)生碎片
- 可預(yù)測的停頓:低停頓的同時實現(xiàn)高吞吐量
應(yīng)用場景
- 面向服務(wù)端應(yīng)用,針對具有大內(nèi)存、多處理器的機器
- 最主要的應(yīng)用是為需要低gc延遲,并具有大堆的應(yīng)用程序提供解決方案
運行過程(不計remembered set操作)
初始標記
標記gc root能直接關(guān)聯(lián)到的對象
- 需要停頓用戶線程
并發(fā)標記
對堆中對象可達性分析
- 并發(fā)執(zhí)行
最終標記
修正并發(fā)標記中因用戶線程運行發(fā)生改變的標記記錄
- 需要停頓線程
篩選回收
對region的回收價值和成本排序,根據(jù)參數(shù)指定回收計劃
- 可以并發(fā)
參數(shù)設(shè)置
- "-xx:+useg1gc":指定使用g1收集器
- "-xx:initiatingheapoccupancypercent":當整個java堆的占用率達到參數(shù)值時,開始并發(fā)標記階段;默認為45
- "-xx:maxgcpausemillis":為g1設(shè)置暫停時間目標,默認值為200毫秒
- "-xx:g1heapregionsize":設(shè)置每個region大小,范圍1mb到32mb;目標是在最小java堆時可以擁有約2048個region
10-8 內(nèi)存分配和回收策略
對象優(yōu)先在eden分配
- 大多數(shù)情況下,對象在新生代eden區(qū)中分配,當eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次minor gc
大對象直接進入老年代
- 所謂大對象,就是需要大量連續(xù)內(nèi)存空間的對象,最典型的大對象就是那種很長的字符串以及數(shù)組
- 長期存活的對象將進入老年代
動態(tài)對象年齡判定
- 如果在survivor空間中相同年齡所有對象大小的總和大于survivor空間的一半,年齡大于或等于該年齡的對象就可以進入老年代,無須等到maxtenuringthreshold中要求的年齡
空間分配擔保
- minor gc之前,jvm檢查老年代最大連續(xù)空間是否大于新生代所有對象的空間,成立則確保minor gc安全
- 不成立,參看參數(shù)handlepromotionfailure是否允許擔保失敗,允許則檢查老年代最大連續(xù)空間是否大于歷次晉升的對象的平均大小,大于則嘗試minor gc
- 否則,進行full gc
- 內(nèi)存管理機制
- 垃圾收集器
- 內(nèi)存分配策略
11 虛擬機類加載機制
11-1 類加載的時機
聲明周期
- 加載、驗證、準備、解析、初始化、使用和卸載7個階段,其中驗證、準備、解析3個部分統(tǒng)稱為連接
以下情況對類進行初始化
- 遇到new、getstatic、putstatic或invokestatic這四條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化
- 使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化
- 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)父類的初始化
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類,虛擬機會先初始化這個主類
- 當使用jdk1.7的動態(tài)語言支持時,如果一個java.lang.invoke.methodhandle實例最后的解析結(jié)果bef_getstatic、bef_putstatic、bef_invokestatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化
11-2 類加載的過程(5)
加載
完成3件事
- 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的java.lang.class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
類的來源
- 從zip包中讀取,這很常見,最終成為日后jar、ear、war格式的基礎(chǔ)
- 從網(wǎng)絡(luò)中獲取,這種場景最典型的應(yīng)用就是applet
- 運行時計算生成,這種場景使用得最多的就是動態(tài)代理基礎(chǔ)
- 由其它文件生成,典型場景就是jsp應(yīng)用,即由jsp文件生成賭贏的class類
- 從數(shù)據(jù)庫中讀取,這種場景相對的少些,例如有些中間件服務(wù)器可以選擇把程序安裝到數(shù)據(jù)庫中來完成程序代碼在集群間的分發(fā)
驗證
- 驗證是連接階段的第一步,這一階段的目的就是為了確保class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全
驗證項
- 文件格式驗證
- 元數(shù)據(jù)驗證
- 字節(jié)碼驗證
- 符號引用驗證
準備
- 準備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都講在方法區(qū)匯總進行分配
解析
- 解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程
解析動作分類
- 類或接口的解析
- 字段解析
- 類方法解析
- 接口方法解析
初始化
- 類初始化是類加載過程中的最后一步,這時才真正開始執(zhí)行類中定義的java程序代碼
11-3 類加載器
- 虛擬機設(shè)計團隊把類加載階段中的“通過一個類的全限定名來獲取此類的二進制字節(jié)流”這個動作放到j(luò)ava虛擬機外部去實現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類,實現(xiàn)這個動作的代碼模塊稱為類加載器
- 比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個class文件,被同一個虛擬機加載,只要它們的類加載器不同,那這兩個類就必定不相等
11-4 雙親委派模型
分類
啟動類加載器
- 這個類加載器使用c++語言實現(xiàn),是虛擬機自身的一部分
其他類加載器
- 這些類加載器由java語言實現(xiàn),獨立于虛擬機外部,并且全都繼承自抽象類java.lang.classloader
- java內(nèi)存模型與線程
- 線程安全
12 java內(nèi)存模型與線程
12-1 硬件效率和一致性
硬件使用了很多手段, 目的是提高硬件執(zhí)行效率
- 為了解決處理器和內(nèi)存速度的矛盾, 加多級緩存
- 為了使處理器內(nèi)部運算單元盡量充分使用, 處理器對輸入代碼進行亂序執(zhí)行(out-of-order execution)
實際jvm虛擬機中也是用了類似的技術(shù)手段來提升性能
- 線程的工作內(nèi)存 - 硬件多級緩存
- 編譯器指令重排序(instruction reorder) - 亂序執(zhí)行
使用優(yōu)化問題所帶來的問題
多級緩存引入了緩存一致性問題
- 使用緩存一致性協(xié)議來解決, 如: msi/ mesi/ mosi/ synapse/ firefly/ dragon protocol 等
12-2 java內(nèi)存模型(jave memory model, jmm);
可參考jsr-133(java memory model and thread specification revision)
- 主要目標是定義程序中各個變量的訪問規(guī)則, 這里的變量指的是實例字段/ 靜態(tài)字段/ 數(shù)組對象的元素 等, 可以被線程共享的變量
- @jvm沒有直接像c/c++一樣直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型, 增加了跨平臺能力;@內(nèi)存模型的定義必須足夠嚴謹, 保證并發(fā)內(nèi)存操作不會產(chǎn)生奇異;@但是定義也必須足夠?qū)捤? 使得虛擬機的實現(xiàn)有足夠的自由空間去利用硬件的各種特性(寄存器/ 高速緩存/ 指令集中某些特有指令)/;
主內(nèi)存和工作內(nèi)存
主內(nèi)存
- jvm堆中的一部分
工作內(nèi)存
- 每個線程都有自己的工作內(nèi)存, 工作內(nèi)存中保存被該內(nèi)存使用的變量的主內(nèi)存副本
- 線程的讀寫操作必須對工作內(nèi)存操作, 無法對主內(nèi)存直接進行讀寫操作
- 線程無法直接讀寫其他線程的工作內(nèi)存;線程間的變量值的傳遞需要通過主內(nèi)存來完成
- jvm虛擬機棧的一部分
- 對應(yīng)于硬件, 很可能對應(yīng)了高速緩存甚至寄存器
內(nèi)存間的交互操作
- 主要講了java內(nèi)存模型的原子性
- read/load需要順序執(zhí)行;store/write需要順序執(zhí)行;但是在指令間可以插入其他指令, 如: read a; read b; load a; use a; load b;
8中原子操作還需要遵從以下規(guī)則
- 不允許read和load / store和write單獨出現(xiàn);即不允許變量從主存讀取了工作內(nèi)存不接受 或者 從工作內(nèi)存回寫了主存不接受的情況
- 不允許一個線程丟棄它最近的assign操作;即變量在工作內(nèi)存中改變了之后必須把該變化同步到主存
- 不允許一個線程無原因地(沒有發(fā)生過assign操作)把數(shù)據(jù)從工作內(nèi)存同步到主存;???防止對主存的無意義刷新更新其他線程已經(jīng)修改的值(大霧);即對一個變量使用store之前必須經(jīng)過assign操作
- 變量只能在主存中"誕生", 不允許在工作內(nèi)存中使用一個未經(jīng)初始化(load/assign)的變量;即對一個變量使用use/ store之前必須經(jīng)過 load/assign操作
- 一個變量在同一時刻只允許一個線程對其進行l(wèi)ock操作, 但lock可以被同一線程重復(fù)執(zhí)行多次(可重入), 執(zhí)行多次lock后必須執(zhí)行相同次數(shù)的unlock操作, 變量才能被解鎖
- 如果對一個變量執(zhí)行l(wèi)ock操作, 將會清空工作內(nèi)存中此變量的值, 在執(zhí)行引擎使用該變量前, 需要重新執(zhí)行l(wèi)oad或者assign操作初始化變量的數(shù)值
- 如果一個變量事先沒有被lock操作鎖定, 那么就不允許對它執(zhí)行unlock操作;不允許一個線程去unlock一個被其他線程鎖住的變量
- 對一個變量執(zhí)行unlock操作之前, 必須將該變量同步回主存中(執(zhí)行store write操作)
- 這些規(guī)則(加上對volatile的一些特殊規(guī)定)確定了那些內(nèi)存訪問操作在并發(fā)下是安全的
volatile的特殊規(guī)定
保證volatile對所有線程的可見性;即 當某線程改變變量值后, 新值對其他線程可立即得知;
- 可見性不等同于一致性;
- volatile在符合以下所有情況時可以保證并發(fā)安全:1. 運算結(jié)果并不依賴變量的當前值;2. 確保只有一個線程可以修改變量;3.變量不需要與其他的狀態(tài)變量同時參與不變約束;
volatile變量可以禁止指令沖排序優(yōu)化
- 利用了內(nèi)存屏障, 指令重排序無法越過內(nèi)存屏障
對long/double的特殊規(guī)定
- long/double的非原子性協(xié)議:允許虛擬機將非volatile修飾的64位數(shù)據(jù)的讀寫操作劃為兩次32位的操作來進行
- 在目前商用虛擬機中64位數(shù)據(jù)的讀寫也是原子性的
原子性/可見性/有序性
- 原子性:由java內(nèi)存模型直接保證的原子性操作包括lock/unlock/read/load/use/assign/store/write
- 可見性:當一個線程修改了共享變量的值, 其他線程能夠立即得知這個修改;java內(nèi)存模型通過在變量修改后將新值同步回主存, 在變量讀取前從主存刷新變量值這種依賴主存為傳遞媒介的方式來實現(xiàn)可見性的; 普通變量和volatile變量都是如此;不過volatile變量的特殊規(guī)定保證了新值能夠立即同步到主存, 以及每次使用前立即從主存刷新; 因此可以說volatile保證了多線程操作時變量的可見性, 但普通變量無法保證這一點;除了volatile變量外, sync同步代碼塊和final也可以保證可見性;
- 有序性:如果在本線程內(nèi)觀察, 所有操作都是有序的; 如果在一個線程中觀察另一個線程, 所有操作都是無序的;前半句指線程內(nèi)表現(xiàn)為串行的語義(whthin-thread as-if-serial semantics);后半句指"指令重排序"現(xiàn)象和"工作內(nèi)存與主內(nèi)存同步延遲"現(xiàn)象;volatile和sync同步代碼塊可以保證有序性;
先發(fā)性原則
- 先行發(fā)生是java內(nèi)存模型中定義的兩項操作間的偏序關(guān)系;如果操作a先行發(fā)生于操作b, 實際指 在發(fā)生b之前, a產(chǎn)生的影響會被b觀察到;
滿足以下任意一規(guī)則, 則不可指令重拍
- 程序次序規(guī)則
- 管理鎖定規(guī)則
- volatile變量規(guī)則
- 線程啟動規(guī)則
- 線程終止規(guī)則
- 線程中斷規(guī)則
- 對象終結(jié)規(guī)則
- 傳遞性
- 時間先后順序和先行發(fā)生原則之間基本沒有太大關(guān)系, 所以衡量并發(fā)安全問題時只按先行發(fā)生原則為準即可;
12-3 java與線程
線程的實現(xiàn)
使用內(nèi)核線程實現(xiàn)
- 內(nèi)核線程定義:由操作系統(tǒng)內(nèi)核(kernel)直接支持;線程切換/調(diào)度/映射到處理器由線程調(diào)度器(thread scheduler)完成;支持多線程的kernel叫做多線程內(nèi)核;
- 輕量級進程定義:程序一般不會直接使用內(nèi)核線程, 而是使用內(nèi)核線程的一種高級接口: 輕量級進程(light weight process);輕量級進程即指我們通常意義所講的線程;輕量級線程和內(nèi)核線程是的關(guān)系是 1 - 1;
- 內(nèi)核線程/輕量級進程的優(yōu)點:成為獨立的調(diào)度單元, 某內(nèi)核線程/輕量級進程阻塞不會影響整個進程(不影響其他線程);
- 內(nèi)核線程/輕量級進程的缺點:線程創(chuàng)建/析構(gòu)/同步都需要系統(tǒng)調(diào)用, 代價較高, 需要在用戶態(tài)和內(nèi)核態(tài)間切換;會消耗內(nèi)核資源(如 內(nèi)核線程的棧空間), 因此一個系統(tǒng)支持內(nèi)核線程/輕量級進程數(shù)是有限的;
使用用戶線程實現(xiàn)
- 用戶線程定義:廣義上 - 非內(nèi)核線程, 如果這樣看輕量級線程也屬于用戶線程;狹義上 - 完全建立在用戶空間的線程庫上, kernel無法感知用戶線程存在, 線程的創(chuàng)建/銷毀/同步/調(diào)度也全部在用戶態(tài)中完成;和進程的關(guān)系是n-1;
- 用戶線程優(yōu)點:如果實現(xiàn)得當, 速度快且低消耗;支持規(guī)模更大的線程數(shù)量(部分高性能數(shù)據(jù)庫的多線程使用用戶線程實現(xiàn));
- 用戶線程缺點:實現(xiàn)極復(fù)雜;某個線程阻塞會對整個進程造成影響;
使用用戶線程加輕量級進程實現(xiàn)
- 結(jié)合使用兩者優(yōu)點, 用戶線程和輕量級線程數(shù)量比不定, 為n-m;
java線程的實現(xiàn)
- 1.2前, 用戶線程
- 從1.2開始, 使用操作系統(tǒng)原生線程模型來實現(xiàn), 所以現(xiàn)在使用那種線程和操作系統(tǒng)有關(guān);
- 對于sun jdk來說: windows和linux使用1 - 1線程模型;solaris因為支持n - m模型, 所以可以使用專有虛擬機參數(shù)設(shè)置;
java線程調(diào)度
協(xié)同式調(diào)度
- 調(diào)度問題由線程自身控制
- 可以避免并發(fā)問題
- 如果某線程阻塞, 可能會造成整個無法運行
搶占式調(diào)度
- 調(diào)度由系統(tǒng)(內(nèi)核線程/輕量級進程)或者其他線程調(diào)度程序(用戶線程)控制
- 線程可主動讓出調(diào)度時間, 但無法主動獲取調(diào)度時間; (java使用thread.yield()讓出)
搶占式調(diào)度可以設(shè)置線程執(zhí)行優(yōu)先級
- java中有10個優(yōu)先級
- 操作系統(tǒng)中線程優(yōu)先級并非10個, 所以有不對稱的對應(yīng)關(guān)系;多的如solaris中有2^32個優(yōu)先級, 但是windows中只有7個;
- 不應(yīng)該太多,java中的優(yōu)先級, 操作系統(tǒng)對系統(tǒng)調(diào)度有優(yōu)化, 如某個線程特別"勤奮", 會被越過優(yōu)先級分配額外的執(zhí)行時間;
線程狀態(tài)和轉(zhuǎn)換
新建
- 創(chuàng)建未啟動
運行
- java中的運行中, 對應(yīng)kernel中的runing和ready, 即運行中或者等待kernel分配執(zhí)行時間
無限期等待
- 需要被顯示喚醒才能繼續(xù)執(zhí)行
切換到此狀態(tài)的方法
- object.wait()
- thread.join()
- locksupport.park()
限期等待
切換到此狀態(tài)的方法
- thread.sleep()
- object.wait timeout
- thread.join timeout
- locksupport.parknanos()
- locksupport.parkuntil()
阻塞
- 在等待獲取排它鎖, 其他線程放棄排它鎖時才可能結(jié)束阻塞狀態(tài)
- 結(jié)束
13 線程安全和鎖優(yōu)化
13-1 線程安全
- 定義:當多個線程訪問一個對象時, 如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行, 也不需要進行額外的同步, 或者在調(diào)用方法行進行任何其他的協(xié)調(diào)操作, 調(diào)用這個對象的行為都可以獲得正確的結(jié)果, 那這個對象是線程安全的.
java語言中的線程安全
- 需要考慮線程安全的前提是: 多個線程間存在共享數(shù)據(jù)
按照線程的安全程度分類
不可變
- final修飾的基礎(chǔ)類型
- 屬性全部為final修飾的對象
絕對線程安全
- 完全滿足<上面對線程安全的定義>
- java中大部分聲明線程安全的對象并非"絕對"線程安全;如: vector兩個線程分別進行增和刪的操作, 會拋出arrayindexoutofboundsexception
相對線程安全
- 保證對該對象單獨的操作是線程安全的, 但是對特定順序的連續(xù)調(diào)用, 就可能需要在調(diào)用時使用額外的同步手段來保證正確性
- 如: vector/hashtable/collections.synchronizedcollection()包裝的集合 等
線程兼容
- 對象并非線程安全, 但可以通過在調(diào)用端正確地使用同步手段來保證安全
- 如: arraylist/hashtable 等
線程對立
- 無論調(diào)用端是否采取同步措施, 都無法保證多鮮橙環(huán)境下的安全
- 在java中, 這種代碼很少出現(xiàn), 而且通常有害, 應(yīng)避免
- 如: thread的suspend()和resume();如果兩線程同時持有一個線程對象, 一個嘗試去中斷線程, 另一個嘗試恢復(fù)線程, 即便使用同步手段, 目標線程都是存在死鎖的風險;
線程安全的實現(xiàn)方法
互斥(阻塞)同步
- 屬于悲觀鎖
synchronized關(guān)鍵字 - 需要reference類型參數(shù)來指明鎖定/解鎖的對象
- 編譯后會生成monitorenter和monitorexit兩個字節(jié)碼指令, 來運行加鎖/解鎖操作;monitorenter: 首先嘗試獲取鎖, 如果對象未被鎖定, 或者當前線程已經(jīng)擁有該對象的鎖(可重入特點), 把鎖計數(shù)器+1;monitorexit: 鎖計數(shù)器-1, 當計數(shù)器為0時, 鎖被釋放;
如果沒有明確指定哪個對象來加鎖 - static方法: 類的class對象
- 普通方法: 當前實例, 即this
- sync映射到了kernel中的輕量級進程, 加鎖/解鎖操作實際都需要在 內(nèi)核態(tài)和用戶態(tài)來回切換, 需要耗費較多的處理器時間, 因此也叫做重量級鎖;虛擬機本身對重量級鎖會進行一些優(yōu)化, 比如: 在通知操作系統(tǒng)前加入一段自旋等待過程, 避免頻繁切入內(nèi)核態(tài);
reentrantlock - 屬于輕量級鎖, 在api層面進行互斥, 相比sycn的優(yōu)點:1. 等待可中斷;2. 公平鎖: 會根據(jù)申請鎖的時間順序來依次得到鎖, 默認構(gòu)造是非公平的;3. 綁定多條件: 可以綁定多個condition對象;4. 1.6之前效率比sync高, 從1.6之后效率相差不大;
非阻塞同步
- 屬于樂觀鎖:基于沖突檢測的樂觀并發(fā)策略, 即: 先進行操作, 如果沒有其他線程爭奪資源, 修改成功;如果有資源爭奪, 產(chǎn)生了沖突, 進行補償措施(最常見的是不斷重試, 直到成功為止);
- 這種并非策略不需要將線程掛起, 因此也被稱為 非阻塞同步;
- 使用樂觀鎖的必須條件: 操作和沖突檢測 是原子性的(ia64和x86指令集通過cas指令來實現(xiàn));
- java中原子性操作都是通過sun.misc.unsafe的compareandswap來實現(xiàn)的
- cas指令會引入aba問題, 比如: x線程修改前探測變量值為a, 后其他線程將變量先改為b, 再改為a, 這樣x進行修改時無法探測到變量實際已經(jīng)被修改過了;但是并不影響并發(fā)的正確性;
無同步方案
- 可重入代碼:不依賴存儲在堆上的數(shù)據(jù)和公共的系統(tǒng)資源;用到的狀態(tài)都通過參數(shù)傳入;不調(diào)用非可重入方法;
- 線程本地存儲???
13-2 鎖優(yōu)化
自旋鎖/自適應(yīng)自旋
- 作為阻塞鎖的補充, 避免頻繁在用戶態(tài)和內(nèi)核態(tài)之間切換;
- 在進入系統(tǒng)阻塞前, 進行一段時間的自旋, 自旋一般為次數(shù);自旋次數(shù)也可以通過前一次在同一個鎖上的自旋時間和鎖的擁有者的狀態(tài)來決定, 這種自旋鎖叫自適應(yīng)自旋鎖;
鎖消除
- 即時編譯器(jit)在運行時會對不可能發(fā)生共享資源爭奪的鎖進行消除
- jit對資源是否可能產(chǎn)生爭奪的判斷 是 通過逃逸分析技術(shù)來進行的:如果堆上的所有數(shù)據(jù)都不會逃逸出去而被其他線程訪問到, 那么就可以把他們當做棧上的數(shù)據(jù)對待, 認為是線程私有的;程序員一般可很清楚的明白數(shù)據(jù)是否可以逃逸, 但是在jdk的api中默認存在很多鎖, 比如字符串"+"拼接在編譯后變成使用stringbuffer的append(), 而stringbuffer是帶鎖的, 鎖消除主要針對這些鎖進行消除;
鎖消除
- 前提:在編寫代碼時的原則是盡量減小同步代碼塊的范圍: 使鎖占用時間盡量小, 其他阻塞線程盡快拿到鎖, 以增加效率;
- 但如果一系列的連續(xù)操作都對同一對象反復(fù)加鎖/解鎖, 甚至加鎖操作出現(xiàn)在循環(huán)體內(nèi), 即使沒有線程競爭, 頻繁加鎖/解鎖也會造成不必要的性能損耗;jvm在探測到這種一連串零碎操作使用同一個對象作鎖的情況時, 會將鎖進行粗化到整個操作序列外部;如: stringbuffer的連續(xù)append()操作;
輕量級鎖
- 在沒有鎖競爭時, 將通知kernel進入互斥狀態(tài)的操作替換為了對于對象頭鎖標示位的cas操作;
- 如果存在鎖競爭, 輕量級鎖會膨脹為重量鎖;在這種情況(存在鎖競爭)下, 輕量級鎖因為增加了額外的cas操作, 會比重量級鎖更慢;
偏向鎖
- 將對象頭標示位設(shè)置為偏向鎖, 在沒有鎖競爭時, 再次執(zhí)行時將同步操作消除掉, 連cas也不做了
14 java內(nèi)存模型定義了8種原子性的操作
14-1 lock
- 作用于主內(nèi)存;把一個對象標識為一條線程獨占的狀態(tài)
14-2 unlock
- 作用于主內(nèi)存;把處于鎖定狀態(tài)的對象釋放
14-3 read
- 作用于主內(nèi)存;將主內(nèi)存中的變量傳輸?shù)焦ぷ鲀?nèi)存
14-4 load
- 作用于工作內(nèi)存;把read操作從主內(nèi)存得到的變量值放入工作內(nèi)存的變量副本中
14-5 use
- 作用于工作內(nèi)存;把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎;每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時執(zhí)行該操作
14-6 assign
- 作用于工作內(nèi)存;把從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量;每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行該操作
14-7 store
- 作用于工作內(nèi)存;把工作內(nèi)存中的一個變量傳出到主內(nèi)存
14-8 write
- 作用于主內(nèi)存;把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中
15 jvm調(diào)優(yōu)
15-1 常見參數(shù)
- xms
- xmx
- xmn
- xss
- -xx:survivorratio
- -xx:newratio
- -xx:+printgcdetails
- -xx:parallelgcthreads
- -xx:+heapdumponoutofmemoryerror
- -xx:+useg1gc
- -xx:maxgcpausemillis
15-2 調(diào)優(yōu)思路
確定是否有頻繁full gc現(xiàn)象
1.1 如果full gc頻繁,那么考慮內(nèi)存泄漏的情況
內(nèi)存泄露角度
- 1.使用jps -l命令獲取虛擬機的lvmid
- 2.使用jstat -gc lvmid命令獲取虛擬機的執(zhí)行狀態(tài),判斷full gc次數(shù)
- 3.使用jmap -histo:live 分析當前堆中存活對象數(shù)量
4.如果還不能定位到關(guān)鍵信息,使用 jmap -dump打印出當前堆棧映像dump文件 - jmap -dump:format=b,file=/usr/local/base/02.hprof 12942
- 5.使用mat等工具分析dump文件,一般使用的參數(shù)是histogram或者dominator tree,分析出各個對象的內(nèi)存占用率,并根據(jù)對象的引用情況找到泄漏點
1.2 如果full gc并不頻繁,各個區(qū)域內(nèi)存占用也很正常,那么考慮線程阻塞,死鎖,死循環(huán)等情況
線程角度
- 1.使用jps -l命令獲取虛擬機的lvmid
- 2.使用 jstack 分析各個線程的堆棧內(nèi)存使用情況,如果說系統(tǒng)慢,那么要特別關(guān)注blocked,waiting on condition,如果說系統(tǒng)的cpu耗的高,那么肯定是線程執(zhí)行有死循環(huán),那么此時要關(guān)注下runable狀態(tài)。
- 3.如果還不能定位到關(guān)鍵信息,使用 jmap -dump打印出當前堆棧映像dump文件
- 4.使用mat等工具分析dump文件,一般使用的參數(shù)是histogram或者dominator tree,分析出各個對象的內(nèi)存占用率,并根據(jù)對象的引用情況找到泄漏點。
1.3 如果都不是,考慮堆外存溢出,或者是外部命令等情況
- runtime.getruntime.exec()
