前言
- JVM記憶體模型,JAVA記憶體模型,JAVA物件模型,這些名字相似的模型分別是什麼?
- 常用的垃圾收集方法有哪些?垃圾收集器有哪些?各自有什麼特點?
- JVM如何監控?調優?
- java編譯後生成的class檔案,內部儲存格式是什麼樣的?
- 類載入有哪幾個過程?什麼是雙親委派模型?
- volatile和synchronized有什麼區別?
- 樂觀鎖和悲觀鎖是什麼?CAS原理?
以上問題在《深入理解java虛擬機器》這本書裡都有詳盡的解答。
一. java各版本發展史
java各大版本特性
- 96年釋出1.0版本,代表技術:JVM,Applet,Awt
- 97年釋出1.1版本,代表技術:jar檔案格式,jdbc,javabean,內部類,反射
- 98年釋出1.2版本,代表技術:jit,collections
- 99年,HotSpot虛擬機器發布,後成為jdk1.3及之後預設虛擬機器
- 00年釋出1.3版本,代表技術:數學運算等類庫,jndi服務
- 02年釋出1.4版本,走向成熟的版本。代表技術:正則,異常鏈,NIO,日誌類,xml解析等
- 04年釋出1.5版本,代表技術:語法更易用,自動裝箱,泛型,註解,美劇,foreach,可變長引數
- 06年釋出1.6版本,使用java6命名。代表技術:鎖,同步,垃圾收集,累加值等演算法優化。宣佈開源
- 09年釋出java7,代表技術:G1收集器,升級類載入架構
- 14年釋出java8,長期支援的版本。代表技術:lambda表示式,stream,介面預設方法和靜態方法,optional,base64,HashMap改進(紅黑樹)
- 17年釋出java9, 短期維護版本。代表技術:模組系統,jshell,介面的私有方法,HTTP2支援等。CMS垃圾回收器被廢棄,預設垃圾回收器為G1(基於單執行緒標記掃描壓縮演算法)
- 18年3月釋出java10,短期維護版本。代表技術:var,G1改進(多執行緒並行GC)
- 18年9月26日釋出java11,長期支援的版本。代表技術:新一代垃圾回收器ZGC(實驗階段),JFR(監控、診斷),httpclient等
二. java記憶體區域劃分
1. 程式計數器
- 當前執行緒所執行的位元組碼行號指示器
- 每個執行緒都有獨立的程式計數器
- 如果執行的是java方法,記錄位元組碼指令地址。如果執行Native方法,則為空(Undefined)
- 不會有OutOfMemoryError出現
2. 虛擬機器棧
- 執行緒私有, 生命週期與執行緒相同
- 描述java方法執行的記憶體模型:每個方法都會建立一個棧幀用於儲存區域性變數,方法出口,運算元棧等資訊。每個方法呼叫對應一個棧幀在虛擬機器棧中入棧到出棧的過程
- 存放基本資料型別(8種)和物件引用型別(地址的指標或者物件的控制程式碼)
- 請求棧深度大於虛擬機器允許深度時,丟擲StackOverflowError異常
- 如果動態擴充套件仍無法申請足夠的記憶體,丟擲OutOfMemoryError異常
3. 本地方法棧
- 作用和虛擬機器棧一樣
- 區別為:本地方法棧服務虛擬機器使用到的Native方法
4. 堆
- 虛擬機器管理的記憶體最大的一塊
- 被所有執行緒共享的區域
- 所有物件的例項在此分片記憶體
- 可細分為多個代
5. 方法區
- 所有執行緒共享的區域
- 儲存類資訊,常量,靜態變數
- 在HotSpot虛擬機器上也稱永久代
- 垃圾收集行為很少出現在這個區域,因為可回收的記憶體很少
6. 執行時常量池
- 是方法區的一部分
- 用於存放編譯器生成的各種字面量和符號引用
7. 直接記憶體
- 不包括在JVM記憶體區域中,不受JVM引數影響
- JVM使用緩衝區時,會在該區區域分配記憶體
- 配置時注意給該區域預留空間,而不是把所有記憶體都分給JVM
- -XX:MaxDirectMemorySize指定,不指定預設與堆最大值一樣(-Xmx)
三. HotSpot虛擬機器物件
1. 物件的建立
-
收到new指令時,先檢查是否能在常量池定位到類的符號引用
-
有則表示類已經被載入,解析和初始化過。否則載入類。
-
根據類大學分配堆記憶體。分配的方式有
- 指標碰撞:記憶體規整(無壓縮整理功能),僅移動指標。Serial,ParNew虛擬機器
- 空閒列表:記憶體不規整(有壓縮整理功能),去空閒列表裡找到一塊足夠的空間。CMS虛擬機器
分配過程的併發問題如何解決
- 同步操作:CAS+重試
- 記憶體按照執行緒預分配,稱為本地執行緒分配緩衝(TLAB)。-XX:+/-UseTLAB引數決定
-
物件初始化為零
-
設定物件頭資訊:物件屬於哪個類,物件hash碼,GC分代年齡,是否啟用偏向鎖等等
-
執行init方法做程式設計師需要的初始化
2. 物件的記憶體佈局
物件在記憶體中的佈局分為三個區域:物件頭,例項資料,對齊填充
2.1 物件頭
- 物件頭包括:物件自身的執行時資料,所屬類類指標,陣列長度(如果是陣列物件)
- 執行時資料區官方稱為Mark Word
- 執行時資料區是非固定的資料結構,根據標誌位不同,儲存內容不一樣
- 型別指標表明該物件屬於哪個類例項
- 如果是陣列物件還包括陣列的長度
2.2 例項資料
- 物件真正儲存的有效資訊
- 儲存順序受分片策略引數和原始碼定義順序影響
- 分配策略預設將長度長的分配在前面,欄位相同的分配到一起
2.3 對齊填充
- 不是必須存在的,僅佔位符的作用
- 物件大小必須為8位元組整數倍,不足的通過對齊補全
3. 物件的訪問定位
-
使用控制程式碼: 堆單獨劃分一塊記憶體作為控制程式碼池,reference儲存控制程式碼地址。物件移動時reference不需要修改。
-
直接指標:reference直接儲存物件地址。速度快。
四. 垃圾收集器與記憶體分配策略
1. 基本概念
1.1 收集的物件
堆,方法區中的記憶體區域
1.2 判定物件是否存活的方法
引用計數法
- 給物件新增引用計數器
- 實現簡單
- 無法解決物件直接相互迴圈引用的問題
- 使用的代表:微軟COM技術
可達性分析
- 使用的代表:java,c#
- 通過GC roots物件作為起始點,到該物件不可達時,證明物件不可用
- GC roots物件包括以下幾種
- 虛擬機器棧中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法中JNI引用的物件
1.3 引用的分類
- 強引用:普遍存在new之後賦值操作,存在則永遠不會被回收
- 軟引用:還有用,但並非必須但物件。記憶體溢位異常之前回收這些物件
- 弱引用:強度比軟引用更弱,只能存活到下一次垃圾回收之前
- 虛引用:最弱到引用關係。無法通過虛引用得到物件。存在的目的是當垃圾回收時收到一個系統通知
1.4 方法區(永久代)的回收
- 該區域的垃圾收集效率遠遠低於新生代(70%-95%)
- 回收兩類內容:廢棄常量,無用的類
- 判定是否是無用類的條件
- 該類所有例項都被回收
- 載入該類的classload被回收
- 該類的java.lang.Class物件沒有在任何地方被引用,無法通過反射訪問
- 滿足以上條件的無用類可以被回收(不是必須)
2. 垃圾收集演算法
2.1 標記-清除演算法
- 最基礎的收集演算法
- 分為標記和清除兩個階段
- 不足之處:
- 效率問題
- 產生大量不連續的記憶體碎片
2.2 複製演算法
- 將記憶體分為大小相等的兩塊,每次使用其中的一塊
- 一塊用完時,將存活的物件複製到另一塊
- 現代虛擬機器新生代都用該演算法
- 不足:
- 記憶體利用率不高
HotSpot虛擬機器將新生代記憶體分為較大的Eden區和兩塊較小的survivor空間。大小比例為8:1。
2.3 標記-整理演算法
- 物件存活率高時大量的複製會影響效率,老年代使用該演算法
- 標記過程與標記-清除演算法一樣
- 後續步驟並不是清理物件,而是讓所有存活的物件都向一段移動,清理邊界以外的記憶體
2.4 分代收集演算法
- 根據物件存活週期不同,採用不同的收集演算法
- 新生代大量物件死亡,少量存活,採用複製演算法
- 老年代物件存活率高,採用標記-清理或者標記-收集演算法
3. HotSpot的演算法實現
3.1 列舉GC Roots
- 可達性分析列舉GC Roots時 ,必須stop the world
- 目前JVM使用準確式GC,停頓時並不需要一個個檢查,而是從預先存放的地方直接取。(HotSpot儲存在OopMap資料結構中)
3.2 安全點
- 基於效率考慮,生成OopMap只會才特定的地方,稱為安全點
- 安全點的選定方法
- 搶先式中斷:現代JVM不採用
- 主動式中斷:執行緒輪詢安全點標識,然後掛起
3.3 安全區域
- 對於沒有分配cpu的執行緒(sleep),安全點無法處理,由安全區域解決
- 安全區域指一段程式碼中引用關係不會發生變化
- 執行緒進入安全區域時,JVM發起GC就不用管這些執行緒,離開時需要檢查GC是否完成,未完成就需要等待
3.4 垃圾收集器
serial收集器
- 最基本,發展歷史最悠久的收集器
- jdk1.3.1之前新生代收集器唯一的選擇
- 單一執行緒收集器
- GC時必須暫停其他所有的工作執行緒
- 簡單高效,對於單CPU的client模式來說是很好的選擇
ParNew收集器
- serial收集器的多執行緒版本
- server模式的JVM首選的新生代收集器
- 單CPU模式下,因執行緒切換開銷,效能絕不比serial好
Parallel Scavenge
- 採用複製演算法的新生代收集器,支援多執行緒
- 可以控制吞吐率
- -XX:MaxGCPauseMillis 最大垃圾收集停頓時間,與吞吐量成反比
- -XX:GCTimeRatio 吞吐量大小
- 提供自適應調節測試
- -XX:UseAdaptiveSizePolicy
- 無法與CMS收集器配合工作
Serial Old
- serial收集器的老年代版本
- 使用標記-整理演算法
- 給client模式下虛擬機器使用
Parallel Old
- Parallel Scavenge老年代版本
- 多執行緒,標記-整理演算法
- JDK1.6開始提供使用
Concurrent Marked Sweep(CMS)
- 老年代收集器
- 目標是儘可能減少GC停頓時間
- 不會等到老年代空間快滿了才回收(和使用者執行緒併發,留記憶體給使用者執行緒)。配置引數為-XX:CMSInitiazingOccupanyFraction
- 使用標記-清除演算法。整個過程分為四步:
- 初始標記:STW,標記GC Roots能關聯到的物件,速度很快
- 併發標記:GC Roots Tracing過程。耗時。和使用者執行緒一起執行
- 重新標記:STW,標記併發標記過程中程式執行導致標記變化的物件,時間比初始標記長,遠比並發標記短
- 併發清除:耗時。和使用者執行緒一起執行
- 優點:
- 併發收集
- 低停頓
- 缺點:
- 佔用正在執行的使用者程式的cpu資源
- 無法處理浮動垃圾(併發清理過程中產生的新垃圾無法當次處理掉)
- 記憶體碎片問題
Garbage First( G1)
- 最前沿的垃圾收集器
- jdk1.7版本釋出,替換jdk1.5的CMS
- 堆記憶體佈局與其他收集器不一樣,新生代老年代不再是物理隔離的,而是Region集合
- 根據各個region垃圾回收的價值,加入優先順序佇列。保證每次GC能在有限時間內得到最高的收集率
- 通過Remember set保證跨region的區域不需要全堆掃描
- 執行步驟
- 初始標記:STW,時很短。標記GC Roots關聯的物件
- 併發標記:可達性分析,耗時長。可與使用者執行緒併發執行
- 最終標記:STW,修正併發標記階段使用者執行緒執行導致標記變化的部分
- 篩選回收:排序各個region的回收價值,制定回收計劃
- 優點
- 並行與併發
- 分代收集
- 空間整理:不會產生記憶體碎片
- 可預測的停頓:幾乎是實時的垃圾收集
4. 記憶體分配與回收策略
4.1 物件優先在Eden區分配
-
eden區空間不足時,JVM發起一次Minor GC
Minor GC vs Full GC
- minor gc:新生代gc,頻繁發生,速度快
- major gc/full gc:老年代gc,速度慢
4.2 大物件直接進入老年代
- 典型代表是:很長的字串或陣列
- 大物件對JVM不友好,導致頻繁發生GC
4.3 長期存活的物件將進入老年代
- 物件經過Eden的第一次minor gc仍然存活,被移動到survivor,年齡+1
- 物件在survivor每經過一次minor gc,年齡+1
- 年齡加到一定程度(預設15),將進入老年代。引數:-XX:MaxTenuringThreshold
4.4 動態年齡判斷
- 並不是永遠要求年齡達到設定的值才進入老年代
- 當survivor空間中相同年齡所有物件大小大於空間的一半,大於等於該年齡的物件就直接進入老年代
4.5 空間分配擔保
- minor gc執行之前會檢查老年代最大可用的連續空間是否大於新生代所有物件總空間
- 不成立則判斷是否大於歷次晉升到老年代物件的平均大小
- 各種條件不滿足則進行full gc
5. jvm效能監控
5.1 jdk的命令列工具
- jps:檢視執行的虛擬機器程式
- jstat:統計資訊監控工具。引數有:
- -class:類裝載資訊
- -gc:監視堆,包括eden、survivor、老年代、持久代空間,gc時間等
- -gccapacity: 同-gc,不過主要關注各區域最大,最小空間
- -gcutil:同-gc,不過主要關注佔用百分比
- -gccause:同-gcutil,不過會輸出導致上一次GC的原因
- -gcnew:監視新生代
- -gcnewcapacity:同-gcnew,關注最大,最小空間
- -gcold:監視老年代
- -gcoldcapacity:同-gcold,關注最大最小空間
- -gcpermcapacity:永久代最大,最小空間
- -compiler:JIT編譯資訊
- printcompilation:JIT編譯的方法
- jinfo:java配置資訊工具。實時檢視和調整虛擬機器各項引數
- jmap:記憶體映像工具。引數有:
- -dump:生成java堆儲存快照
- -finalizerinfo:等待執行finalize方法的物件
- -heap:顯示java堆詳細資訊:回收期,引數配置,分代狀況
- -histo:堆物件統計資訊:類,例項數量,總容量
- -permstat:永久代記憶體狀態
- -F:強制生成快照
- jhat:分析dump檔案。一般不用。用第三方的VisualVM,eclipse,Memory Analyzer, heap analyzer等
- jstack:java堆疊追蹤工具,用於定位長時間停頓的執行緒當前棧情況
5.2 jdk的視覺化工具
- jconsole
- VisualVM
五. 類檔案結構
1. class類的檔案結構概述
- class檔案是一組以8位位元組為基礎單位的二進位制流
- 各個資料項嚴格緊湊排步
- 8位位元組以上的資料,按高位在前的順序分割為多個8位位元組儲存
- 採用類似與c語言結構體的偽結構儲存
- 偽結構有兩種資料型別:
- 無符號:基本資料型別,包括u1,u2,u4,u8,數字代表位元組數
- 表(複合結構):以_info結尾
- 下表的順序,數量,儲存的位元組都是嚴格限定的
2. magic
- 頭四個位元組
- 確定該class檔案是否能被jvm接受,用於身份識別
- 值為:0xCAFFEBABE
3. 版本號
- minor-version:5-6位元組
- major-version:7-8位元組
4. 常量池
- class檔案中的資源倉庫,數量不固定,在入口的地方由一個u2型別的資料指定常量池的容量
- 常量池第0項是空出來的,目的在於後面某些常量池索引值不引用任何一個常量池,就把索引值置為0
- 主要存放兩大類常
- 字面量: 文字字串,final常量等
- 符號引用:類和介面的全名,欄位的名稱和描述符,方法的名稱和描述符
- 常量池中每一項常量都是一個表,每種表開始的第一位是u1型別的標識位,代表當前的常量屬於哪種常量型別
- 常量池的專案型別如下:
5. 訪問標識
- 常量池之後的兩個位元組
- 標識該類為普通類、介面、列舉、還是註解。public還是private等
6. 類索引、父類索引與介面索引集合
- 類索引、父類索引是u2型別資料,介面索引是-組u2型別資料集合
- 用於確定這個類的繼承關係
7. 欄位表集合
- 描述介面或類中宣告的變數,不包括區域性變數
- 包含的資訊有:作用域,static修飾符,final修飾符,資料型別,volatile,transient,名稱
8. 方法表集合
- 類似於欄位表集合
- 包括訪問標識、名稱索引、描述符索引、屬性表集合
9. 屬性表集合
- 用於描述某些場景的專有資訊
- 沒有順序,長度和內容的限制,只要不和已有的屬性名重複
六. 位元組碼指令
1. 概述
- jvm的指令=操作碼(1位元組)+運算元
- 大多數指令只有操作碼,沒有運算元
- 操作碼的數量不會超過256(2^8)
- 由於運算元沒有對齊,所以超過一個位元組的資料,執行時需要重建資料。優點是省略很多填充和間隔,缺點是效能有損失
2. 載入和儲存指令
- 用於將資料在棧幀中的區域性變數表和運算元棧間來回傳輸
- 將區域性變數載入到操作棧:iload, iload_,lload,fload,dload,aload(reference load)
- 從運算元棧儲存到區域性變數表:istore, istore_,lstore,fstore,dstore,astore
- 常量載入到運算元棧:bipush,sipush
3. 運算指令
- 對運算元棧上對兩個值做運算,再存回去
- byte,short,char,boolean型別都使用int型別指令替代
- 加法指令:iadd,ladd,fadd,dadd
- 減法指令:isub,lsub,...
- 乘法指令:imul,...
- 除法指令:idev,...
- ...
4. 型別轉化指令
- 寬化型別轉化:無需顯示都轉化指令
- int轉long,float,double
- long轉float,double
- float轉double
- 窄化型別轉化:必須顯示使用轉化指令
- 包括:i2b,i2c,i2s,l2i...
5. 物件建立與轉化指令
- 建立類例項的指令:new
- 建立陣列的指令:newarray,anewarray,multianewarray
6. 運算元棧管理指令
- 運算元棧頂一個或兩個資料出棧:pop,pop2
- 複製棧頂資料再壓棧:dup,dup2
- 交換棧頂倆元素:swap
7. 控制轉移指令
- 條件分支:ifeq,iflt,...
- 複合條件分支:tableswitch,lookupswitch
- 無條件分支:goto,ret
8. 方法呼叫與返回指令
- 呼叫物件的例項方法:invokevirtual
- 呼叫介面方法:invokeinterface
- 呼叫特殊方法:初始化,父類方法等:invokespecial
- 呼叫靜態方法:invokestatic
9. 異常處理指令
- athrow
10. 同步指令
- synchronized對應的指令:monitorenter,monitorexit
七. 類載入機制和類載入器
1. 類載入機制
1.1 概述
類從被載入到記憶體中開始,到解除安裝出記憶體為止,整個生命週期包括
- 載入
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 解除安裝
類載入時機沒有強制規定,但是初始化階段,有且只有以下情況下必須對類進行初始化:
- 遇到new,getstatic,putstatic,invokestatic這四條指令碼時。對應java程式碼為:new,設定靜態欄位,呼叫靜態方法
- java.lang.reflect包對類進行反射時
- 初始化一個類時,如果父類還沒有被初始化時初始化父類(介面沒有這個要求)
- 虛擬機器啟動時,主類(main)的初始化
- java.lang.invoke.MethodHandle特定的解析結果時
1.2 載入
- 通過類名獲取二進位制位元組流:來源可以是jar,war等。也可以是網路,代理等。
- 將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構
- 在記憶體中生成java.lang.Class物件,作為方法區該類的訪問入口
1.3 驗證
- 為了確保class檔案的位元組流中包含的資訊符合虛擬機器要求
- 保護虛擬機器自身的一項重要工作,防止被惡意攻擊
- 驗證內容具體包括:
- 檔案格式驗證:魔數是否是0xCAFEBABE, 主次版本號是否被當前jvm允許,常量池型別是否正確等等
- 後設資料驗證:是否有父類,是否繼承了final類,非抽象類是否實現了所有方法等等
- 位元組碼驗證:最複雜。驗證資料放入和取出棧是同一型別,指令不會跳轉到方法體以外等
- 符號引用驗證:符號引用中通過名稱能否找到對應的類等
1.4 準備
- 為類變數(static型別)分配記憶體並設定類變數初始值的階段
- 初始值一般指0,而不是程式碼初始化的值。除非指定為final的static變數
- 以下是預設的初始值
1.5 解析
- 將常量池內的符號引用替換為直接引用
- 符號引用:以一組符號來描述引用的目標,與JVM記憶體佈局無關
- 直接引用:與JVM記憶體佈局有關,直接指向目標的指標或者偏移地址
- 解析包括:
- 類或介面的解析
- 欄位解析
- 類方法解析
- 介面方法解析
1.6 初始化
- 真正執行類中定義的java程式碼
- 執行類構造器方法的過程
- client方法由所有static變數和static程式碼合併得到
- 該方法執行是多執行緒安全的
2. 類載入器
2.1 類載入器
- 存在與jvm外部,實現類的載入操作
- 任意一個類,都由類載入器和類本身共同確定唯一性
- 比較類是否相等,只有在同一個類載入器載入的前提下才有意義
2.2 類載入器的分類
- 啟動類載入器:jvm識別,載入lib目錄下特定的jar
- 擴充套件類載入器:載入lib/ext目錄的jar
- 應用程式載入器:載入使用者類,ClassLoader實現,使用者可直接使用
- 自定義載入器:自定義的載入器 關係如下:
2.2 雙親委派模型
- 各個類載入器之間如圖的層次關係稱為雙親委派模型
- 要求除了頂層的啟動類載入器外,其餘載入器都必須有父載入器(通過組合而不是繼承關係來實現)
- 工作過程:類載入器收到載入請求,它首先不會自己嘗試去載入類,而是把請求委託給父類去載入。所以所有的請求都會先被啟動類載入器載入。當父類載入不了時,才由子類載入
- 作用:保證了基礎類在各種類載入器中都是同一個類,否則java型別體系將一片混亂
- 應用:tomcat不同服務要隔離,公共部分要重用。各式各樣的類需要載入,都是通過雙親委派模型實現的
八. jvm位元組碼執行機制
1. 執行時棧結構
1.1 概述
- 支援方法呼叫和執行的資料結構
- 處於jvm記憶體模型中的java虛擬機器棧區域
- 儲存了區域性變數表,運算元棧,動態連線,方法返回地址等資訊
- 每個方法的呼叫都對應虛擬機器棧的入棧到出棧過程
1.2 區域性變數表
- 存放方法引數和區域性變數
- 區域性變數不像類成員變數會被預設初始化
- 區域性變數表第0號索引預設存放this
1.3 運算元棧
- 方法執行過程中,各種位元組碼指令往運算元棧入棧和出棧
1.4 動態連線
- 每個棧幀中都有一個該棧所屬方法的引用,用於動態連線
1.5 方法返回地址
2. 方法呼叫
2.1 解析
2.2 分派
- 靜態分派
- 動態分派
- 單分派和多分派
- 虛擬機器動態分派實現
3. 方法的執行
- 解釋執行
- 編譯執行
九. java記憶體模型
1. 效率與一致性
- 快取記憶體解決了處理器與記憶體的速度矛盾
- 但是引入了快取一致性的問題
- 處理器的優化和編譯器的指令重拍也會導致快取不一致
2. java記憶體模型
2.1 概述
- 特性:圍繞著在併發過程中如何處理原子性,可見性,有序性這三個特徵建立的
- 作用:JVM定義JMM來遮蔽各種硬體和作業系統的記憶體訪問差異,達到在各種平臺有一致性的記憶體訪問效果
- 目標:定義變數的訪問規則,包括變數儲存到記憶體和從記憶體取出變數值。這裡的變數指會被共享的例項欄位,類欄位。不包括不被共享的區域性變數
- 規定:所有變數都儲存在主存中,每個執行緒都有自己的工作記憶體,工作記憶體儲存了主記憶體變數的副本。執行緒對變數的操作必須在工作記憶體中,不能在主存中。不同執行緒之間也不能互相訪問,執行緒間變數傳遞需要經過主存。
2.2 主存與工作記憶體互動協議
協議包括的原子操作:
一個變數如何從主存拷貝到工作記憶體,如何從工作記憶體同步回主存,java記憶體模型定義了8中操作來完成,每種操作都是原子性的:
- lock:作用與主記憶體變數,變數被一個執行緒獨佔
- unlock:作用與主記憶體變數,解鎖,可被其他執行緒鎖定
- read:作用與主記憶體變數,把一個變數值從主記憶體傳輸到工作記憶體,以便load
- load:作用與工作記憶體變數,把read操作從主存得到的值放入工作記憶體變數副本
- use:作用與工作記憶體變數,把工作記憶體變數值傳遞給執行引擎
- assign:作用與工作記憶體變數,把執行引擎的值賦給工作記憶體變數
- store:作用與工作記憶體變數,工作記憶體變數傳給主記憶體,以便write操作
- write:作用與主記憶體變數,store得到的值放入主存變數
協議規則
- 主記憶體複製到工作記憶體:必須順序執行read和load
- 工作記憶體同步回主存:必須順序執行store和write
- 沒有要求連續執行,即中間可以插入其他操作
- 不允許read和load、store和write操作之一單獨出現
- 使用use,store之前必須執行assign和load操作。即變數只能在主記憶體中誕生
- 一個變數同一個時刻只能有一個執行緒lock,但同一個執行緒可以多次lock,然後執行相同次數unlock才能被釋放
- 執行lock時,會清空工作記憶體中變數的值,使用時需要重新load和assign
- 沒有被lock,就不能執行unlock
- 執行unlock時,必須先把變數值同步回主存
2.3 java記憶體模型定義Volatile的特殊規則
變數定義為volatile之後,將具備兩種特性:
-
可見性:保證此變數對所有執行緒的可見性。即一個執行緒修改了變數,另一個執行緒立馬可以得知。而普通變數需要經過主存傳遞才能完成。
可見性並不能保證併發安全,因為操作可能不是原子的。
-
禁止指令重排序優化。保證變數賦值順序與程式碼執行順序一致。
2.4 java記憶體模型對long,double型別的特殊規則
- 對於64位資料型別,JVM執行將沒有被volatile修飾的變數讀寫劃分為兩個32位操作來進行。即long和double的非原子性協定
- 不過JVM把64位資料的操作實現為原子性的。所以不需要專門宣告為volatile
2.5 原子性,可見性,有序性
- 原子性:java記憶體模型的規則保證了基本資料型別的訪問是原子性的。更大範圍的原子性可通過synchronized和lock來保證
- 可見性:java記憶體模型通過在變數修改後將新值同步回主記憶體,讀取變數前從主記憶體重新整理新值這種通過主記憶體傳遞的方式實現可見性。volatile保證了多執行緒操作時變數的可見性,普通變數則不能保證。synchronized和final也能實現可見性
- 有序性:本執行緒觀察,所有操作都是有序的。在一個執行緒中觀察另一個執行緒,所有操作都是無序的。volatile和synchronized保證執行緒操作的有序性。
十. 執行緒安全與鎖
1. 執行緒狀態
任意一個時間點,一個執行緒有且只能有一種狀態
- 新建(New):建立後尚未啟動的狀態
- 執行(Runable):正在執行或等待cpu為它分片執行時間
- 無限期等待(Waiting):不會被分配cpu時間,要等待被其他執行緒顯示喚醒。以下方法會出現該狀態
- 沒有timeout的Ojbect.wait方法
- 沒有timeout的Thread.join方法
- LockSupport.park方法
- 限期等待(Timed Waiting):不會被分配cpu時間,不過不需要被喚醒,一定時間後系統自動喚醒。
- Thread.sleep()
- 有timeout的Ojbect.wait方法
- 有timeout的Thread.join方法
- LockSupport.parkNanos方法
- LockSupport.parkUntil方法
- 阻塞(Blocked):執行緒進入同步區域,等待獲取排他鎖的狀態
- 結束(Treminated):結束執行
2. 執行緒安全的實現方法
2.1 互斥同步(阻塞同步,悲觀鎖)
- 同步:多執行緒併發訪問資料時,保證統一時刻只被一個執行緒使用
- 互斥:實現同步的手段,包括:臨界區(Critical Section)、互斥量(Mutex)、訊號量(Semaphore)
- synchronized:最基本的互斥同步手段。編譯後會在同步程式碼前後新增monitorenter和monitorexit指令。執行monitorenter時,要嘗試獲取物件的鎖,已經擁有就吧鎖計數加1(可重入),否則阻塞知道鎖被釋放。阻塞會呼叫核心,消耗大量切換時間。JVM優化機制會在前面加一段自旋等待。
- ReentrantLock:api層面的互斥鎖,多了一些高階特性。如等待可中斷,可實現公平鎖等等。預設非公平。
- 互斥同步的缺點:執行緒阻塞和喚醒的效能問題
2.2 非阻塞同步(樂觀鎖)
- 需要硬體來保證操作和衝突檢測的原子性
- CAS:compare-and-swap,典型的非阻塞同步。包括3個運算元
- 記憶體位置:V
- 舊的預期值:A
- 新值:B
- CAS只有當V符合A時,採用B更新V,否則不更新。無論是否更新,都返回V的舊值,該過程是原子操作
- CAS的缺點:ABA問題:舊值被改過再改回來無法感知到
ThreadLocal
- 將一些不需要被多執行緒訪問的變數由單個執行緒去獨享
3. 鎖優化
JDK1.6版本針對高併發實現了各種鎖優化技術
3.1 自旋鎖和自適應自旋
- 自旋鎖:執行緒等待的時候,並不是掛起,而是執行一個忙迴圈。
- 自適應自旋鎖:根據上一次自旋時間及擁有者狀態自動調整自旋時間
3.2 鎖消除
- 編譯器執行時,自動檢測出那些不可能存在共享資料競爭的鎖,然後進行消除
3.3 鎖粗化
- 將一串零碎的加鎖同步操作,擴大範圍到整個序列以外
3.4 輕量級鎖
- 相對於使用作業系統互斥量來實現的傳統鎖而言的。
- 使用CAS操作避免了使用互斥量的開銷
3.5 偏向鎖
- 消除資料在無競爭情況下的同步,進一步提高效能
- 無競爭的情況下把整個同步都消除掉,CAS都不做