深入理解java虚拟机脑图文档
二、內存區域和內存溢出
運行時數據區域
程序計算器
線程私有,當前線程鎖執行的字節碼的行號指示器,不會出現OOM。
java虛擬機棧
- 概念:
線程私有,java方法執行的線程內存模型,每個方法唄執行時jvm會哦同步創建一個棧幀,用戶儲存:局部變量表,操作數棧,動態連接,方法出口等。
- 出現異常:
若線程請求的棧深度超過jvm所允許的深度,拋出 StackOverflowError
棧擴展時無法申請到足夠的內存,拋出OOutOfMemoryError
本隊方法棧
線程私有,服務于本地方法。
java堆
- 概念:線程共享,jvm啟動時創建,存放對象的實例,隨著即時編譯器的進步,java對象也可能在棧上分配
- 組成:新生代,老年代,永久代,Eden,Survivor… 把java堆細分只要是為了更好的回收內存,或者更快的分配內存
方法區
- 概念:線程共享,儲存被JVM加載的類型信息,常量,靜態變量,即時編輯器編譯后的代碼緩存等數據。
- 變化:JDK7的HotSpot,把原來放在永久代的字符串常量池,靜態變量移動到了堆中,JDK8廢棄了永久代的概念,使用了本地內存中的元數據區進行了替換
HostSpot虛擬機對象
對象的創建
-
在常量池中價差類的符號引用
-
改符號引用代表的類進行加載,解析和初始化
-
對中分配內存
- 指針碰撞:內存分配是規整的,每次分配內存時,使用指針位移的方式進行。
- 空閑列表:jvm維護一個列表,記錄堆內存那些內存塊是可用的,每次分配完內存之后更新該列表。
- 并發問題:jvm采用CAS保證更新操作的原子性,另一方式:把內存分配動作按照線程劃分在不同的空間中進行。
-
對象頭設置
- 對象是哪個類的實例,哈希碼,GC分代年齡…;至此jvm視角,一個對象已創建完成
- 從java程序的視角,構造函數尚未執行(Class文件中的) 字段默認是0值,此時程序對象還不能使用。
-
執行構造函數() 方法
- 此時刻對象才算完全構造出來
對象的內存分布
-
對象頭
- 運行時數據
官方稱之為"Mark Word", 動態定義的數據結構,包括:哈希碼,GC分代年齡,所狀態標志,線程持有的鎖,偏向線程id,偏向時間戳等等。
- 執行類型元數據的指針
通過這個指針確定該對象是哪個類的實例。
- 如是數組,則還有個數組長度
-
實例數據
程序代碼中所定義的各種類型的字段內容,無論是父類繼承下來的字段還是本類自己的都會記錄下來,按照默認的分配策略,相同寬度的字段總是分配到一起。
- 對齊填充
jvm要求對象的起始地址必須是8字節的整數倍,所以如果對象實例數據沒有對齊的話,就需要通過對齊填充。
對象的訪問定位
-
通過棧上的reference數據來曹組偶對上的具體對象
-
訪問方式
-
使用句柄訪問
java堆中劃分一塊內存作為句柄池,reference保存的就是對象的句柄地址,而句柄保存的是實例地址和對應的類型數據地址
**優勢:**reference中存放的是穩定的句柄,對象被移動的時候,修改的是句柄中實例對象的指針,reference不必修改。
-
使用直接指針訪問
棧上直接存放的就是堆中對象的內存地址,這種方案就需要考慮類型數據如何存放。
**優勢:**訪問速度快,節省了一次指針定位的開銷。
OutOfMemoryError異常
Java堆溢出
-Xmx 最大對對內存; -Xms 初始堆內存
虛擬機棧和本地方法棧溢出
-Xss 設置棧內存容量
字符串常量池
String::intern()
- jdk7之前,把首次出現的字符串實例對象復制到永久代中的字符串常量池中,并返回永久代的實例的地址。
- jdk7以及后面的版本,字符串常量池移動達到了堆中,那么在需要在常量池中記錄字符串首次出現的實例引用即可,并返回常量池中的地址。
方法區溢出
- -XX:MaxMetaspacceSize 最大元空間,以字節為單位,
- -XX:MetaspaceSize 初始元空間
三、垃圾收集區與內存分配策略
GC要解決的問題
哪些對象可以回收
- 定義: 在對象中添加一個引用計數器,當該對象被引用一次,計數器就加1,引用失效一次,計數器減1,計數器為0 那么該對象就沒有被使用
- 有點:原理簡單容易實現,判斷效率也很搞。
- 缺點: 無法解決對象之間互相引用的問題。
- 定義:通過一系列稱之為”GC Roots“ 的對象作為起始節點根據引用關系進行遍歷,沒有遍歷達到的對象則列為可被回收范圍。
- GC Roots:
- jvm棧中引用的對象,例如:方法中使用的參數,局部變量,臨時變量等。
- 方法去總靜態屬性引用的對象。
- 方法去中常量引用的對象。
- 本地方法棧中JNI引用的對象。
- JVM內部的引用。流入:基本數據類型對應的class對象 系統類加載器。
- 被同步鎖持有的對象。
- 反應jvm內部情況的jmxbean jvmti中注冊回調,本地代碼緩存等。
- 強引用: 程序中普遍存在的引用賦值,只要存在這種關系,被引用的對象就不會被回收
- 軟引用:一些有用但非必須的對象,將要發生內存溢出異常前,會把這些對象列入回收范圍進行二次回收。 嘗嘗被用來實現緩存技術。例如 圖片緩存,網頁緩存。
- 弱引用:非必須對象,只能怪生存到下一次gc發生過為止。
- 虛引用:無法通過一個個虛引用獲取一個個對象的實例,唯一的目的一只是為了這個對象被gc時可以收到一個系統通知。
- finaliize() 何時可以被執行, 對象的fiinalize方法沒有被復寫; 對象的finalize方法已經被jvm調用過,這兩種情況都視為 沒有必要執行。
- 對象經過可達性分析,進行第一次標記后,若需要執行finalize方法 則改對象被放置在F-Queue隊列中
- 稍后會被jvm自建的,低優先級的finalizer線程執行對象的finalize方法,在改方法中進行自救(例如吧自己的引用賦值給其他變量)
- finalize方法是對象逃脫被gc的最后一次機會,隨后收集器會對F-queue中的對象進行第二次小規模的標記。
- 任何對象的finalize方法都只能被jvm自動調用一次。
- 廢棄的常量,常量池中的對象沒被引用,那么gc該區域的時候,判定要被回收的話,則會被清理掉。
- 不在使用的類型:
- 該類型所有的實例都被回收
- 加載該類的類加載器已被回收
- 改類對應的class對象沒有被其他地方引用
垃圾收集算法
分代收集器
-
在大多數程序運行的實際情況的經驗上提出粗的該理論,建立在兩個分代假說
弱分代假說:絕大多數對象都是朝生夕滅
搶分代假說:熬過多次gc過程的對象就越難消亡。
跨代引用假說:跨代引用相對于同代引用來說僅占極少數。
-
gc收集器設計原則,把java堆分不同的區域,然后把回收的對象按照年齡分配到不同的區域中存儲
標記-清除算法
- 首先標記出所有需要回收的對象,同一回收掉所有被標記的對象。
- 優點:實現簡單。
- 缺點: 執行效率不穩定,內存空間碎片化問題。
標記-復制算法
- 內存分為兩個大小相等的區域,每次只是使用其中一塊,某一塊使用完了之后,就把還存活的對象負債到另一塊區域上,清理掉已經使用的那塊。
- 優點:實現簡單。運行高效;
- 缺點:浪費內存空間。
標記-整理算法
- 把所有的存活對象都移動到內存空間的一端,然后直接吃力掉邊界意外的對象
- 優點:解決了標記-請出去的內存碎片化問題 和 標記 - 復制的 浪費內存的問題
- 缺點: 算法相對復雜, 內存地址的移動, 增加的gc的停頓時間
HotSpot算法細節
根節點枚舉
- 根節點枚舉的過程中,需要保證對象的引用關系,不會發生變化,存在 ”Stop The World“
- OopMap
- 為了解決避免遍歷所有的執行上下文的引用位置,使用OopMap的結構存放對象的引用
- 一旦類加載器動作完成時,HotSpot就會吧對象中的某偏移量對應的類型計算出來,使用OopMap保存這些對象的引用
安全點
-
為了解決快速準備完成gc roots的枚舉,引入了安全點, 安全區域
-
用戶線程執行到安全點才會暫停,安全點位置是為編譯器自動插入,一般第具備 讓程序長時間執行的特征, 例如 方法的調用 循環跳轉 異常挑戰等指令順序復用的地方。
-
用戶線程達到安全點并暫停的方案
- 搶先試中斷:系統先把所有的用戶線程都終端,然后篩選出不在安全點上的線程恢復,然后過一會再對他進行中斷,直到到達安全點。
- 主動式中斷:gc收集器需要中斷線程的時候,設置一個標記,用戶線程現在執行的過程中不斷的輪詢這個標記,發現標記為真則執行到最近的安全點上中斷掛起。
? HotSpot使用內存保護陷阱的方式,通過一條匯編指令完成輪詢和觸發線程中斷
安全區域
- 安全區域能去報在某一時間代碼片段之中,引用關系不會發生變化,例如 用戶線程處于sleep狀態
- 當用戶線程要離開安全區域時,需要檢查jvm是否已經完成了根節點美爵等需要暫停用戶線程場景
記憶集和卡表
- 為了解決對象跨代引用帶來的問題,引入了記憶集,用戶記錄非收集區域指向手機區域的指針集合的抽象數據結構
- 卡表 針對記憶集 這種定義的一個具體實現。
- HotSpot中設計了card_table字節數據這個卡表 大小為512字節,數組的每個元素標識為內存區域中的一塊特定大小內存塊
- 只要這個特定的內存區域中的對象的字段存在跨代指針,則把對應數組元素值標記為1, 改元素稱之為變臟,gc時,只要掃碼下卡表中變臟的元素,然后找出對應的內存區域中的對象加入到gc roots 中進行掃描
寫屏障
- 為了解決如何維護卡表元素狀態
- 這里的寫屏障可以看做jvm層的引用類型字段賦值,這個動作的aop切面,引用對象的復制前后都在寫屏障的覆蓋范疇內
并發的可達性分析
- 為了解決并發比那里對象圖,降低用戶線程的停頓,引入三色標記
- 按照 是否被gc收集器訪問過 這個條件標記為三種顏色
白色: 為收集器訪問過
黑色: 已經被收集器訪問過,并且這個對象的所有引用都已經被掃描過
灰色:已經被收集器訪問過,但這個對象至少存在一個引用沒有被掃描過
- 對象消失問題,即原本已經被gc掃描過的對象引用發生了變化 兩個條件
- 賦值器插入至少一條黑色對象到白色對象的引用
- 賦值器刪除了全部從灰色對象到白色對象的直接或者間接引用。
- 解決對象小時問題,破壞這兩個中任何一個條件即可。
增量更新:黑色對象一旦新插入了指向被色對象的引用之后,他就變成灰色對象。
原始快照(SATB):無論引用關系刪除與否,都會按照剛開始掃描時的對象圖快照來進行搜索。
垃圾收集器
Sarial收集器
單線程工作的,收集新生代內存的收集器 使用標記 - 復制算法
實現簡單,搞笑,但是用戶線程停頓時間較長。
ParNew收集器
其實是一個多線程版本的serial收集器 支持多線程并行收集,使用標記-復制算法。
目前僅有他能跟CMS收集器配合
Parallel Scavenge收集器
達到一個可控的吞吐量,支持多線程;吞吐量= 運行用戶代碼時間/(運行用戶代碼時間 + 運行垃圾收集的時間)
通過具體的參數可以進行精確控制吞吐量,使用 標記復制算法
Serial Old 收集器
是serial收集器的老年代版本,使用標記-整理算法
Parallel Old 收集器
是parallel Scavenge 的老年代版本 支持多線程并行手機 使用標記-整理算法。
CMS收集器
Concurrent Mark Sweep 以獲取最短回收停頓時間為目標的收集器
步驟: 初始標記,單線程執行,暫停用戶線程,標記一下,gc roots 能直接關聯的對象 速度很快;
? 并發標記:多線程并發從gc roots的指尖管理的對象遍歷整個對象圖的過程
? 重新標記:修正并發標記過程中已經被標記卻發生變動的對象
? 并發清除: 并發清除掉已經被標記的對象
優點: 并發收集 低停頓
確定: 對處理器資源比較敏感,跟用戶線程存在競爭關系;
? 無法處理 浮動垃圾, 只能等到下次gc 時被回收
? 采用 標記- 清除算 會產生內存空間的碎片化。
G1收集器
-
概念:
面向局部手機的設計思路和基于 Region 的內存布局形式,追求應用的分配速率,而非把java堆一次性清理干凈,是垃圾收集器技術發展史上的里程碑
按照職責分離的原則,在jdk10 中提出了統一垃圾收集器接口 jvmgl
停頓時間模型: 能夠支持在一個長度為m毫秒的時間片刻內,垃圾收集所用的時間不超過n毫秒的目標
Region:把連續的java堆劃分為多個大小相等獨立區域,每個Region都可以根據需要 扮演Eden區,Servivor區,老年代 收集器可針對不同的角色region使用不同的策略進行垃圾回收
Humongous:儲存打對象,合并多個額region存放這些大對象, 只要超過region的一半可判定為大對象, 每個regison大小可通過參數設定。
G1會根據統計每個region的回收的價值和收益 根據用戶設定的收集停頓時間,優先處理回收價值最大的那些region
-
待解決的問題
跨region引用對象的處理, 記憶集和卡表解決
并發標記階段如何保證收集線程和用戶線程互不干擾,采用原始快照算法實現。
如何建立可靠的停頓預測模型: 通過衰減均值理論實現,region的統計狀態月新越能決定他的回收價值。
-
4個步驟
初始標記: 單線程執行,暫停用戶線程標記gc roots 直接關聯的對象,并修改tams只針對的值
并發標記:對線程并發從gc roots的直接關聯的對象遍歷整個對象圖的過程
最終標記: 短暫暫停用戶線程,并行處理并發標記階段留下的satb記錄
篩選回收: 更新region的統計數據,對各個region的回收價值和成本進行排序,根據設定的期望停頓時間來指定回收計劃, 可選則多個region組成回收集,把其中存活的對象復制到空的region中,回收掉舊的region,這個過程需要移動存活對象,需要暫停用戶線程。
-
優點:
可指定最大停頓時間。
分region的內存布局,按收益動態確定回收的創新設計帶來巨大優勢。
G1從整體看是基于“標記 - 整理”算法實現的,從局部(兩個region)看有是局域 標記-復制算法實現的。
-
缺點: g1收集器執行 更加消耗內存,額外負載也比較高。
Shenandoah 收集器
目標是能在任何堆大小下都能吧垃圾收集的停頓時間限制在10毫秒以內
-
步驟
初始標記:單線程執行,暫停用戶線程,標記gc roots 直接關聯的對象, 并修改 tams只針對的值
并發標記:多線程并發從gc roots的直接關聯的對象遍歷珍格格對象圖的過程。
最終標記:短暫暫停用戶線程,并行處理并發標記階段留下的satb記錄
并發清理:清理掉那些整個區域中都不存在一個存貨對象的region
并發回收:多線程并發吧手機的存活對象賦值到為被使用的region中,使用讀屏障和 BrooksPointers 的轉發指針來處理用戶線程還在改變引用的問題
初始引用更新:短暫暫停用戶線程,簡歷一個線程集合點,確保所有的并發回收階段中手線程都已經完成分配給他們的對象移動任務。
并發更新引用:多線程并發執行,真正把對中所有指向舊對象的引用修正到復制后的新地址。
最終引用更新:暫停用戶線程,修正存在于gc roots中的而引用, 暫停時間和gc roots 中的數量成正比。
并發清理:珍格格回收集中的region 已無存活對象, 清理回收掉這些region即可。
ZGC收集器
基于region內存布局,不設置分代,使用了讀屏障,染色指針和內存多重映射等技術實現的可并發標記-整理算法的 以低延遲為首要目的收集器。
-
步驟:
并發標記: 跟g1一樣,需要經過初始標記,最終標記。
并發預備沖分配:根據特定的查詢條件得出收集過程清理那些region,把這些region組成 充分配集
并發重分配:把重分配集中存活對象復制到新的region中,并為重分配集中的每個region維護一個轉發表,記錄從舊對象到新對象的轉向關系。
并發重映射:修正整個對中指向重分配集中舊對象的所有引用。
如何選擇合適的收集器
收集器的權衡
應用程序關注是什么? 例如數據分析 科學計算,那就關注吞吐量,若是客戶端應用程序需要關注內存的占用。
運行應用的基礎設施?
jvm收集器日志
查看gc基本信息: < jdk9 使用 -XX:+PrintGC >= jdk9 使用 -Xlog:gc
查看gc詳細信息: < jdk9 使用 -XX:PrintGCDetails >=jdk9 使用 -Xlog:gc*
查看gc前后堆,方法區可用容量變化: < jdk9 使用 -XX:+PrintHeapAtGc >= jdk9 使用 -Xlog:gc+heap=debuge
內存分配與回收策略
自動管理內存的目標
自動給對象分配內存。
自動回收掉無用對象的內存
對象的生命旅程
對象優先分配在eden區域 : 大對數情況下,對象在新生代Eden區中分配,當Eden區域沒有足夠的空間時,則發送一次MinorGc
大對象直接進入老年代: 創建的打對象,需要連續的內存空間,指定大于-XX:PretenureSizeThreshold參數的對象,直接在老年代進行分配
長期存活的對象進入老年代: 誕生于Eden中的對象,經過Minor GC 一次之后對象的年齡就會+1(年齡保存到對象頭中),當年齡增加到-XX:MaxTenuringThreshold參數(默認15)設置的值時,晉升到老年代。
動態對象年齡判定”:在Survivor空間的相同年齡的對象大小總和大于單個Survivor空間的一半,則年齡>= 該年齡的對象直接進入老年代,無序關心年齡閾值。
空間擔保: 發生Minor gc之前, jvm先檢查老年代的最大可用的連續空間是否大于新生代對象的總和或者是歷次精神的平均大小,就會進行Minor GC, 否則直接Full GC
四、jvm性能分析工具
五、調優案例分析與實戰
java虛擬機管理大內存
- 回收大塊對內存導致的長時間停頓
- 打內存必須要有64位java 虛擬機的支持
- 必須保證應用程序的足夠穩定
- 相同的程序在64位虛擬機中消耗的內存比32位要大。
若干虛擬機獨立部署應用
- 節點競爭全局資源
- 很難高效率利用某些資源池
- 32位java虛擬機收到系統的內存限制
- 大量使用本地緩存,造成內存浪費。
六、類文件結構
兩種基礎數據類型
無符號數
基本的數據類型: 以u1, u2, u4, u8來分別表示1個字符 2 個字符 4 個字符 8 個字符的無符號數
表
多個無符號數或者其他表作為數據想構成的符合數據類型,便于區分,命名習慣性的以_info 結尾
class文件內容剖析
魔數與class文件的版本
每個class文件的前4個字節成為魔數, 唯一的作用就是確定是否為一個能被虛擬機接受的的class文件,固定值為: 0xCAFEBABE
次版本號:第5 6 兩個字節,jdk1.2 - jdk12之間這個值一直為0,jdk12后又再次啟用這個次版本號
主版本號:第7 8 兩個字符,應對jdk的版本號,每個jdk都對應一個版本號, jdk8 = 52
Winhex可以打開16進制的class文件
常量池
常量池容量計數值
- 主版本號后面就是常量池的入口,放置的是一個u2類型的數據,從1開始計數,代表常量池常量數量
- 把0項空出的目的在于如果后面某些指向常量池的索引值的數值在特定的情況下需要達到不引用任何一個常量池項目的含義,可以把索引值設置為0
常量池
- 可以比喻為class文件的資源倉庫
- 字面量: 接近java語言層面的常量的概念,如文本字符串,final修飾的常量
- 符號引用
- 被模塊導出或者開放的包
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的額名稱和描述符
- 方法句柄和方法類型
- 動態調用點和動態常量
- 項目類型:
- 常量池總每個項目都是一個表,戒指jdk13共有17中常量類型
- 17中常量類型的表結構第一個字節都是u1類型的標志位 標記屬于那種常量類型
- 圖示
訪問標志
常量池結束之后,跟著兩個字節是訪問標志,類或者接口的訪問信息,包括class還是interface,public類型 abstract類型,final類型
- ACC_PUBLIC 0x0001 是否為public類型
- ACC_FINAL 0x0010 是否被聲明final
- ACC_SUPER 0x0020 是否允許使用invokespecial指令新語義,1.0.2后比阿尼的次標記都為真。
- ACC_INTERFACE 0x0200 標識是個接口
- ACC_ABSTRACT 0x0400 是否為abstract類型
- ACC_SYNTHETIC 0x1000 標識這個類并非用戶代碼生成
- ACC_ACCOTATION 0x200 標識這是一個注解
- ACC_ENUM 0x4000 標識是個枚舉
- ACC_MODULE 0x8000 標識是個模塊
類索引,父類索引,接口索引集合
- 類索引 用于確定這個類的全限定名
- 父類索引 用戶卻抵擋這個類的父類的權限淡定名 除了object類之后其他所有的類的父類索引都不為0
- 接口索引集合 入口有一項u2類型的數據為接口計數器,若該類沒有實現任何接口,則該計數器為0, 后面的接口集合不占用任何字節。
- 類素銀和父類索引個使用一個u2誒膝蓋的索引值表示,各自指向儀個樂行為CONSTANT_Class_info的類描述符常量,通過改常量中的索引值可以找到定義在CONSTANT_Uft8_info 類型的常量中全限定名字符串
字段表
用戶描述接口或者類中聲明的變量,包括類級別的變量和實際級別的變量。
- 字段表結構, 包括字段的作用域(private protected public) 實例變量還是了變量(static)是否可被序列化(translent), 可變性(fianl) 并發可見性(volatile),字段類型(基本類型,對象,數組),字段名
- 字段表結合中不會列出父類和父接口中集成而來點的字段,但有可能出現原本java的代碼中不存在字段。
方法表集合
用與描述類中的方法的相關描述
依次包括 訪問標志 名稱索引,描述符索引 屬性表集合 等
方法里面的java代碼 編譯為字節碼指令之后 存放在方法屬性中一個名為 Code 的屬性里面
如果沒有重寫父類的方法,在方法表集合中就不會出現來自父類的方法信息,但是有可能出現 有編輯器自動添加的方法,例如:類構造器 clinit() 方法 實例構造器 init() 方法
屬性表集合
每個屬性 他的名稱都要從常量池中引用一個constant_uft8_info 類型的常量來表示,屬性值的構造則是完全自定義,通過一個u4長度的屬性說明屬性值占用的位數即可
- Code屬性
java 程序中方法體中的代碼被被編譯后,最終的字節碼指令存儲在code屬性中
但是并不是所有的方法表都存在這個屬性,例如 接口或這抽象類中的方法就不存在愛code屬性
- Exceptions屬性
在方法表與code屬性平級的一項屬性,作用是列列舉所有的可能拋出的受檢查異常,記載throws關鍵字后面列舉的異常。
- LinenumberTable屬性
描述java源代碼行號和字節碼行號(字節碼偏移量)之間的關系。
- LoccalVariableTable屬性
描述棧幀中局部變量表的變量和java源碼中定義的變量之間的關系
Jdk5 之后引入了泛型, 增加了這個屬性,有于描述符中的泛型參數化類型被擦書,所以使用了字段的特征來完成泛型的描述
- SourceFile屬性
用于記錄生成這個class文件的源碼文件名稱
- SourcecDebugExtension屬性
用戶儲存額外的代碼調試信息
- ConstantValue屬性
通知虛擬機自動給static的變量進行賦值
- InnerClasses屬性
記錄內部類和宿主類之間的關聯
- Deprecated屬性
屬于標志性的不二屬性,只有存在和不存在的區別,表示某個類 字段或者方法已不推薦使用
- Synthetic屬性
表示某字段或者方法比你更不是有java源碼生成的 而是編譯自己添加的
- StackMapTable屬性
Jdk6 新增 這個熟悉你在虛擬機類加載的字節碼驗證階段被新類型檢查驗證其使用,目的在于代替以前比較消耗性能的基于數據流分析的類型推到驗證器
- Bootstrapmethods屬性
jdk7新增,用戶保存 invokedynamic 指令引用的印引導方法限定符
- MethodParameters屬性
Jdk8 新增, 記錄方法行參數名稱和信息
字節碼指令
定義
指令由一個字節長度的,代表某種特定的擦操作含義的數字(操作碼)以及個跟隨氣候的0至多個代表次擦操作所需的參數 構成
jvm采用面向操作舒展而不是面向寄存器的架構,所以大多指令都不含操作數,僅有操作碼,志林干的參數放在操作數棧中。
字節碼與數據類型
-
打賭偶數的指令都包含其操作對應的數據類型信息
例如 iload 指令用于從局部遍歷表中加載int型的數據到操作數棧中
fload執行則是把float類型的數據加載到操作數棧
-
操作碼記助符中都有特殊的字符
i代表對int類型的數據操作, l表示long s表示 short b表示byte c表示char f表示 float d表示double a 表示 reference
分類
加載和存儲指令
-
加載和存儲指令用于吧數據在棧幀中的局部遍歷表和操作哦舒展之間傳遞
-
將一個局部變量加載達到操作數棧
iload, iload_<n>, lload, lload_<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n> -
將一個數值從操作數棧存儲到局部變量表
istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n> -
將一個常量加載到操作數棧
pipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_<i>, locounst_<l>, fcounst_<f>, dcounst_<d> -
擴沖局部變量表點的訪問索引指令
wide -
說明
上面的指令中有一分部是<n> 實際上表示的是一組命令,例如 iload_<n> 代表了iload_0, iload_1 , iload_2, iload_3 這幾個指令, 這種表示只是 iload 的這一種特殊形式,后面的數字其實表示的是 操作數 例如 iload_0 等價于 iload 0
運算符指令
-
算數指令用于對兩個操作數棧上的值進行某種運算,并把結果陳聰新存入操作數棧頂。
-
加減乘除指令
iadd, ladd, faddd, ddadd, isub, imul, idiv.... -
求余取反位移
irem, lrem, frem, drem, ineg, ishl -
按位或
ior, lor -
按位與
iand, land -
按位異或
ixor, lxor -
局部變量自增
iinc -
比較指令
ddcmpg, dcmpl, fcompg, fcmpl, lcmp
類型轉換指令
-
可以吧兩個不同類型數值類型進行互相轉化
-
寬化類型轉換
小范圍類型向大范圍類型安全的轉化,無序顯示轉化指令
int --> long, floag, double
long -> float, double.
fload -> double
-
窄話處理轉換
必須顯示地使用轉化指令來完成 i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2l, d2f
可能導致轉化結果產生不同的正負號,不同的數量級的情況,以及經度缺失。
對象與訪問指令
-
創建類實例指令
new
-
創建數組的指令
newarray, anewarray, multianewarray
-
訪問類字段,和實例字段的指令
getfield, putfield, getstatic, putstatic
-
把一個數組元素加載到操作數棧的指令
bdload, caload, saloadd, ialoadd, laload, faload, daload, aaload
-
把一個操作數棧的值存儲到數組元素中的指令
bastore, castore, sastore, iastoore, fastore, dastoore, aastore
-
取數組長度的指令
arraylength
-
檢查類實例類型的指令
instanceof, checkcast
操作數棧管理命令
-
棧頂一個或兩個元素出棧
pop, pop2
-
復制棧頂一個或兩個元素,并把復制的元素壓入棧頂。
dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2
-
將棧頂的兩個元素互換
swap
控制轉移指令
-
有條件或無條件地從指定位置指令的已下條指令開始執行
-
條件分支
ifeq, iflt, ifle, ifne, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_ifmpgt, if_icmple, if_icmplt, if_ifmpgt, if_icmple, if_icmpge, if_acmpeq, if_acmpne
-
符合條件分支
tableswitch, lookupswitch
-
無條件分支
goto, goto_w, jsr, jsr_w, ret
方法調用和返回指令
invokevirtual
用于調用對象的實例方法,根據對象的實際類型進行分派
invokeinterface
用于接口方法的調用,在運行時搜索一個實現了合格接口方法對象,找出適合的方法調用
invokespecial
用于調用一些特殊處理的實例方法,包括 實例初始化方法和私有方法 父類方法
invokestatic
調用類的靜態方法
Invokedynamic
用不在運行是動態解析出調用點限定符所引用的方法
異常處理指令
athrow 指令來完成實現, 在java虛擬機中處理異常catch采用異常表類完成
同步指令
java語言中的synchronized語句塊來表示的, 指令集中有monitorenter,和 moitorexit 兩條指令來支持這語義的
方法級別的同步是隱士的,無序通過字節碼指令來控制, jvm 可以通過常量池中的方法表結構 acc_synchronized 訪問標志符確定工藝個方法是否聲明為同步,方法級別的同步與代碼塊級別低的都使用管城(monitor) 來實現
七、虛擬機類加載機制
類的生命周期
加載,驗證,準備 , 解析,初始化,使用,卸載,其中驗證,準備 解析統稱為連接
6種情況必須立即對類進行初始化
使用如下字節碼指令的時候
遇到new,getstatic pustatic invokestatic指令時 類型沒有進行初始化,則需要先觸發其初始化階段
對應的java 代碼中的使用場景
? 使用 new 關鍵字實例化對象的時候
? 讀取或設置一個類型的靜態字段的時候(被final修飾,已經編譯期吧結果放入常量池的靜態字段除外)
? 調用一個類型的靜態方法的時候
使用java.lang.reflect包的方法的對類型進行反射調用的時候。
當類型初始化的時候,發現父類還沒有進行初始化,則需要先觸發器父類的初始化
當虛擬機啟動的時候,用戶需要制定一個啟動類,虛擬機會初始化這個主類
當s會用jdk7新加入的動態語言支持時, 部分方法句柄對應的類型沒有進行初始化,則需要進行初始化
一個接口定義了jdk8新家的默認方法,(default修飾)時,如果這個接口對應的實現類發生初始化,那么這個接口要先被初始化
類加載的過程
加載
- 通過一個類的全限定名來獲取定義此類的二進制字節流
- 通過字節流代表的靜態儲存接口準話為方法區的運行時數據結構
- 在內存中生成一個代表這個類的class對象,作為方法區這個類的數據訪問的入口
- 注意 改階段用戶程序可以通過自定義類加載器的方式進行局部參與
驗證
確保class文件的字節流中包含的信息符合java虛擬機規范的約束要求
文件格式驗證
字節流是否符合class文件的個是你規范,并且是否能被當前版本的虛擬機處理
是否魔數開頭: 0xCAFEBABE
主,次版本號是否在當前的java虛擬機接受范圍內
常量池中的常量是否偶不被支持的類型(檢查常量的tag)
元數據驗證
對字節碼描述信息進行語義分析并對元數據信息進行語義校驗
這個類是否有父類
這個類是否集成了不允許被繼承的類
如果這個類是抽象類是否實現了其父類或接口中所要求的所有的抽象方法
字節碼驗證
同構數據流分析和控制流分析,確定程序語義的合法性,符合邏輯性,對類的方法體進行校驗分析
保證在任何時刻操作數棧的數據類型與質量代碼序列都能配合工作
保證任何跳轉指令都不會跳轉到到方法體以外的字節碼指令上
符號引用驗證
虛擬機把符號引用轉化為直接引用的時候,這個動作會在解析階段中發生,檢查類型是否缺少或者禁止訪問他依賴的某些外部類,方法,字段等資源
符號引用中通過自費重描述的全限定名是否能找到對應的類
在指定類中是否存在和服方法的字段描述符以及簡單名稱所描述的方法和字段
符號引用的類,字段 ,方法的可訪問性 是否可被當前類訪問
準備
類的靜態變量 static 被分配內存并設置初始值的過程
靜態成員變量在準備階段過后初始設置為零值(不同數據類型都對應各自的零值 如 boolean false, reference: null)
靜態成員變量被賦值為java代碼中的值是在putstatic指令執行時完成,這個指令存放與類構造器()方法之中
靜態成員變量被final修飾,即講臺常量,那么在準備階段就會直接賦值為java代碼中的值
解析
java虛擬機將常量池內的符號引用替換為直接引用的過程
包括類或接口的解析,字段的解析,方法的解析
初始化
在初始化階段,則會根據程序編寫指定的主觀計劃去初始化類變量和其他資源,簡單的說其實就是執行類構造器() 方法的過程
() 是編譯器自動收集類中的所有類變量賦值動作和靜態語句塊中的語句的合并
靜態語句塊中只能訪問定義在他之前的變量,定義在他之后的變量只能進行復制操作,不能訪問
jvm會保證子類的clinit() 方法執行之前 父類的 clinit() 方法已經執行完畢, 也就意味著父類定義的靜態語句塊要優先于子類的類變量賦值操作
一個類中沒有類變量的賦值也沒有靜態代碼塊,那么編譯器可以不為這個類生成 clinit 方法
接口中不能使用靜態語句塊,但仍然有變量初始化的復制操作,因此接口也會生成 clinit 方法
jvm會保證一個類的clinit 方法只被執行一次 使用了 cas 同步鎖機制。
類加載器
類加載階段 通過一個類的全限定名來獲取描述該類的二進制字節流,放在虛擬機外部去實現,得以讓應用程序自己可以決定如何獲取所需要的類, 例如 類層次劃分 osgi 程序熱部署, 字節碼加密等
類與類加載器
類加載器用于實現類的加載動作
對于任意一個類,都必須有加載他的類加載器和這個類本身一起共同確立其在java虛擬機中的唯一性。
雙親委派模型
要求除了頂層的啟動類加載器之外, 其余的類加載器都應有自己的父類加載器,當一個類加載器收的到加載類的請求,先把請求委托給父類加載器,一直把請求發到底層,只有當父類加載器反饋自己無法加載這個類(他的搜索范圍中找不到所需的類)時,子類加載器才會嘗試自己完成加載
好處:java中類隨著他的類加載器一起舉杯了一種帶有優先級層次的關系
3類系統類加載器
-
啟動類加載器 boootstrap classloader
負責及愛在 $java_hone\lib 目錄 胡哦哦這 Xbootclasspath 指定路徑中農存放可被jvm識別的類庫加載打動jvm內存中 jvm 按照文件名識別, 例如 rt.jar tools.jar
-
擴展類加載器 Exensioon Classloader
負責加載 java_home\lib\ext 目錄中 或者 java.ext.dirs 變量指定的路徑中的類庫
-
應用程序類加載器 application classloader
負責加載用戶類路徑 classpath 上所有的類庫
八、虛擬機字節碼執行引擎
運行時棧幀結構
jvm以方法作作為最基本的執行單元,棧幀則是用于支持虛擬機允許方法調用和方法執行背后的數據結構,每個方法的調用到執行結束,都對應著一個棧幀的入棧到出棧的過程
每個棧幀都包括: 局部變量表,操作數棧,動態連接,方法返回地址,額外的附加信息
一組變量值的存儲空間,用于存放方法的參數和方法內部定義的局部變量,在編譯器就確定了最大容量,在code屬性點的max_loocals數據項中
包括方法的參數, 實例方法的隱藏傳參數this, catch定義的異常,方法體中的聲明的變量
變量槽
- 局部變量表的分配內所使用的最小單位,長度不超過32位的數據類型 (byte,char,float,int,short, boolean, return , returnAddress) 每個局部變量占用一個變量槽
- 64位的數據類型long和double放在兩個連續的變量槽中,reference跟虛擬機的實現有關,32位點的棧32位,64位的還需要砍是佛偶開啟指針壓縮
- 局部變量表哦中的變量槽可以重用,當pc計數器的值已經超過了某個變量的作用域,那么他對應的變量槽可以交個其他變量來重
操作數棧
- 操作數棧的最大深度也在編譯器就確定了最大深度,寫入了code屬性的max_stacks 數據項中
- 32位數據類型所占的棧容量為1, 64位的數據類型占用的容量為2
動態連接
- 每個站真逗包含一個執行運行時常量池中該棧幀所屬方法的引用,為了支持在方法的的調用的過程中的動態連接
方法返回地址
- 當前方法完成調用退出時,必須返回到方法被調用的地方,程序才能繼續執行,方法返回時需要在棧幀中保存一些信息,用來幫助恢復他的上層主調用方法的執行狀態。
附加信息
- 允許增加一些規范里面沒有描述的信息到棧幀中,例如 調試,性能手機的相關信息,跟虛擬機的實現有關。
方法的調用
解析
所有方法調用的目標class文件中都有一個常量池中的符號引用,在類加載的解析階段把斧蛤引用轉化為了直接引用
java源文件編譯完成之后,就確定可唯一調用的版本,可以在類加載中的解析階段吧符號引用直接解析為直接引用,包括靜態方法, 私有方法, 實例構造器,父類方法,被final修飾的方法
分派
分派的調用過程將會揭示一些多臺的最基本的提現
-
靜態類型
靜態類型的變化僅僅在使用時候偶發生,最終靜態類型實在編譯器可知的
-
實際類型
變化的結果只有在運行期才能確定
-
靜態分派
所有依賴靜態類型來決定方法的執行版本的分派動作叫做靜態分派,經典的應用就是在編譯期進行的靜態分派,進而通過參數確定使用哪個重載版本,并生成對應的字節碼指令。
虛擬機在重載時,通過參數的靜態類型作為判斷依據,靜態類型在編譯器可知的,所以在編一階段,根據參數的靜態類型決定了會使用哪個重載版本。
-
動態分派
在運行期間根據實際類型確定方法執行版本的分派過程稱為動態分派。
invokevirtual指令吧常量池中的方法的符號引用解析到直接引用上,并根據方法的接收者的實際類型來選擇版本。
-
單分派和多分派
單分派:根據一個宗量對對表方法進行選擇
多分派:根據多與一個宗量對目標方法進行選擇。
動態類型語言
類型檢查的主體過程是在運行期而不是在編譯器進行的,例如 javascript php python
那么在編譯器就進行類型的檢查過程的語言叫做靜態類型語言, 例如 java c++
java.lang.invoke
出了單純的依靠符號引用來確定的調用的目標方法這條路之外,提供一種新的動態確定目標方法的脊椎,成為 方法句柄 method handle
methodhandle 則設計為可服務于java虛擬機之上的語言, 包括java語言
invokedynamic指令
每個含有 invokedynsmic 指令的位置都被成為 動態調用點, 這條指令的代表符號引用是 constan_invokedynamic_info 常量
可以獲取3項信息:
引導方法: 有固定的參數,并且返回值是callsite對象, 這個對象黨代表了真正要執行的目標方法調用
方法類型
名稱
字節碼解釋執行
傳統編譯過程: 編寫源碼程序 -> 詞法分析 -> 語法分析 --> 抽象語法樹 —> 中間代碼 —> 生成器 --> 目標代碼
解釋執行過程: 編寫的源碼程序—> 詞法分析-----> 語法分析 ------> 抽象語法書 ------> 指令流 ------> 解釋器 ------> 解釋執行
javac編譯萬傳給你了程序代碼經過詞法分析, 語法分析到抽象語法書,再編譯整個語法書生成線性字節碼指令流的過程
javac編譯器輸出的字節碼指令流,基本上是采用基于棧的指令集架構,大部分的字節碼指令都是零地址指令,即他們的指令不帶參數,依賴操作數棧進行工作
九、類加載及子系統案例
Tomcat 正統的類加載架構
解決的問題
部署在同一個服務器上的兩個web應用程序鎖使用的java類庫可實現相互隔離
部署在同一個服務器上的兩個web應用程序鎖使用的javva類庫可實現相互共享
服務器需要保證吱聲的安全不受部署的web應用程序影響
支持jsp的web服務器
目錄結構和類加載器
/common目錄, 可被tomcat和所有的web應用程序共同使用, CommonClassLoader
/server目錄,可被tomcat使用,對所有的web應用成都不可見,CatalinaClassLoader
/shared目錄, 被所有的web應用程序共同使用, 但對tomact自己不可見 SharedClassLoader
/Webapp/WEB-INF目錄,僅僅可被web應用使用 WebappClassLoader
單獨處理jsp文件 JasperLoader
OSGI
基于java語言的動態模塊化規劃
字節碼生成技術
javac javassist cglib asm
動態代理, 代理類的處理羅家可以在原水方法進行環繞修飾, 記載調用原始方法之前或之后添加自己的代碼邏輯
Backport工具
把高版本的jdk編寫的代碼放到第版本jdk環境中部署運行
十、前端編譯與優化
代表性編譯器產品
前端編譯器, jdk 的javac eclipse jdt的增量式編譯器 ECJ
即時編譯器 HosSpot虛擬機的C1 C2 graal 編譯器
提前編譯器, JDK的Jaotc, GCJ, Ecelsior JET
javac 編譯器
編譯過程
準備階段: 初始化促使華插入式注解處理器
解析與填充符號表的過程
語法, 語法分析 構造出抽象語法樹
填充符號表, 產生符號地址和符號信息
插入式注解處理器的注解處理過程
分析與字節碼生成的過程
標注檢查, 對語法的靜態信息進項檢查
數據和控制流分析
解語法糖: 語法糖能錢少哦代碼量,增加程序的可讀性,解語法糖就是在編譯階段還原回原始的基礎語法結構
字節碼生成:把前面步驟所生成的信息轉為字節碼指令寫到磁盤中,并進行了少量的代碼添加和轉化工作 例如 實例構造器 init() 和構造器 clinit()
java語法糖
泛型
本質是參數化類型或者參數化多臺的應用,“類型擦除式泛型”
由于jdk5引入泛型時,java已經面世十余年,遺留點的代碼規模非常大,為了保證java5之后引入泛型以前的編譯點的class文件能夠繼續執行, 最終選擇了直接把現有的類型原地泛型化,不添加新的類型
-
類型擦除
讓所有的泛型化的實例類型 如ArrayList 自動成為arrayList 的子類型或者還原回他本身, 否則類型轉換就是不安全的
-
缺陷
類型擦除實現了泛型直接導致了對原始類型數據支持成了麻煩,因為支持int long 與object之間的強制轉換。
運行期間無法獲取泛型的類型信息
帶來了模棱兩可的 模糊情況,例如方法的重載參數是兩個不同的類型的list, 卻不能被編譯通過,因為類型被擦除了。
-
結論
從Signature屬性的出現,可以看出所謂類型擦除,僅僅是對方法的code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息, 這也是我們能過反射手段獲取到參數化類型的根本依據
-
值類型與為類型的泛型
Valhalla項目中規劃了多種泛型的實現方案,其中包括具現化
提供 值類型 的語言層面的支持
自動拆裝箱
== 運算在遇到算數運算符時會自動拆箱
equals() 方法不處理數據轉型的關系, 或者說數量類型一樣并且值一樣 才為真
條件編譯
條件為常量的if語句可以實現條件編譯
十一、后端編譯與優化
即時編譯器
java程序最初都是通過解釋器進行解釋執行的,當時jvm發現某個代碼塊執行的特別頻繁,就會吧這些代碼任定位熱點代碼,為提供熱點代碼的執行效率,jvm將會吧這些代碼編譯成本到底機器碼,完成這個任務的后端編輯器叫做即時編譯器
解釋器與編譯器
當程序要像循序啟動和執行的時候, 適合使用解釋執行,省去了編譯的時間,立即執行
程序啟動之后, 隨著時間的推移,編譯器會把越來越多的熱點代碼編譯成本地機器碼,減少解釋器的中間損耗,獲取更高的執行效率
熱點代碼
被多次執行的方法
被多次執行的循環體
不管是那種情況, 編譯的目標對象都是整個方法體
熱點探測判定
基于采樣的熱點探測: 周期性的檢查某個線程的調用棧頂,如果某個方法經常出現在棧頂,那這個方法就是熱點方法
基于計數器的熱點探測: 為每個方法創建計數器,統計方法的調用次數,執行次數超過一定的閾值則認為他是熱點方法
編譯過程
默認條件下,無論采用哪種編譯執行方式,虛擬機在編譯還未完成編輯之前, 都仍然按照解釋方法繼續執行代碼, 而編譯動作則交給編譯線程中進行
提前編譯器
提前吧字節碼編譯為本地機器碼,但這跟具體的硬件平臺信息相關, 無法做到一次編譯,到處運行的理念。
ART使用體檢編譯,在android的時間里大放異彩, 干掉了即使編譯器Dalvik
編譯器優化技術
方法內聯
把目標方法的代碼原封不動的復制到發起調用的方法中,避免發生真是的方法調用
方法內聯的條件
- 被調用方法是否是熱點代碼
- 被調用方法是否大小合適
- 運行時方法是否可唯一確定
逃逸分析
分析對象的動態作用域,當一個對象在方法里面被定義后,他可能被外不方法所引用(例如 調用參數傳遞到其他方法中) 這稱之為方法逃逸
當前線程中的對象賦值給其他線程中訪問的實例對象, 這稱之為 線程逃逸
代碼優化:
如果能證明一個對象不會逃逸打動方法或者線程之外或者逃逸成都比較低,則可以為這個對象采取不同程度的優化。
-
棧上分配
如確定一個對象不會逃逸出線程之外,那么這個對象可以在棧上分配內存,這樣對象所占用的內存空間就會隨著棧幀的出棧而銷毀
-
標量替換
- 標量: 若一個數據無法在分解成更小的數據來表示,(int,long, reference類型等)不能進一步拆分,那么這些數據稱之為標量
- 聚合量:相反地,一個數據可以進行被分解,那么這些數據稱之為聚合量。
- 標量替換: 把一個java對象拆散,根據程序的訪問情況,把用到的成員比阿尼朗回復為原始類型來訪問,這個過程為標量替換。
- 如果一個對象不會被方法外部訪問,并且這個對象可以被拆散,那么程序真正執行的時候將可能不去創建這個對象。
-
同步消除
如果一個對象不會逃逸存儲線程,無法被其他線程訪問,那么這個對象的讀寫肯定不會有競爭,那么對這個對象的同步措施就可以安全的消除。
公共子表達式消除
如果一個表達式E已經被計算過了,并且從先前計算到現在E中所有的變量的值都沒有發生改變,那么E的這次出現成為公共子表達式
數組邊界檢查消除
在訪問數組元素的時候,系統將自動會檢查上下界限,編譯器通過數據流分析可以得知操作元素的下表不會超過數據的范圍,則可以進行數組上下界檢查的消除。
Graal編譯器
jdk10增加jvmci,虛擬機編譯器接口
響應HotSpot的編譯請求,并將該請求分發給java實現的就是編譯器。
允許編譯器訪問Hostpot中與即時編譯相關的數據結構,包括類,字段 方法 性能監控數據等。
提供HotSpot代碼緩存的java端抽象,以便于部署編譯完成的二進制機器碼
代碼中間層表示
中間層表示也被等價的稱之為理想層,從編譯器內部來看整個過程, 字節碼 ----> 理想圖 ---->優化 ---->機器碼的轉變過程
代碼優化與生成
-
生成理想圖
理想圖本省的數據結構:一組不為空的節點集合,他的節點都是用valueNode 的不同類型的子類節點表示
字節碼生成理想圖:可以按照字節碼解釋器的思路去理解他。
-
理想圖的操作
規范化:如何縮減理想圖的規模,在理想圖的基礎上優化代碼索要采取的措施,對于理想圖的規范化不限于單個操作碼范圍內,很多都是立足于全局進行的。
理想圖轉化為機器碼:先生成低級中間表示LIR 然后在用Hotspot 同一后端來產生機器碼
十二、java內存模型與線程
java內存模型
主內存和工作內存
java內存模型規定了所有的變量都存在主內存中
每條線程都有自己的工作內存,線程的工作內存中保存了唄改線程使用的變量的副本, 線程對變量的操作多發生生在工作內存中,線程之間的變量值的傳遞需要通過住內存來完成
內存間的交互操作
一個變量如何從主內存拷貝到工作內存, 又如何從工作內存同步到內存中,java內存模型定了8種操作來完成
- lock 鎖定 作用與主內存的變量 把一個變量標志位一條線程獨占的狀態
- unlock 解鎖 作用于主內存的變量, 把一個處于鎖定狀態的變量釋放出來,此時才可以被其他線程鎖定
- read讀取 作用于主內存的變量,把一個變量從住內存中傳輸到工作內存中
- load 載入 作用于工作內存的變量,把從主內存read過程來的變量放入工作內存變量副本中
- use 使用 作用于工作內存的變量, 工作內存中的一個變量值傳遞給執行引擎
- assign 賦值 作用于工作內存的變量, 把執行引擎接受的值賦值給工作內存的變量
- store 存儲 作用于工作內存的變量,工作內存中的一個變量的值傳送到主內存中
- write 寫入 作用于主內存的變量, 把store 操作的變量放入主內存的變量中
針對8中操作的規則
- 不允許read 和load store 和write 操作之一單獨出現
- 不允許一個線程丟棄他最近的assign 操作
- 不允許以個線程沒有經過assign操作的值同步回主線程
- 一個新的變量只能在主內存中產生
- 一個變量同一個時刻只運行一條線程對其進行lock操作
- 如果對一個變量執行lock 操作, 那將會清理掉工作內存中次變量的值
- 如果一個變量實現沒有被lock操作鎖定,那么不允許使用unlock操作
- 對一個變量執行unlock操作之前,必須先把此變量同步會住內存
volatile型變量的特殊規則
-
兩項特性
保證此變量對所有線程的可見性,當一個線程修改了這個變量的值,那么其他線程都可以立即得知
禁止指令重排序
-
volatile規則
工作內存中,每次使用被volatile修改的變量錢都先從主內存中刷新最新的值到本地內存中
工作內存中,每次修改給volatile修飾的變量都必須立即同步回主內存中
要求被volatile修改變量不會被指令重排序優化,從而保證代碼的執行順序與程序的順序相同
原子性,可見性,有序性
-
原子性
一系列操作是一個整體,要么成功,要么失敗,在java內存模型中,大致可以認為,基本數據類型的訪問,讀寫都是具備原子性 除了long和double的非原子性協定
-
可見性
當一個線程修改了共享變量,其他線程能夠立即得知這個修改
java內存模型中通過主內存作為傳遞媒介,不同的線程可修改或讀取主流程的變量,而被volatile修改的變量的特殊之處在于,被修改的變量能立即同步到主內存,每次使用變量之前都從主內存中讀取。
java中能保證可見性的三個關鍵性, volatile synchronized, final
-
有序性
線程內表現為串行語義,指令重排序現象和工作內存與主內存延遲的現象
先行發生原則
判斷數據是否存在競爭,線程是否安全的非常有用的手段,說操作a現行發生于操作b,那么是說操作b發生之前,操作a產生的應先功能被操作b觀察到
-
程序次序規則
在一個程序內,按照控制流順序,書寫在前面的操作現行發生于書寫在后面的操作
-
管城鎖規則
一個unlock操作操作先行發生于后面對同一個多的lock操作
-
volatile變量規則
對一個volatile變量的寫操作現行發生于后面對這個變量的讀操作
-
線程啟動規則
Thread對象的start()方法現行發生于此線程的每個動作
-
線程終止規則
線程中所有發生的操作都先行與線程的終止操作
-
線程中斷規則
對線程的interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷時間的發生
-
對象的直接規則
一個對象的初始化完成先行發生于他的finalize()方法的開始
-
傳遞性
操作a先行與操作b 操作b先行與操作c 那么可以得出操作a先行發生于操作c的結論
java的線程
java的每一個線程都直接映射到一個操作系統線程線程來實現的
-
新建New
新建: 創建后尚未啟動
-
運行 Runing
運行:包括操作系統線程狀態中的Runing和Ready,處于此狀態的線程有可能正在執行,也有可能等待操作系統給他分配執行時間
-
無限等待 Waiting
無線等待:不會被分配處理器的執行時間,需要等待其他線程的顯示地喚醒[包括 Object::wait(), Thread:: join() , LockSUpport::park()]
-
限期等待Timed Waiting
限期等待:不會被分配處理器執行時間,達到等待時間之后有系統自動喚醒[包括Thread::sleep(s), Object:wait(s), Thread:join(s), LockSupport:parkNanos(), LockSupport:parkUnitl()]
-
阻塞 Blocked
堵塞:等待這獲取到一個排他鎖, 這個時間將在另外一個是線程放棄這個鎖的時候發生
-
結束 Terminated
已經終止的線程狀態
十三、線程安全與鎖優化
線程安全
線程安全的定義
當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和膠體執行,也不需要進行額外的同步,在調用方也不需要進行其他的協調操作,調用這個對象可以獲取正確的結果,那這個對象是線程安全的
JAVA語言中的線程安全
-
共享操作數據分為5類
不可變 : 不可變的兌現過一定是線程安全的,例如 final關鍵字,被final修飾的string intger 就不可變
絕對線程安全: 不管運行環境如何,調用者都不需要額外的同步措施
相對線程安全: 通過意義上將的線程安全
線程兼容: 對象本身并不是線程安全的, 但是可以通過在調用端正確的使用同步手段來保證對象在迸發環境中安全使用
線程對立: 不管調用端是否采用了同步措施,都無法在多線程環境中迸發使用代碼
-
線程安全的實現方法
-
互斥同步(悲觀鎖)
-
同步
多個線程迸發訪問共享數據時,保證共享數據在同一時刻只被一條線程使用
缺點: 無論共享的數據是否出現競爭,先任務有競爭的線程,進行加鎖,浙江會導致用戶狀態和心態轉換,維護所計數器和檢查是否有唄堵塞等開銷
-
互斥
實現同步的一種手段
-
互斥是因,同步是過,互斥是方法,同步是目的
-
synchrionized
最基本的互斥同步手段,javac編譯之后生成monitorenter, monitorexit 兩個字節碼指令,這兩個指令都需要制定一個reference類型的參數來指明要鎖定的和解鎖的對象
執行monitorenter指令時,首先嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當前線程已經持有了這個對象的鎖,就吧鎖的計數器的值增加一,而執行monitorexit指令時把鎖的計數器減一,計數器值為0,鎖就會被釋放,如果獲取對象鎖失敗,那當前線程就應當被阻塞等待,知道請求鎖定的對象被持有他的線程釋放為止。
被synchronized修飾的同步塊對同一條線程來說是可重入的。
被synchronized修飾的同步塊在持有所的線程執行完釋放鎖之前 會無條件的阻塞后面的其他的線程進入
-
lock接口
ReentrantLock可重入鎖
- 等待可中斷: 長時間等待鎖釋放的線程,可以選擇放棄等待處理其他事情。
- 公平鎖: 多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來一次獲取鎖
- 鎖綁定多個條件: 值以個ReentrantLock對象可以同時綁定多個Condition對象
ReentrantReadRriteLock
-
-
非堵塞同步(樂觀鎖)
- 先不管風險,直接進行操作,出現了沖突在進行一段時間不長行的重試,知道出現沒有競爭的共享數據位置,不在需要把線程堵塞掛起。
- CAS:比較并交換,硬件處理器直接通過一條指令完成,屬于原子操作。
- 需要三個參數:修改一個變量V的值為B, 先檢查符合就的預期值A的話,則更新成功,負責更新失敗。
- ABA問題
-
無同步方案
如果一個方法本來就沒有涉及到共享數據,那么他自然就不需要在添加任何的同步措施
可重入代碼:一個方法輸入相同的數據,都能返回相同的結果,則該方法的返回值是可預測的。
線程本地存儲,共享數據的可見范圍限制在一個線程內部,通過ThreadLocal類實現線程本地儲存的功能
-
鎖優化
jdk6實現了各種所優化技術,當使用synchronized加鎖時,在鎖定對象之前,先進行了一些列的鎖優化
-
自旋鎖
自選等待避免了線程切換的開銷,但是卻要占用處理器的時間,如果占用時間過長反而帶來的收益會降低,所以自旋有次數的限制,達到這個次數之后任然沒有成功獲得鎖,就使用傳統的方式掛起線程。
JDK6對自旋鎖做了優化,引入了自適應,意味著自旋的限制次所不在固定,jvm會根據同一個對象的上次獲得鎖次數和擁有這的運行狀態判斷,允許自旋次數的適當增加,另一方面,自旋很少成功獲得鎖,那在以后獲取鎖將有可能直接省掉自旋過程,以避免浪費處理器資源。
-
鎖消除
jvm即時編譯器在運行時, 對一些代碼要求同步,但是對唄檢測到的不可能存在的共享數據競爭的鎖進行消除,鎖消除的主要判定來源于逃逸分析的數據支撐。
-
鎖粗化
如果jvm探測到有一串零碎的操作都是對同一個對象加鎖,將會把加鎖同步的范圍擴展到整個操作序列的外部。
-
輕量級鎖
在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗
加鎖工作過程
- 進入同步代碼塊的時候,若同步對象沒有被鎖定(對象投中所標記為是 01 ) 則jvm在當前線程的棧幀中簡歷一個名為所記錄的空間,存儲所對象的mark work 的拷貝。
- jvm使用cas嘗試吧對象的mark work更新為鎖記錄的指針
- 更新成功 則表示改線程擁有了對象的鎖,同時鎖標記為改成了 00,表示對象處于輕量級鎖狀態
- 如果更新失敗,則表示至少有一條線程跟當前線程競爭這個對象的鎖,檢查對象的wark word是否執行當前線程的棧幀,如果是,說明當前的線程已經擁有了這個對象的鎖,直接執行同步塊的代碼
- 檢查對象投若不是當線程的棧幀,說明鎖對象已經被其他線程給占用了,必須要膨脹為重量鎖,標記為改為 10, 此時mark word中存儲的是重量級所的指針
-
偏向鎖
消除數據在無競爭情況下的同步原語進一步提升程序的性能
這個鎖會偏向于第一個獲取他的線程,如果在接下來的執行過程中,該所一直被其他的線程獲取,則持有偏向鎖的線程將永遠不需要進行同步
總結
以上是生活随笔為你收集整理的深入理解java虚拟机脑图文档的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 权限管理中的RBAC与ABAC
- 下一篇: CTFSHOW-信息搜集