深刻理解java虛擬機

前言

  1. JVM內存模型,JAVA內存模型,JAVA對象模型,這些名字類似的模型分別是什麼?
  2. 經常使用的垃圾收集方法有哪些?垃圾收集器有哪些?各自有什麼特色?
  3. JVM如何監控?調優?
  4. java編譯後生成的class文件,內部存儲格式是什麼樣的?
  5. 類加載有哪幾個過程?什麼是雙親委派模型?
  6. volatile和synchronized有什麼區別?
  7. 樂觀鎖和悲觀鎖是什麼?CAS原理?

以上問題在《深刻理解java虛擬機》這本書裏都有詳盡的解答。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指令時,先檢查是否能在常量池定位到類的符號引用程序員

  • 有則表示類已經被加載,解析和初始化過。不然加載類。算法

  • 根據類大學分配堆內存。分配的方式有shell

    • 指針碰撞:內存規整(無壓縮整理功能),僅移動指針。Serial,ParNew虛擬機
    • 空閒列表:內存不規整(有壓縮整理功能),去空閒列表裏找到一塊足夠的空間。CMS虛擬機

    分配過程的併發問題如何解決c#

    • 同步操做:CAS+重試
    • 內存按照線程預分配,稱爲本地線程分配緩衝(TLAB)。-XX:+/-UseTLAB參數決定
  • 對象初始化爲零api

  • 設置對象頭信息:對象屬於哪一個類,對象hash碼,GC分代年齡,是否啓用偏向鎖等等數組

  • 執行init方法作程序員須要的初始化緩存

2. 對象的內存佈局

對象在內存中的佈局分爲三個區域:對象頭,實例數據,對齊填充tomcat

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都不作
相關文章
相關標籤/搜索