深刻理解JVM讀書筆記

 

 

 

 

第一部分 走進Java

第1章 走進java

1.1 概述

1.2 java技術體系

  • java程序設計語言、java虛擬機、java API類庫統稱爲JDK,JDK是用於支持java程序開發的最小環境
  • java API類庫中的java SE API子集和java 虛擬機統稱爲JRE,JRE是支持java程序運行的標準環境

1.3 java發展史

1.4 java虛擬機發展史

1.4.1 Sun Classic/Exact VM

  • Exact VM使用準確式內存管理:虛擬機能夠知道內存中某個位置的數據具體是什麼類型,這樣能在GC的時候準確判斷堆上的數據是否還可能被使用。

1.4.2 Sun HotSpot VM

  • SUN JDK和OpenJDK中所帶的虛擬機
  • 熱點代碼探測技術:經過執行計數器找出最具備編譯價值的代碼,而後通知JIT編譯器以方法爲單位進行編譯。經過編譯器與解釋器協同工做,可在最優化的程序響應時間與最佳執行性能中取得平衡,且無需等待本地代碼輸出才能執行程序。

1.4.3 Sun Mobi-Embe VM / Meta-Circular VM

1.4.4 BEA JRockit /IBM J9 VM

1.4.5 Azul VM / BEA Liquid VM

1.4.6 Apache Harmony / Google Android Dalvik VM

1.4.7 Microsoft JVM及其它

1.5 展望java技術的將來

1.5.1 模塊化

1.5.2 混合語言

1.5.3 多核並行

1.5.4 進一步豐富語法

  • forkjoin包
  • lambda

1.5.5 64位虛擬機

1.6 實戰:本身編譯JDK

1.6.1 獲取JDK源碼

1.6.2 系統需求

1.6.3 構建編譯環境

1.6.4 進行編譯

1.6.5 在IDE工具中進行源碼調試

1.7 本章小結

 

第二部分 自動內存管理機制

第2章 Java內存區域與內存溢出異常

2.1 概述

 

2.2 運行時數據區域

2.2.1 程序計數器

  • 可看做是當前線程所執行的字節碼的行號指示器,字節碼解釋器經過改變計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復都須要依賴它實現
  • 每一個線程都有獨立的程序計數器,各線程之間互不影響,獨立存儲
  • 若是線程正在執行的是java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址,若是是Native方法,則計數器值爲空(Undefined)
  • 惟一一個不會出現OutOfMemoryError的區域

2.2.2 Java虛擬機棧

  • 虛擬機棧描述的是java方法執行的內存模型
  • 每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態鏈表、方法出口等信息。
  • 局部變量表存放了編譯器可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,不等同於對象自己,多是一個指向對象起始地址的引用指針,也可能指向一個表明對象的句柄或其它與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)
  • long和double類型的數據會佔用2個局部變量空間(slot),其他只佔1個
  • 局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,方法運行期間不會改變局部變量表的大小
  • 若是線程請求的棧深度大於虛擬機所容許的深度(通常1000-2000),將拋出StackOverflowError異常;若是棧拓展時沒法申請到足夠的內存,會拋出OutOfMemoryError

2.2.3 本地方法棧

  • 與虛擬機棧做用相識,區別是爲虛擬機使用到的Native方法服務
  • 虛擬機規範對本地方法棧中方法使用的語言、使用方式與數據結構並無強制規定,甚至(如Sun HotSpot)直接把本地方法棧與虛擬機棧合二爲一
  • 與虛擬機棧同樣,也會拋出StackOverflowError和OutOfMemoryError異常

2.2.4 java堆

  • 被全部線程共享,在虛擬機啓動時建立,惟一目的就是存放對象實例,是垃圾收集器管理的主要區域
  • 從內存回收的角度看,因爲內存收集器基本都採用分類收集算法,因此可分爲:新生代和老年代,新生代細緻點可分爲Eden、Form Survivor、To 空間
  • 從內存分配的角度看,線程共享的java堆中可能劃分出多個線程私有的分配緩衝區(TLAB)
  • java堆能夠處於物理上不連續的,只須要邏輯上連續便可
  • -Xms和-Xmx可設置java堆大小

2.2.5 方法區

  • 與java堆同樣,各個線程共享的內存區域,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據
  • java虛擬機規範對方法區的限制很是寬鬆,甚至能夠選擇不實現垃圾收集
  • 相對而言,垃圾收集行爲在這個區域較少出現,該區域的主要回收目標是常量池的回收和類型的卸載,條件至關苛刻
  • 舊版本的HotSpot虛擬機選擇把GC擴展到方法區,但本質上方法區與堆並不等價,JDK1.7的HotSpot已經把本來放在永久代的字符串常量池移出
  • 當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常

2.2.6 運行時常量池

  • 運行時常量池是方法區的一部分
  • Class文件中除了有類的版本、字段、接口、方法等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放
  • 運行時常量池相對於Class文件常量池的另外一個特徵是具有動態性,java語言可在運行期間將新的常量放入池中,好比String類的intern()方法
  • 當常量池沒法再申請到內存時,將拋出OutOfMemoryError異常

2.2.7 直接內存

  • 例如NIO能夠使用Native函數庫直接分配堆外內存,而後經過存儲在java堆中的DirectByteBuffer對象做爲這塊內存的引用,但這仍然會受到本機總內存大小以及處理器尋址空間的限制
  • 直接內存並非虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的內存區域,但使用錯誤也會出現OutOfMemoryError異常

2.3 HotSpot虛擬機對象探祕

2.3.1 對象的建立

  • 1、當虛擬機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過,若是沒有就執行相應的類加載過程
  • 2、類加載檢查經過後,虛擬機爲新生對象分配內存,對象所需的內存大小在類加載完成後即可以徹底肯定,將一塊肯定大小的內存從java堆中劃分出來。併發下分配完修改指針會出現線程安全問題,解決方案一種是分配內存空間的動做進行同步處理--實際上虛擬機採用cas加上失敗重試保證原子性,另外一種把內存分配的動做按照線程劃分在不一樣的空間之中進行,每一個線程在java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB),哪一個線程須要分配內存,就在哪一個TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定。虛擬機是否使用TLAB能夠經過 -XX:+/-UseTLAB參數來設定
  • 3、虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB,這一工做提早至TLAB分配時進行
  • 4、虛擬機對對象進行必要的設置,例如這個對象是哪一個類的實例、如何找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息存放在對象頭中。
  • 5、從虛擬機的角度來看,對象已經產生,但從Java程序視角來看,對象建立纔剛剛開始,"init"方法還沒執行,全部字段仍是零。執行完new指令後接着執行init方法,按照程序員的意願進行初始化,真正可用的對象纔算徹底產生

2.3.2 對象的內存佈局

  • HotSpot虛擬機中,對象在內存中的佈局分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
  • 對象頭(Header):一部分存儲對象自身運行時的數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。另外一部分是類型指針,指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。若是對象是一個java數組,在對象頭中還必須有一塊用於記錄數組長度,普通Java對象可經過元數據肯定大小,可是數組不行
  • 實例數據(Instance Data):對象真正儲存的有效信息,程序代碼中定義的各類字段內容,包含父類繼承而來的。
  • 對齊填充(Padding):並非必然存在,佔位做用

2.3.3 對象的訪問定位

  • java程序經過棧上局部變量表的reference數據來操做堆上的具體對象。java虛擬機規範並不關心實現。目前主流的訪問方式有句柄訪問和直接指針兩種
  • 句柄訪問:java堆中劃分出一塊內存做爲句柄池,reference中存儲的就是句柄地址,而句柄中包含了對象實例數據與類型數據具體的地址信息

  • 直接指針訪問:java堆對象的數據中放置對象類型數據的指針信息,reference直接指向對象地址

2.4 實戰:OutOfMemoryError異常

2.4.1 java堆溢出

  • 對象數量達到最大堆的容量限制後會產生內存異常。
  • -XX:+HeapDumpOnOutOfMemoryError可讓虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照
  • 異常內容"java.lang.OutOfMemoryError:Java heap space",須要區分是內存泄漏仍是內存溢出
  • 內存泄漏:沒法被回收致使內存耗盡,可經過工具查看泄露對象到GC Roots的引用鏈
  • 內存溢出:空間過小不夠用,調節虛擬機堆參數(-Xmx與-Xms)

2.4.2 虛擬機棧和本地方法棧溢出

  • -Xss參數設定棧大小
  • 若是線程請求的棧深度大於虛擬機所容許的最大深度(虛擬機默認通常是1000~2000),將拋出"java.lang.StackOverflowError"異常
  • 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,將拋出"java.lang.OutOfMemoryError:unable to create new native thread"異常

2.4.3 方法區和運行時常量池溢出

  • 運行時常量池是方法區的一部分
  • 方法區存放Class相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述。反射會動態生成類,這些信息也會填充到方法區致使溢出
  • -XX:PermSize和-XX:MaxPermSize設置方法區大小
  • 異常內容"java.lang.OutOfMemoryError:PermGen Space"

2.4.4 本機直接內存溢出

  • DirectMemory可經過-XX:MaxDirectMemorySize指定,若是不指定,默認與java堆最大值(-Xmx)同樣
  • 異常內容"java.lang.OutOfMemoryError"沒法看見明顯的異常

2.5 本章小結

 

 

第3章 垃圾收集器與內存分配策略

3.1 概述

  • GC三要素:
  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

3.2 對象已死嗎

3.2.1 引用計數法

  • 定義:給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1,引用失效時減1,計數器爲0的對象就是不可能再被使用。
  • 沒法解決對象之間循環引用問題

3.2.2 可達性分析算法

  • 定義:經過一系列的稱爲"GC Roots"的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話說,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。
  • 在java語言中,可做爲GC Roots的對象包括下面幾種:
  1. 虛擬機棧(棧幀中的局部變量表)中引用的對象;
  2. 方法區中類靜態屬性引用的對象;
  3. 方法區中常量引用的對象;
  4. 本地方法棧中JNI(既通常說的Native方法)引用的對象。

3.2.3 再談引用

  • 引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)
  • 引用強度:強>軟>弱>虛
  • 強引用:只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。相似"Object obj = new Object"。
  • 軟引用:在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。SoftReference類來實現軟引用。
  • 弱引用:被弱引用關聯的對象只能生存到下一次垃圾收集器發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。WeakReference類來實現弱引用。
  • 虛引用:一個對象是否有虛引用的存在,不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。PhantomReference類來實現虛引用

3.2.4 生存仍是死亡

  • finalize()方法是對象逃脫死亡命運的最後一次機會,若是對象要在finalize()中成功拯救本身,只要從新與引用鏈上的任何一個對象創建關聯便可。
  • finalize()方法只會執行一次,下一次GC時仍然會被回收掉。
  • finalize()運行代價高昂,不肯定性大,沒法保證各個對象的調用順序,平時不要使用它。

3.2.5 回收方法區

  • 雖然java虛擬機規範不要求虛擬機在方法區實現垃圾收集,但部分虛擬機中仍然實現了方法區的回收。緣由是反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
  • 永久代的垃圾收集主要回收兩部份內容:廢棄常量和無用的類。
  • 廢棄常量的斷定:當前系統中沒有任何對象的實例,就會被清理出常量池。
  • 無用的類斷定
  1. 該類的全部實例都已經被回收,也就是java堆中不存在該類的任何實例
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法
  • HotSpot參數控制
  1. 是否對類進行回收 :-Xnoclassgc
  2. 查看類加載和卸載信息:-verbose:class、-XX:+TraceClassLoading、-XX:+TraceClassUnLoading

3.3 垃圾收集算法

3.3.1 標記--清除算法

  • 定義:分爲"標記"和"清除"兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。
  • 缺點:一是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,致使分配大對象時沒法找到足夠的連續內存而提早觸發GC。

3.3.2 複製算法

  • 定義:它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。
  • 優勢:實現簡單,運行高效,內存分配時不用考慮內存碎片等複雜狀況
  • 缺點:內存空間要求高
  • 算法步驟:將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活着的對象一次性地複製到另外一快survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。
  • 新生代中的對象98%是"朝生夕死",不須要按1:1來劃分空間,HotSpot虛擬機默認Eden和Survivor的大小是8:1。
  • 當Survivor空間不夠用時,須要依賴其它內存(指老年代)進行分配擔保

3.3.3 標記--整理算法

  • 老年代通常使用"標記--整理"算法。
  • 定義:標記過程與"標記--清除"同樣,可是後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一段移動,而後直接清理掉端邊界之外的內存。

3.3.4 分代收集算法

  • 根據對象存活週期的不一樣將內存劃分爲幾塊。通常分爲"新生代"和"老年代"。
  • 新生代:每次垃圾收集都有大批對象死去,只有少許存活,選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集
  • 老年代:對象存活率高、沒有額外空間進行擔保,只能使用"標記--清理"或者"標記--整理"算法

3.4 HotSpot的算法實現

3.4.1 枚舉根節點

  • 可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中
  • 分析GC Roots期間不能夠出現對象引用關係仍然變化的狀況,必須停頓全部java執行線程(Sun稱爲"Stop The World")
  • 目前主流的java虛擬機使用的都是準確式GC,虛擬機有辦法直接得知哪些地方存放着對象引用
  • 在HotSpot中,使用一組稱爲OopMap的數據結構,在類加載完成的時候,HotSpot就把對象內各個偏移量是什麼類型計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。GC在掃描時就能夠直接得知這些信息

3.4.2 安全點

  • 程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點(Safepoint)時才能暫停。安全點的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過度增大運行時的負荷。
  • 安全點的選定是以程序"是否具備讓程序長時間執行的特徵"爲標準進行選定的,最明顯的特徵是指令序列複用,例如方法調用、循環跳轉、異常跳轉等。
  • 在GC發生時須要讓全部線程"跑"到最近的安全點上停頓,有兩種方案"搶先式中斷(Preemptive Suspension)"和"主動式中斷(Voluntary Suspension)"
  1. 搶先式中斷:不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它"跑"到安全點上。如今幾乎沒有虛擬機採用這種方式
  2. 主動式中斷:當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標記被觸發時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方

3.4.3 安全區域

  • 定義:安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。
  • 來源:部分線程處於"不執行"(好比Sleep或者Blocked)的狀態,這時候線程沒法響應JVM的中斷請求,須要安全區域(Safe Region)來解決。
  • 步驟:在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者時整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開Safe Region的信號位置。

3.5 垃圾收集器

  • 沒有最好的收集器,更沒有萬能的收集器,只有最合適的收集器

3.5.1 Serial收集器

  • 單線程收集器,使用一個CPU或一條收集線程去完成收集工做。在進行垃圾收集時,必須暫停其它全部的工做線程,直到它收集結束。
  • 優勢:簡單而高效(與其它收集器處於單線程的狀況下相比,沒有線程交互的開銷),是虛擬機運行在Client模式下的默認新生代收集器

3.5.2 ParNew收集器

  • Serial收集器的多線程版本,除了使用多線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略都與Serial收集器徹底同樣。
  • 它是許多運行在Server模式下的虛擬機首選的新生代收集器。其中與性能無關的緣由是,除了Serial收集器,目前只有它能與CMS收集器配合工做。
  • 控制參數:
  1. -XX:+UseConcMarkSweepGC:使用此命令後ParNew會默認爲新生代收集器
  2. -XX:+UseParNewGC:強制使用ParNew收集器
  3. -XX:+ParallelGCThreads:限制垃圾收集的線程數

3.5.3 Parallel Scavenge收集器

  • 新生代收集器,並行的多線程收集器,使用複製算法
  • 與其它收集器不一樣的是它的關注點是達到一個可控制的吞吐量(吞吐量 = 運行代碼時間 / (運行代碼時間 + 垃圾收集時間))
  • 控制參數
  1. -XX:MaxGCPauseMillis:最大垃圾收集停頓時間,大於0的毫秒數。GC停頓時間的縮短是以犧牲吞吐量和新生代空間換來的。
  2. -XX:GCTimeRatio:設置吞吐量大小,是一個大於0且小於100的整數
  3. -XX:+UseAdaptiveSizePolicy:開關參數,GC自適應調節策略,根據當前系統的運行狀況自動調整參數(新生代大小-Xmm、Eden與Survivor區的比例-XX:SurvivorRatio、晉升老年代對象大小-XX:PertenureSizeThreshold等細節)以提供最合適的停頓時間或者最大的吞吐量。

3.5.4 Serial Old收集器

  • Serial收集器的老年代版本,單線程收集器,使用"標記--整理"算法,主要意義在於給Client模式下的虛擬機使用
  • Server模式下用途:一是JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,二是做爲CMS收集器的後續預案

3.5.5 Parallel Old收集器

  • Parallel Scavenge的老年代版本,使用"標記--整理"算法
  • 一樣是"吞吐量優先",在注重吞吐量以及CPU資源敏感的場合,可優先考慮Parallel Scavenge加Parallel Old收集器

3.5.6 CMS收集器

  • CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。重視響應速度、但願系統停頓時間最短是可考慮使用
  • CMS收集器是基於"標記--清除"算法實現的,整個過程分爲4個步驟
  1. 初始標記:單線程,"Stop The World",速度很快,標記GC Roots能直接關聯到的對象
  2. 併發標記:單線程,可與其它線程並行,耗時較長,GC Roots Tracing,追蹤其它關聯對象。
  3. 從新標記:多線程,"Stop The World",耗時比初始標記長,遠低於併發標記,修正併發標記期間因爲程序繼續運行所致使的標記變更。
  4. 併發清除:單線程,可與其它線程並行,耗時較長,清理對象
  • 缺點:
  1. 對CPU資源很是敏感,併發階段由於佔用一部分線程(或者說CPU資源)致使應用程序變慢。
  2. 沒法處理浮動垃圾(Floating Garbage),可能出現"Concurrent Mode Failue"失敗致使另外一次Full GC的產生。併發清理階段程序產生了新的垃圾,若是CMS運行期間預留的內存沒法知足程序要求,就會出現"Concurrent Mode Failue"失敗
  3. CMS是"標記--清除"實現的收集器,會致使大量碎片空間。-XX:+UseCMSCompactAtFullCollection(默認開啓)CMS開啓內存碎片合併,會致使停頓時間變長。-XX:CMSFullGCsBeforeCompaction用於設置執行多少次不壓縮的Full GC後,來一次壓縮的(默認爲0,每次都要整理)

3.5.7 G1收集器

  • 前沿成果之一,特色並行與併發、分代收集、空間整合、可預測停頓。但還不夠成熟,暫時還未大規模使用
  • 將整個java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。
  • 運做大體分爲如下步驟
  1. 初始標記:單線程,"Stop The World",速度很快,標記GC Roots能直接關聯到的對象,修改TAMS(Next Top at Mark Start)的值,讓下一階段程序併發運行時,能在正確可用的Region中建立新對象
  2. 併發標記:單線程,可與其它線程並行,耗時較長,
  3. 最終標記:多線程,"Stop The World",修正併發標記期間因爲程序繼續運行所致使的標記變更。
  4. 篩選回收:首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃

3.5.8 理解GC日誌

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs] 100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]css

最前面的數字"33.125:"和"100.667:"表明了GC發生的時間html

具體狀況具體分析吧,這裏不作太多細節描述前端

3.5.9 垃圾收集器參數總結

參數java

描述c++

UseSerialGC程序員

虛擬機運行在Client模式下的默認值,打開此開關後,使用Serial + Serial Old的收集器組合進行內存回收web

UseParNewGC算法

打開此開關後,使用ParNew + Serial Old的收集器組合進行內存回收shell

UseConcMarkSweepGC數據庫

打開此開關後,使用ParNew+ CMS + Serial Old的收集器組合進行內存回收。Serial Old收集器將做爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用

UseParallelGC

虛擬機運行在Server模式下的默認值,打開此開關後,使用Parallel Scavenge + Serial Old (PS Mark Sweep)的收集器組合進行內存回收

UserParallelOldGC

打開此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行內存回收

SurvivorRatio

新生代中Eden區域與Survivor區域的容量比值,默認爲8,表明Eden: Survivor = 8:1

PretenureSizeThreshold

直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配

MaxTenuringThreshold

晉升到老年代的對象年齡。每一個對象在堅持過一次Minor GC以後,年齡就增長1,當超過這個參數值時就進入老年代

UseAdaptiveSizePolicy

動態調整Java堆中各個區域的大小以及進入老年代的年齡

HandlePromotionFailure

是否容許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的全部對象都存活的極端狀況

ParallelGCThreads

設置並行GC時進行內存回收的線程數

GCTimeRatio

GC時間佔總時間的比率,默認值是99, 即容許1%的GC時間。僅在使用Parallel Scavenge收集器時生效

MaxGCPauseMillis

設置GC的最大停頓時間。僅在使用Parallel Scavenge收集器時生效

CMSInitiatingOccupancyFraction

設置CMS收集器在老年代時間被使用多少後觸發垃圾收集。默認值爲68%,僅在使用CMS收集器時生效

UseCMSCompactAtFullCollection

設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片整理。僅在使用CMS收集器時生效

CMSFullGCsBeforeCompaction

設置CMS收集器在進行若干次垃圾收集後再啓動一次內存碎片整理,僅在使用CMS收集器時生效

 

3.6 內存分配與回收策略

  • 對象主要分配在新生代的Eden區上,若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。
  • 少數狀況下也可能會直接分配在老年代中

3.6.1 對象優先在Eden分配

  • 大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次MinorGC
  • 新生代GC(Minor GC):發生在新生代的垃圾回收動做,這裏java對象大多都是朝生夕滅,因此Minor GC很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC / Full GC):發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC。Major GC的速度通常會比Minor GC慢10倍以上。

3.6.2 大對象直接進入老年代

  • 大對象是指須要大量連續內存空間的java對象
  • 參數控制:

    -XX:PretenureSizeThreshold:大於這個設置的對象直接進入老年區

3.6.3 長期存活的對象將進入老年代

  • 虛擬機給每一個對象定義了一個對象年齡(Age)計數器,每在Survivor區中"熬過"一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認15歲),就會被晉升到老年代中
  • 參數控制:

-XX:MaxTenuringThreshold:晉升老年代的年齡閾值。大於0的整數

3.6.4 動態對象年齡斷定

  • 虛擬機並非永遠地要求對象的年齡必須到達MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代

3.6.5 空間分配擔保

  • JDK 6 Update 24以前在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次Minor GC是有風險的;若是小於,或者HandlePromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC。
  • JDK 6 Update 24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC

3.7 本章小結

  • 沒有固定收集器、參數組合,也沒有最優的調優方法,虛擬機也就沒有什麼必然的內存回收行爲,根據實際應用需求、實現方式選擇最優的收集方式才能獲取最高的性能。

 

 

第4章 虛擬機性能監控與故障處理工具

4.1 概述

  • 經常使用的數據有:運行日誌、異常堆棧、GC日誌、線程快照(threaddump / javacore文件)、堆轉儲快照(heapdump / hprof文件)等

4.2 JDK的命令行工具

  • JDK的bin目錄下有不少小工具

名稱

主要做用

jps

jvm process status tool,顯示指定系統內全部的hotspot虛擬機進程

jstat   

jvm statistics monitoring tool,用於收集hotspot虛擬機各方面的運行數據

jinfo 

configuration info for java,顯示虛擬機配置信息

jmap  

memory map for java,生成虛擬機的內存轉儲快照(heapdump文件)

jhat 

jvm heap dump browser,用於分析heapmap文件,它會創建一個http/html服務器

讓用戶能夠在瀏覽器上查看分析結果

jstack  

stack trace for java ,顯示虛擬機的線程快照

4.2.1 jsp:虛擬機進程情況工具

  • 能夠列出正在運行的虛擬機進程,並顯示虛擬機執行主類名稱以及這些進程的本地虛擬機惟一ID。

屬性

做用

-q

只輸出LVMID,省略主類的名稱

-m

輸出虛擬機進程啓動時傳遞給主類main()函數的參數

-l

輸出主類的全名,若是進程執行的是jar包,輸出jar路徑

-v

輸出虛擬機進程啓動時jvm參數

 

4.2.2 jstat:虛擬機統計信息監視工具

  • jstat是用於監視虛擬機各類運行狀態信息的命令行工具。它能夠顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾回收、JIT編譯等運行數據,在沒有GUI圖形界面,只是提供了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具

選項

做用

-class

監視裝載類、卸載類、總空間以及類裝載所耗費的時間

-gc

監視java堆情況,包括eden區、兩個survivor區、老年代、永久代等的容量、已用空間、GC時間合計信息

-gccapacity

監視內容與-gc基本相同,但輸出主要關注java堆各個區域使用到最大、最小空間

-gcutil

監視內容與-gc基本相同,但輸出主要關注已使用控件佔總空間的百分比

-gccause

與-gcutil功能同樣,可是會額外輸出致使上一次gc產生的緣由

-gcnew

監視新生代GC狀況

-gcnewcapacity

監視內容與-gcnew基本相同,輸出主要關注使用到的最大、最小空間

-gcold

監視老年代GC狀況

-gcoldcapacity

監視內容與-gcold基本相同,輸出主要關注使用到的最大、最小空間

-gcpermcapacity

輸出永久代使用到的最大、最小空間

-compiler

輸出JIT編譯過的方法、耗時等信息

-printcompilation

輸出已經被JIT編譯過的方法

4.2.3 jinfo:java配置信息工具

  • jinfo的做用是實時的查看和調整虛擬機各項參數
  • jinfo格式 jinfo [option] pid

4.2.4 jmap:java內存映像工具

  • jmap命令用於生成堆轉儲快照。

選項

做用

-dump

生成java堆轉儲快照。格式爲: -dump:[live,]format=b,file=<filename>,其中live子參數說明是否只dump出存活的對象

-finalizerinfo

顯示在F-Queue中等待Finalizer線程執行finalize方法的對象。只在Linux/Solaris平臺下有效

-heap

顯示java堆詳細信息,如使用哪一種收集器、參數配置、分代狀況等,在Linux/Solaris平臺下有效

-jisto

顯示堆中對象統計信息,包含類、實例對象、合集容量

-permstat

以ClassLoader爲統計口徑顯示永久代內存狀態。只在Linux/Solaris平臺下有效

-F

當虛擬機進程對-dump選項沒有相應時。可以使用這個選項強制生成dump快照。只在Linux/Solaris平臺下有效

4.2.5 jhat:虛擬機堆轉儲快照分析工具

  • Sun JDK提供jhat與jmap搭配使用,來分析dump生成的堆快照。jhat內置了一個微型的HTTP/HTML服務器,生成dump文件的分析結果後,能夠在瀏覽器中查看。(沒什麼卵用,傻子才用瀏覽器看文件)

4.2.6 jstack:java堆棧跟蹤工具

  • jstack命令用於生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧集合,生成線程快照的主要目的是定位線程出現長時間停頓的緣由,如線程死鎖、死循環、請求外部資源致使長時間等待等。

選項

做用

-F

當正常輸出的請求不被響應時,強制輸出線程堆棧

-l

除堆棧外,顯示關於鎖的附加信息

-m

若是調用到本地方法的話,能夠顯示c/c++的堆棧

 

4.2.7 HSDIS:JIT生成代碼反彙編

  • 反編譯成彙編語言,方便查看細節

4.3 JDK的可視化工具

4.3.1 JConsole:java監視與管理控制檯

  • JConsole(Java Monitoring and Management Console)是一種基於JMX的可視化監視、管理工具。
  • JDK/bin目錄下的"jconsole.ext"
  • 主要功能:內存監控、線程監控(死鎖詳情查看)

4.3.2 VisualVM:多合一故障處理工具

多合一工具:運行監控、故障處理、性能分析

生成、瀏覽堆轉儲快照

分析程序性能

BTrace動態日誌追蹤:動態加入本來不存在的調試代碼

4.4 本章小結

 

 

第5章 調優案例分析與實戰

5.1 概述

5.2 案例分析

5.2.1 高性能硬件上的程序部署策略

  • 緣由:業務致使頻繁建立生命週期短的大量大對象,致使老年代被快速塞滿,GC頻繁
  • 初始解決方案:啓用內存更大的64位JDK,可是效果並不明顯,緣由是64位的JDK性能提高並不大。
  • 最終解決方案:使用若干個32位虛擬機創建邏輯集羣來利用硬件資源
  • 重點:在32位Windows平臺中每一個進程只能使用2GB的內存,考慮到堆之外的內存開銷,堆通常最多隻能開到1.5GB。在某些Linux或UNIX系統(如Solaris)中,能夠提高到3GB乃至接近4GB的內存,但32位中仍然受最高4GB(2^32)內存的限制。

5.2.2 集羣間同步致使的內存溢出

  • 緣由:使用了JBossCache做爲緩存,可是JBoss之間會頻繁通訊,若是通訊不通暢,發送的信息會保留在內存中,時間久了就會發生內存溢出
  • 解決方案:用Redis哇。這是個死循環,JBoss本身也無法沒徹底解決

5.2.3 堆外內存致使的溢出錯誤

  • 緣由:堆外內存(Direct Memory)並不在堆內存中被分配,但一樣受線程總空間和Full GC影響,當對外內存滿了,即便堆中仍然有內存,也沒法被使用和清除致使內存溢出
  • 解決方案:調節堆大小,或者把堆外內存改成其它方案
  • 重點:NIO操做須要使用到堆外內存,使用時須要注意

5.2.4 外部命令致使系統緩慢

  • 緣由:每一個用戶請求的處理都須要執行一個外部shell腳原本得到系統的一些信息。執行這個shell腳本是經過Java的Runtime.getRuntime().exec()方法來調用的。這種調用方式能夠達到目的,可是它在Java虛擬機中是很是消耗資源的操做,即便外部命令自己能很快執行完畢,頻繁調用時建立進程的開銷也很是大。
  • 解決方案:去掉這個Shell腳本執行的語句,改成使用Java的API去獲取這些信息
  • 重點:Java虛擬機執行這個命令的過程是:首先克隆一個和當前虛擬機擁有同樣環境變量的進程,再用這個新的進程去執行外部命令,最後再退出這個進程。若是頻繁執行這個操做,系統的消耗會很大,不只是CPU,內存負擔也很重。

5.2.5 服務器JVM進程崩潰

  • 緣由:對接其它系統,第三方系統接口響應時間太長,致使在等待的線程和Socket鏈接愈來愈多,超過虛擬機承受能力以後致使進程奔潰
  • 解決方案:優化接口,或者改成消息隊列

5.2.6 不恰當數據結構致使內存佔用過大

  • 緣由:業務須要頻繁加載數據,頻繁建立100多萬個HashMap<Long,Long>Entry,致使Minor GC頻繁,並且GC時間長達500毫秒
  • 解決方案:不修改代碼的前提,將Survivor空間去掉,讓新生代中存活的對象在第一次Minor GC後當即進入老年代,等到Major GC的時候再清理掉他們
  • 重點:慎用HashMap,分析一下空間效率。在HashMap<Long,Long>結構中,只有Key和Value所存放的兩個長整型數據是有效數據,共16B(2×8B)。這兩個長整型數據包裝成java.lang.Long對象以後,就分別具備8B的MarkWord、8B的Klass指針,在加8B存儲數據的long值。在這兩個Long對象組成Map.Entry以後,又多了16B的對象頭,而後一個8B的next字段和4B的int型的hash字段,爲了對齊,還必須添加4B的空白填充,最後還有HashMap中對這個Entry的8B的引用,這樣增長兩個長整型數字,實際耗費的內存爲(Long(24B)×2)+Entry(32B)+HashMapRef(8B)=88B,空間效率爲16B/88B=18%,實在過低了。

5.2.7 有Windows虛擬內存致使的長時間停頓

  • 緣由:Window GUI桌面程序最小化的時候,它的工做內存被自動交換到磁盤的頁面文件之中,若是期間發生GC,須要由於恢復頁面文件而致使GC長時間停頓
  • 解決方案:加入參數"Dsun.awt.keepWorkingSetOnMinimize=true"來解決。這個參數在許多AWT的程序上都有應用,例如JDK自帶的Visual VM,用於保證程序在恢復最小化時可以當即響應。

5.3 實戰:Eclipse運行速度調優

5.3.1 調優前的程序運行狀態

  • 寫eclipse插件能夠統計啓動狀態

5.3.2 升級JDK1.6的性能變化及兼容問題

  • JDK升級影響巨大,不要輕易升級
  • 最大老年代空間設置失效,手動設置永久代大小(-XX:MaxPermSize=256M)。緣由是JDK5與JDK6的公司不一樣,致使eclipse對JDK5的特殊處理無效

5.3.3 編譯時間和類加載時間的優化

  • 類加載時間:筆者發現,JDK在本身機器環境(並不具備廣泛性)的類加載速度沒有明顯提高,取消了字節碼驗證(-Xverify:none),稍微提高了一點點速度
  • 編譯時間:clint(單核)和server(多核)有不一樣的編譯器,效果也不一樣。
  • 理解Hotspot虛擬機的來由:JDK1.2之後,虛擬機內置了兩個運行時編譯器,若是一個java方法被調用次數達到必定程度,就被被斷定爲熱點代碼交給JIT編譯器即時編譯爲本地代碼,提升運行速度。

5.3.4 調整內存設置控制垃圾收集頻率

  • 老年代空間不足可是未達到最大設置值時,會致使Full GC,能夠直接設置老年代爲最大值避免擴容致使的GC(-:Xms = -:Xmx,-XX:PermSize = -XX:MaxPermSize)
  • 代碼中的System.gc()能夠用參數屏蔽(-XX:+DisableExplicitGC),可是慎用!

5.3.5 選擇收集器下降延遲

選擇青年代老年代垃圾收集器有不一樣的效果。好比開發環境操做頻繁,通常都是一邊編譯一邊工做,CMS最合適此場景(-XX:UseConcMarkSweepGC、-XX:+UseParNewGC)

5.4 本章小結

 

 

第三部分 虛擬機執行子系統

第6章 類文件結構

6.1 概述

  • 將咱們編寫的程序編譯成與操做系統和機器指令集無關的、平臺中立的格式做爲程序編譯後的存儲格式

6.2 無關性的基石

  • 各類不一樣平臺的虛擬機與全部平臺都統一使用的程序存儲格式--字節碼(ByteCode)是構成平臺無關性的基石
  • java語言中的各類變量、關鍵字和運算符號的語義最終都是由多條字節碼命令組合而成的,所以字節碼命令所能提供的語義描述能力比java語言自己更增強大

6.3 Class類文件的結構

  • 任何一個Class文件都對應着惟一一個類或接口的定義信息,但反過來講,類或接口並不必定都得定義在文件裏(譬如類或接口也能夠經過類加載器直接生成)
  • Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎所有是程序運行的必要數據,沒有空隙存在。
  • 根據java虛擬機規範的規定,Class文件格式採用一種相似於C語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表
  1. 無符號數:屬於基本的數據類型,以u一、u二、u四、u8來分別表明1個字節、2個字節、4個字節和8個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值
  2. 表:由多個無符號或者其餘表做爲數據項構成的複合數據類型,全部表都習慣性地以"_info"結尾。表用於描述有層次關係的複合結構數據,整個Class文件本質上就是一張表。(想象XML)

6.3.1 魔數與Class文件的版本

  • 每一個Class文件的頭4個字節稱爲魔數(Magic Number),它的惟一做用是肯定這個文件是否爲一個能被虛擬機接受的Class文件
  • 魔數值固定爲 0xCAFEBABE,緊接着魔數以後的4個字節爲Java版本信息:第5和第6個字節是次版本號(minor_version),第7和第8個字節是主版本號(major_version)

6.3.2 常量池

  • 緊接着主次版本號以後的是常量池入口,常量池能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其它項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一
  • 因爲常量池中常量的數量是不固定的,因此在常量池的入口須要放置一項u2類型的數據,表明常量池容量計數值,在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣作的目的在於知足後面某些指向常量池的索引值的數據在特定狀況下須要表達"不引用任何一個常量池項目"的含義,這種狀況就能夠把索引值置爲0來表示。根本緣由在於,索引爲0也是一個常量(保留常量),只不過它不位於常量表中。這個常量就對應Null值,因此常量池的索引從1而非0開始。
  • 常量池中主要存放兩大類常量:字面量和符號引用
  1. 字面量:比較接近於java語言層面的常量概念,如文本字符串、聲明爲final的常量值等。
  2. 符號引用:包含三類常量"類和接口的全限定名"、"字段的名稱和描述符"、"方法的名稱和描述符"
  • 當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析、翻譯到具體的內存地址之中

    常量池的項目類型

類型

標誌

描述 

CONSTANT_utf8_info

1 

UTF-8編碼的字符串

CONSTANT_Integer_info

3

整形字面量

CONSTANT_Float_info

4

浮點型字面量

CONSTANT_Long_info

長整型字面量

CONSTANT_Double_info

雙精度浮點型字面量

CONSTANT_Class_info

類或接口的符號引用

CONSTANT_String_info

字符串類型字面量

CONSTANT_Fieldref_info

字段的符號引用

CONSTANT_Methodref_info

10

類中方法的符號引用

CONSTANT_InterfaceMethodref_info

11

接口中方法的符號引用

CONSTANT_NameAndType_info

12

字段或方法的符號引用

CONSTANT_MothodType_info

16

標誌方法類型

CONSTANT_MethodHandle_info

15

表示方法句柄

CONSTANT_InvokeDynamic_info

18

表示一個動態方法調用點

常量池中的14種常量項的結構總表

6.3.3 訪問標誌

  • 在常量池結束以後,緊接着的兩個字節表明訪問標誌,這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類仍是接口;是否認義爲public類型;是否認義爲abstract類型;若是是類的話,是否被申明爲final等

6.3.4 類索引、父類索引與接口索引集合

  • 類索引和父類索引都是一個u2類型的數據,而接口索引集合是一組u2類型的數據的集合,Class文件中由這三項數據來肯定這個類的繼承關係

6.3.5 字段表集合

  • 字段表用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但不包括在方法內部申明的局部變量。
  • 結構包含有:訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)
  • 包含的信息有:字段的做用域(public、private、protected修飾符)、是實例變量仍是類變量(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主內存讀寫)、能否被序列化(transient修飾符)、字段數據類型(基本類型、對象、數組)、字段名稱
  • 注意:字段表集合中不會列出從超類或者父類接口中繼承而來的字段,但有可能列出本來java代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段了。

6.3.6 方法表集合

  • 結構與字段表相同,包含有:訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)
  • 方法裏的java代碼,通過編譯器編譯成字節碼指令後,存放在方法屬性表集合中一個名爲"Code"的屬性裏面
  • 若是父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息

6.3.7 屬性表集合

屬性名稱

使用位置

含義

Code

方法表

Java代碼編譯成的字節碼指令

ConstantValue

字段表

final關鍵字定義的常量值

Deprecated

類、方法表、字段表

被聲明爲deprecated的方法和字段

Exceptions

方法表

方法拋出的異常

EnclosingMethod

類文件

僅當一個類爲局部類或者匿名類時才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法

InnerClasses

類文件

內部類列表

LineNumberTable

Code屬性

Java源碼的行號與字節碼指令的對用關係

LocalVariableTable

Code屬性

方法的局部變量描述

StackMapTable

Code屬性

JDK1.6中新增的屬性,供新的類型檢查驗證器(Type Checker)檢查和處理目標方法的局部變量和操做數棧所須要的類型是否匹配

Signature

類、方法表、字段表

JDK1.5中新增的屬性,這個屬性用於支持泛型狀況下的方法簽名,在Java語言中,任何類、接口、初始化方法或成員的泛型簽名若是包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲他記錄泛型簽名信息。因爲Java的泛型採用擦除法實現,在爲了不類型信息被擦出後致使簽名混亂,須要這個屬性記錄泛型中的相關信息

SourceFile

類文件

記錄源文件名稱

SourceDebugExtension

類文件

JDK 1.6中新增的屬性,SourceDebugExtension屬性用於存儲額外的調試信息,譬如在進行JSP文件調試時,沒法同構Java堆棧來定位到JSP文件的行號,JSR-45規範爲這些非Java語言編寫,卻須要編譯成字節碼並運行在Java虛擬機中的程序提供了一個進行調試的標準機制,使用SourceDebugExtension屬性就能夠用於存儲這個標準所新加入的調試信息

Synthetic

類、方法表、字段表

標識方法或字段爲編譯器自動生成的

LocalVariableTypeTable

JDK 1.5中新增的屬性,他使用特徵簽名代替描述符,是爲了引入泛型語法以後能描述泛型參數化類型而添加

RuntimeVisibleAnnotations

類、方法表、字段表

JDK 1.5中新增的屬性,爲動態註解提供支持。RuntimeVisibleAnnotations屬性用於指明哪些註解是運行時(實際上運行時就是進行反射調用)可見的

RuntimeInVisibleAnnotations

類、方法表、字段表

JDK 1.5新增的屬性,與RuntimeVisibleAnnotations屬性做用恰好相反,用於指明哪些註解是運行時不可見的

RuntimeVisibleParameter

Annotations

方法表

JDK 1.5新增的屬性,做用與RuntimeVisibleAnnotations屬性相似,只不過做用對象爲方法參數

RuntimeInVisibleAnnotations

Annotations

方法表

JDK 1.5中新增的屬性,做用與RuntimeInVisibleAnnotations屬性相似,只不過做用對象爲方法參數

AnnotationDefault

方法表

JDK 1.5中新增的屬性,用於記錄註解類元素的默認值

BootstrapMethods

類文件

JDK 1.7中新增的屬性,用於保存invokedynamic指令引用的引導方法限定符

6.4 字節碼指令簡介

  • java虛擬機的指令由一個字節長度的、表明某種特定操做含義的數字(稱爲操做碼,Opcode)以及跟隨其後的零至多個表明此操做所需參數(稱爲操做數,Operands)而構成

6.4.1 字節碼與數據類型

  • 對於大部分與數據類型相關的字節碼指令,他們的操做碼助記符中都有特殊的字符來代表專門爲哪一種數據類型服務:i表明對int類型的數據操做,l表明Long,s表明short,b表明byte,c表明char,f表明float,d表明double,a表明reference。
  • 大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器在編譯期或運行期將byte和short類型數據帶符號擴展爲相應的int類型數據,將boolean和char類型數據零位擴展爲相應的int類型數據。

6.4.2 加載和存儲指令

  • 將一個局部變量加載到操做數棧的指令包括:iload,iload_<n>,lload、lload_<n>、float、 fload_<n>、dload、dload_<n>,aload、aload_<n>。
  • 將一個數值從操做數棧存儲到局部變量表的指令:istore,istore_<n>,lstore,lstore_<n>,fstore,fstore_<n>,dstore,dstore_<n>,astore,astore_<n>
  • 將常量加載到操做數棧的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
  • 局部變量表的訪問索引指令:wide

6.4.3 運算指令

  •   1)加法指令:iadd,ladd,fadd,dadd
  •   2)減法指令:isub,lsub,fsub,dsub
  •   3)乘法指令:imul,lmul,fmul,dmul
  •  4)除法指令:idiv,ldiv,fdiv,ddiv
  •   5)求餘指令:irem,lrem,frem,drem
  •  6)取反指令:ineg,leng,fneg,dneg
  •   7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr
  •  8)按位或指令:ior,lor
  •   9)按位與指令:iand,land
  •  10)按位異或指令:ixor,lxor
  • 11)局部變量自增指令:iinc
  • 12)比較指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
  • 當一個操做產生溢出時,將會使用有符號的無窮大來表示,若是某個操做結果沒有明確的數學定義的話,將會使用NaN值來表示。全部使用NaN值做爲操做數的算術操做,結果都會返回NaN

6.4.4 類型轉換指令

  • 1)int類型到long,float,double類型
  • 2)long類型到float,double類型
  • 3)float到double類型
  • 將int 或 long 窄化爲整型T的時候,僅僅簡單的把除了低位的N個字節之外的內容丟棄,N是T的長度。這有可能致使轉換結果與輸入值有不一樣的正負號。

6.4.5 對象建立與訪問指令

  • 1)建立實例的指令:new
  • 2)建立數組的指令:newarray,anewarray,multianewarray
  • 3)訪問字段指令:getfield,putfield,getstatic,putstatic
  • 4)把數組元素加載到操做數棧指令:baload,caload,saload,iaload,laload,faload,daload,aaload
  • 5)將操做數棧的數值存儲到數組元素中執行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
  • 6)取數組長度指令:arraylength JVM支持方法級同步和方法內部一段指令序列同步,這兩種都是經過moniter實現的。
  • 7)檢查實例類型指令:instanceof,checkcast

6.4.7 控制轉移指令

  • 1)條件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等
  • 2)複合條件分支:tableswitch,lookupswitch
  • 3)無條件分支:goto,goto_w,jsr,jsr_w,ret

6.4.8 方法調用和返回指令

  • invokevirtual指令:調用對象的實例方法,根據對象的實際類型進行分派(虛擬機分派)。
  • invokeinterface指令:調用接口方法,在運行時搜索一個實現這個接口方法的對象,找出合適的方法進行調用。
  • invokespecial:調用須要特殊處理的實例方法,包括實例初始化方法,私有方法和父類方法
  • invokestatic:調用類方法(static)

6.4.9 異常處理指令

6.4.10 同步指令

  • java虛擬機能夠支持方法級的同步和方法內部一段指令序列的同步
  • 方法級的同步是隱式的,即無需經過字節碼指令來控制。當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設置,若是設置了,執行線程就要求先成功持有管程,而後才能執行方法,最後當方法完成(不管是正常完成仍是非正常完成)是釋放管程。在方法執行期間,執行線程持有了管程,其它任何線程都沒法再獲取到同一個管程。若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理此異常,那麼這個同步方法所持有的管程將在異常拋到同步方法以外時自動釋放。
  • 同一段指令集序列一般由java語言中的synchronized語句塊來表示,java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義

6.5 公有設計和私有實現

6.6 Class文件結構的發展

6.7 本章小結

 

第7章 虛擬機類加載機制

7.1 概述

  • 虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型,這就是虛擬機的類加載機制

7.2 類加載的時機

  • 類從被加載到虛擬機內存中開始,到卸載出內存爲止,他的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準備、解析3個部分統稱爲鏈接(Linking)
  • 加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,而解析階段則不必定:它某些狀況下能夠在初始化階段以後纔開始,這是爲了支持java語言的運行時綁定。
  • 當一個類在初始化時,要求其父類所有都已經初始化過了,可是一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化
  • 有且只有5種主動引用的方式會觸發類的初始化
  1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化
  3. 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類
  5. 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic方法的句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化
  • 被動引用不會觸發初始化,如下是3個例子
  1. 經過子類引用父類的靜態字段,不會致使子類的初始化
  2. 經過數組定義來引用類,不會觸發此類的初始化
  3. 常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義常量的類,所以不會觸發定義常量類的初始化

7.3 類加載的過程

7.3.1 加載

  • 在加載階段,虛擬機須要完成如下3件事情
  1. 經過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口
  • 加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而後在內存中實例化一個java.lang.Class類的對象

7.3.2 驗證

  • 驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全
  • 驗證大體上會完成下面4個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
  • 文件格式驗證:第一階段要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理

        一、 是否已魔數CAFEBABE開頭;

        二、 主次版本號是否在當前虛擬機處理範圍以內;

        三、 常量池中的常量是否有不被支持的類型(使用tag標識校驗);

        四、 指向常量的索引是否有不存在或者支持的類型;

        五、 字符類型是否符合規範;

        六、 class文件是否被修改過;

        ...... 

  • 元數據驗證:第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合java語言規範的要求

    一、 這個類是否有父類(除java.lang.Object類以外都有父類);

        二、 這個類的父類是否繼承了不容許被繼承的類(final修飾);

        三、 若是這個類不是抽象類,是否實現了其父類或者接口中要求實現的全部方法;

        四、 類中的字段、方法是否跟父類中的字段、方法衝突;

        ......

  • 字節碼驗證:第三階段是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的

一、 變量要在使用以前進行初始化;

    二、 方法調用與對象引用類型要匹配;

    三、 數據和方法的訪問要符合權限設置規則;

    四、 對本地變量的訪問都落在運行時本地方法棧內;

    五、 運行時堆棧沒有溢出;

    ......

  • 符號引用驗證:最後一個階段的校驗發生在虛擬機將符號引用轉爲直接引用的時候,這個轉化動做將在鏈接的第三階段--解析階段中發生

     一、 經過全限定名可否找到對應的類;

        二、 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段;

        三、 符號引用中的類、方法、字段的訪問性(private、protected、public)是否能夠被當前類訪問到;

        ......

7.3.3 準備

  • 準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
  • 這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在java堆中。
  • 這裏所說的初始值"一般狀況"下時數據類型的零值

7.3.4 解析

  • 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程
  • 靜態方法、私有方法、實例構造器、父類方法稱爲非虛方法。其它方法稱爲虛方法(除去final方法)
  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號能夠是任何的字面量,只要使用時能無歧義地定位到目標便可。(好比Class文件是一組以8位字節爲基礎單位的二進制流)。
  • 直接引用(Direct References):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。
  • 類或接口的解析

     假設當前代碼所處的類爲D,若是要把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,那虛擬機完成整個解析的過程須要如下3個步驟:

  1. 若是C不是一個數組類型,那虛擬機將會把表明N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程當中,因爲元數據驗證、字節碼驗證的須要,又可能觸發其餘相關類的加載動做,例如加載這個類的父類或實現的接口。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。
  2. 若是C是一個數組類型,而且數組的元素類型爲對象,也就是N的描述符會是相似"[Ljava/lang/Integer"的形式,那將會按照第1點的規則加載數組元素類型。若是N的描述符如前面所假設的形式,須要加載的元素類型就是"java.lang.Integer",接着由虛擬機生成一個表明此數組維度和元素的數組對象。
  3. 若是上面的步驟沒有出現任何異常,那麼C在虛擬機中實際上已經成爲一個有效的類或接口了,但在解析完成以前還要進行符號引用驗證,確認D是否具有對C的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError異常。
  • 字段解析

要解析一個未被解析過的字段符號引用,首先將會對字段表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是字段所屬的類或接口的符號引用。若是在解析這個類或接口符號引用的過程當中出現了任何異常,都會致使字段符號引用解析的失敗。若是解析成功完成,那將這個字段所屬的類或接口用C表示,虛擬機規範要求按照以下步驟對C進行後續字段的搜索。

  1. 若是C自己就包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  2. 不然,若是在C中實現了接口,將會按照繼承關係從下往上遞歸搜索各個接口和他的父接口,若是接口中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段的直接引用,查找結束。
  3. 不然,若是C不是java.lang.Object的話,將會按照繼承關係從下往上遞歸搜索其父類,若是在父類中包含了簡單名稱和字段描述符都與目標相匹配的字段,則返回這個字段直接引用,查找失敗。
  4. 不然,查找失敗,拋出java.lang.NoSuchFieldError異常。

若是查找過程成功返回了引用,將會對這個字段進行權限驗證,若是發現不具有對字段的訪問權限,將拋出java.lang.IllegalAccessError異常。

  • 類方法解析

    類方法解析的第一個步驟與字段解析同樣,也須要先解析出類方法表的class_index項中索引的方法所屬的類或接口的符號引用,若是解析成功,咱們依然用C表示這個類,接下來虛擬機將會按照以下步驟進行後續的類方法搜索。

  1. 類方法和接口方法符號引用的常量類型定義是分開的,若是在類方法表中發現class_index中索引的C是個接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
  2. 若是經過了第1步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
  3. 不然,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
  4. 不然,在類C實現的接口列表及他們的父接口之中遞歸查找是否有簡單名稱和描述符都與目標相匹配的方法,若是存在匹配的方法,說明類C是一個抽象,這時查找結束,拋出java.lang.AbstractMethodError異常。
  5. 不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError。

最後,若是查找過程成功返回了直接引用,將會對這個方法進行權限驗證,若是發現不具有對此方法的訪問權限,將拋出java.lang.IllegalAccessError異常。

  • 接口方法解析

    接口方法也須要先解析出接口方法表的class_index項中索引的方法所屬的類或接口的符號引用,若是解析成功,依然用C表示這個接口,接下來虛擬機將會按照以下步驟進行後續的接口方法搜索。

  1. 與類方法解析不一樣,若是在接口方法表中發現class_index中的索引C是個類而不是接口,那就直接拋出java.lang.IncompatibleClassChangeError異常。
  2. 不然,在接口C中查找是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
  3. 不然,在接口C的父接口中遞歸查找,直到java.lang.Object(查找範圍會包括Object類)爲止,看是否有簡單名稱和描述符都與目標相匹配的方法,若是有則返回這個方法的直接引用,查找結束。
  4. 不然,宣告方法查找失敗,拋出java.lang.NoSuchMethodError異常。

因爲接口中的全部方法默認都是public,因此不存在訪問權限的問題,所以接口方法的符號解析應當不會拋出java.lang.IllegalAccessError異常。

7.3.5 初始化

  • 在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。
  • 從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程,<clinit>()方法執行的細節以下:
  1. <clinit>()方法是由編譯器自動收集類中全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句能夠賦值,可是不能訪問
  2. <clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢,因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做
  3. <clinit>()方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。
  4. 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()方法。但與類不一樣的是,接口的<clinit>()方法不須要先執行父接口的<clinit>()方法
  5. 虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其它線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢

7.4 類加載器

  • 虛擬機設計團隊把類加載階段中的"經過一個類的全限定名來獲取描述此類的二進制字節流"這個動做放到java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所須要的類。實現這個動做的代碼稱爲"類加載器"

7.4.1 類與類加載器

  • 對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在java虛擬機中的惟一性,每個類加載器,都有一個獨立的類名稱空間。(比較兩個類是否"相等",須要來源於同一個Class文件,被同一個類加載器加載)

7.4.2 雙親委派模型

  • 從java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstrap ClassLoader),是虛擬機的一部分;另外一種就是全部其它的類加載器,這些類加載器都由java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader。
  • 啓動類加載器(Bootstrap ClassLoader):負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被java程序直接引用
  • 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器
  • 應用程序類加載器(Application ClassLoader):這個加載器由sun.misc.Launcher$App-ClassLoader實現。通常稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器
  • 雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼
  • 雙親委派模型的工做過程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,由於全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。

7.4.3 破壞雙親委派模型

  • 雙親委派被破壞的3種狀況
  • 向前兼容:妥協JDK1.2版本前的代碼,引入loadClass()方法引導用戶去重寫
  • JNDI服務:啓動類加載器沒法識別JNDI接口,只好引入"線程上下文加載器",父類加載器請求子類加載器去完成類加載動做
  • 程序動態性(代碼熱替換、模塊熱部署):OSGI等

    7.5 本章小結

 

第8章 虛擬機字節碼執行引擎

8.1 概述

8.2 運行時棧幀結構

  • 棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址和一些額外的附加信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程

8.2.1 局部變量表

  • 局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在java程序編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中肯定了該方法所須要分配的局部變量表的最大容量。
  • 局部變量表的容量以變量槽(Variable Slot)爲最小單位,在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程
  • 爲了儘量節省棧幀空間,局部變量表中的Slot是能夠重用的。局部變量表中的Slot存有對象引用時,GC不會回收此對象。不過通常JIT編譯會自動優化掉賦Null值的操做,無須手動賦值。
  • 局部變量不像前面介紹的類變量那樣存在"準備階段"。若是一個局部變量定義了但沒有賦初始值是不能使用的

8.2.2 操做數棧

  • 操做數棧(Operand Stack)也常稱爲操做棧,它是一個後進先出(Last In First Out,LIFO)棧。在java程序編譯爲Class文件時,就在方法的Code屬性的max_stacks數據項中肯定了操做數棧的最大深度。
  • 當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做。
  • 在概念模型中,兩個棧幀做爲虛擬機棧的元素,是徹底相互獨立的。但大多虛擬機的實現裏都會作一些優化處理,令兩個棧幀出現一部分重疊。

8.2.3 動態鏈接

  • 每一個棧幀都包含一個指向運行時常量池中該幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)
  • Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用 ,這種轉化稱爲靜態解析。另一部分將在每一次運行期間轉化爲直接引用,這部分稱爲動態鏈接

8.2.4 方法返回地址

  • 當一個方法開始執行以後,只有兩種方式能夠退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion ) 。
  • 另外一種退出方法是,在方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理,這種退出方式稱爲異常完成出口( Abrupt Method Invocation Completion),它不會給上層調用者產生任何返回指。

8.2.5 附加信息

  • 虛擬機規範容許具體的虛擬機實現增長一些規範裏沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息徹底取決於具體的虛擬機實現。在實際開發中 ,通常會把動態鏈接、方法返回地址與其餘附加信息所有歸爲一類,稱爲棧幀信息。

8.3 方法調用

  • 方法調用並不等同於方法執行,方法調用階段惟一的任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程

8.3.1 解析

  • 在類加載的解析階段,會將其中的一部分符號引用轉化爲直接引用,這種解析能成立的前提是:"編譯器可知,運行期不可變",主要包括靜態方法和私有方法兩大類。它們都不可能經過繼承或別的方式重寫其它版本
  • 在java虛擬機裏面提供了5條方法調用字節碼指令,以下
  1. invokestatic:調用靜態方法
  2. invokespecial:調用實例構造器<init>方法、私有方法和父類方法
  3. invokevirtual:調用全部虛方法(靜態方法、私有方法、實例構造器、父類方法稱爲非虛方法。其它方法稱爲虛方法(實例方法等,除去final方法,)
  4. invokeinterface:調用接口方法,會在運行時再肯定一個實現此接口的對象
  5. invokedynamic:如今運行時動態解析出調用點限定符所引用的方法,而後再執行該方法,在此以前的4條調用指令,分派邏輯是固話在java虛擬機內部的,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

8.3.2 分派

  • 解析與分派這兩種不是二選一的排它關係,它們是在不一樣層次上去篩選、肯定目標方法的過程
  • 多態的基本體現"重載"和"重寫"在java虛擬機中是如何實現的
  1. 靜態分派
    1. 靜態類型:接口或抽象類,實際類型:普通類
    2. 靜態類型的變化僅僅在使用時發生,變量自己的靜態類型不會被改變,而且最終的靜態類型在編譯器可知。而實際類型變化的結果在運行期纔可肯定
    3. 編譯器在重載時是經過參數的靜態類型而不是實際類型做爲判斷依據
    4. 靜態分派發生在編譯階段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的
    5. 靜態方法會在類加載期就進行解析,而靜態方法也能夠擁有重載版本,選擇重載版本的過程也是經過靜態分派完成的
  2. 動態分派
  • 最終執行方法的字節碼指令是invokevirtual,它的解析過程分爲如下步驟
  1. 找到操做數棧頂的第一個元素所指向的對象的實際類型,記做C
  2. 若是在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束;若是不經過,則返回java.lang.ILLegalAeecssError異常
  3. 不然,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
  4. 若是始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常
  • 因爲invokevirtual指令的第一步就是在運行期肯定接收者的實例類型,因此兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不一樣的直接引用上,這個過程就是java語言中方法重寫的本質
  1. 單分派與多分派
  • 靜態分派屬於多分派類型
  • 動態分派屬於單分派類型
  1. 虛擬機動態分派的實現
  • 最經常使用的"穩定優化"手段就是爲類在方法區中創建一個虛方法表(),使用虛方發表索引來代替元數據查找以提升性能
  • 虛方法表中存放着各個方法的實際入口地址。若是某個方法在子類中沒有被重寫,那麼子類的虛方法表裏面的地址入口和父類相同方法的地址入口是一直的,都指向父類的實現入口。若是子類中重寫了這個方法,子類方法表中的地址將會替換爲指向子類實現版本的入口地址
  • 方發表中全部從Object繼承來的方法都指向了Object的數據類型
  • 方發表通常在類加載的連接階段進行初始化,準備了類的變量初始值後,虛擬機會把該類的方法表也初始化完畢

8.3.3 動態類型語言支持

  • 動態類型語言:動態類型語言的關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期
  • JDK1.7與動態類型:invokedynamic指令與java.lang.invoke包
  • java.lang.invoke包:提供了一種新的動態肯定目標方法的機制,稱爲MethodHandle:相似lamda的Function,用代碼模擬了invokevirtual這幾種指令的過程。使其能動態的執行。與反射Reflection的區別:

    一、都是在模擬方法調用,Reflection是模擬java代碼層次的方法調用,MethodHandle模擬字節碼層次的方法調用;

    二、Reflection是重量級,是方法在java一端的全面映像,MethodHandle是輕量級,僅僅包含與執行該方法相關的信息;

    三、理論上虛擬機能夠對字節碼指令的優化(如方法內聯)可應用在MethodHandle上,但目前不夠完善;

    四、Reflection設計目標只爲java語言服務,MethodHandle服務一全部java虛擬機上的語言

  • invokedynamic指令:

每一次含有invokedynamic指令的位置都稱做"動態調用點"(Dynamic CallSite),這條指令的第一個參數變爲CONSTANT_InvokeDynamic_info常量,從中可獲取引導方法(Bootstrap Method)、方法類型(MethType)和名稱。根據它提供的信息,虛擬機能夠找到並執行引導方法,從而獲取一個CallSite對象,最終調用要執行的目標方法

  • 掌握方法分派規則:invokedynamic與前面4條"invoke*"指令最大差異就是它的分派邏輯不是由虛擬機決定的,而是由程序員決定

8.4 基於棧的字節碼解釋執行引擎

  • java虛擬機的執行引擎分爲解釋執行(經過解釋權執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種,本章只看解釋執行

8.4.1 解釋執行

  • 解釋器:抽象語法樹 --> 指令流 --> 解釋器 --> 解釋執行。(上圖中間分支)
  • javac編譯器完成了程序代碼通過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性字節碼指令流的過程(上圖下面分支)

8.4.2 基於棧的指令集與基於寄存器的指令集

  • java編譯器輸出的指令流,基本上是一種基於棧的指令集架構,指令流中的指令大部分都是零地址指令
  • 基於棧的指令集優勢是可移植、代碼相對更加緊湊、編譯器實現更加簡單。缺點是寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地受到硬件的約束。執行速度相對較慢

8.4.3 基於棧的解釋器執行過程

8.5 本章小結

 

第9章 類加載及執行子系統的案例與實戰

9.1 概述

9.2 案例分析

9.2.1 Tomcat:正統的類加載器架構

  • 在tomcat目錄結構中,有三組目錄/common/*,/server/*,/shared/*能夠用來存放類庫,另外還有Web應用程序自身的目錄/WEB-INF/*一共四組,把Java類庫放在這些目錄的含義和區別以下:
  • 放置在/common/*目錄:這些類庫能夠被tomcat和全部的web應用程序共同使用
  • 放置在/server/*目錄:這些類庫能夠被tomcat使用,全部的web應用程序都不可見
  • 放置在/shared/*目錄:這些類庫能夠被全部的web應用程序共同使用,可是對tomcat本身不可見
  • 放置在/WEB-INF/*目錄:這些類庫僅僅對當前的web應用程序使用,對tomcat和其餘的web應用程序都是不可見的
  • 主要的類加載器有CommonClassLoader,CatalinaClassLoader,SharedClassLoader和WebappClassLoader,它們分別加載/common/*,/server/*,/shared/*和/WEB-INF/*目錄下的類庫,其中WebApp加載器和JSP類加載器實例一般會存在多個
  • 每個web應用程序對應一個webapp類加載器,每個JSP文件對應一個JSP類加載器.
  • CommonClassLoader能加載的類均可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader本身能加載的類則與對方相互隔離.WebAppClassLoader能夠使用SharedClassLoader
  • Tomcat6.x把/common/*,/server/*,/shared/*三個目錄默認合併到一塊兒變成一個/lib目錄

9.2.2 OSGi:靈活的類加載器架構

  • OSGi中的每一個模塊(Bundle)與普通java類庫區別並不大,二者都是以jar格式封裝,而且內部存儲的都是java package和Class。可是Bundle能夠申明它所依賴的java package,也能夠申明它容許導出發佈的java package
  • 在OSGi裏面,Bundle之間的依賴關係從傳統的上層模塊依賴底層模塊轉變爲平級模塊之間的依賴。Bundle類加載器之間只有規則,沒有固定的委派關係
  • 在OSGi裏,加載器之間的關係再也不是雙親委派模型的樹形結構,而是已經進一步發展成了一種更爲複雜、運行時才能肯定的網狀結構

    1)將以java.*開頭的類委派給父類加載器加載。

    2)不然,將委派列表名單內的類委派給父類加載器加載。

    3)不然,將Import列表中的類委派給Export這個類的Bundle的類加載器加載。

    4)不然,查找當前Bundle的Class Path,使用本身的類加載器加載。

    5)不然,查找類是否在本身的Fragment Bundle中,若是在,則委派給Fragment Bundle的類加載器加載。

    6)不然,查找Dynamic Import列表的Bundle,委派給對應Bundle的類加載器加載。

    7)不然,類查找失敗。

9.2.3 字節碼生成技術與動態代理的實現

  • Jdk動態代理的原理,再也不細說,注意Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法就是生成一個相同接口的代理類Object。網上那些示例都過於複雜。

9.2.4 Retrotranslator:跨越JDK版本

  • Retrotranslator的做用是將JDK1.5編譯出來的Class文件轉變爲能夠在JDK1.4或1.3上部署的版本。

9.3 實戰:本身動手實現遠程執行功能

直接修改符合Class文件格式的byte[]數組中的常量池部分,將常量池中指定內容的CONSTANT_Utf8_info常量替換爲新的字符串(把系統的類替換爲本身寫的類),而後用本身的ClassLoader

9.3.1 目標

9.3.2 思路

9.3.3 實現

9.3.4 驗證

9.4 本章小結

 

第四部分 程序編譯與代碼優化

第10章 早期(編譯期)優化

10.1 概述

  • 前端編譯器:把*.java文件轉變爲*.class文件
  • JIT編譯器:把字節碼轉變爲機器碼
  • AOT編譯器:直接把*.java文件編譯成本地機器代碼

10.2 Javac編譯器

10.2.1 Javac的源碼與調試

  • 編譯過程大體可分爲3個過程
  1. 解析與填充符號表過程
  2. 插入式註解處理器的註解處理過程
  3. 分析與字節碼生成過程
  • 交互順序以下圖

  • 編譯過程的主題代碼

 

10.2.2 解析與填充符號表

解析步驟由圖10-5中的parseFiles()方法完成,包括了經典程序編譯原理中的詞法分析和語法分析兩個過程

  1. 詞法、語法分析
  • 詞法分析是將源代碼的字符流轉變爲標記(Token)集合,標記是編譯過程的最小元素
  • 語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每個節點都表明着程序代碼中的語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋等均可以是一個語法結構
  1. 填充符號表
  • 完成了語法分析和詞法分析以後,下一步就是填充符號表的過程,10-5中的enterTree()方法。符號表(Symbol Table)是由一組符號地址和符號信息構成的表格,相似K-V值對的形式
  • 符號表中所登記的信息在編譯的不一樣階段都要用到。在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

10.2.3 註解處理器

  • JDK1.6中實現了JSR-269規範,提供了一組插入式註解處理器的標誌API在編譯期間對註解進行處理,能夠把它看做是一組編譯期插件,在這些插件裏面,能夠讀取、修改、添加抽象語法樹中的任意元素。
  • 若是這些插件在處理註解期間對語法樹進行了修改,編譯期將回到解析及填充符號表的過程從新處理,直到全部插入式註解處理器都沒有再對語法樹進行修改成止,每一次循環稱爲一個Round,也就是10-4的迴環過程

10.2.3 語義分析與字節碼生成

  • 語法分析以後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,但沒法保證源程序是符合邏輯的。而語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查
  1. 標註檢查,10-5中的attribute()方法
  • 檢查的內容包括諸如變量使用前是否已被申明、變量與賦值之間的數據類型是否可以匹配等
  • 常量摺疊,將int a= 1 + 2優化爲int a = 3;
  1. 數據及控制流分析,10-5中的flow()方法
  • 對程序上下文邏輯更進一步的驗證,它能夠檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否全部的受查異常都被正確處理了問題
  1. 解語法糖
  • 語法糖,也稱糖衣語法,指在計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用
  • java中最經常使用的語法糖主要是泛型、變成參數、自動裝箱/拆箱等
  1. 字節碼生成
  • 字節碼生成階段不只僅把前面各個步驟所生成的信息(語法樹、符號表)轉化成字節碼寫到磁盤中,編譯器還進行了少許的代碼添加和轉換工做
  • 實例構造器<init>()方法和類構造器<clinit>()方法就是在這個階段添加到語法樹中
  • 保證必定先執行父類的實例構造器,而後初始化變量,最後執行語句塊
  • 除了生成構造器以外,還有其它的一些代碼替換工做用於優化程序的實現邏輯,如把字符串的加操做替換爲StringBuffer或StringBuilder的append()操做(我的認爲還有/*2優化,優化爲位運算)

10.3 Java語法糖味道

10.3.1 泛型與類型擦除

  • 它的本質是參數化類型的應用,也就是說所操做的數據類型被指定爲一個參數。這種參數類型能夠用在類、接口、方法的建立中,分別稱爲泛型類、泛型接口和泛型方法
  • java語言中的泛型只在程源碼中存在,在編譯後的字節碼文件中就已經替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,而且在相應的地方插入了強制轉型代碼
  • java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型
  • 泛型重載沒法編譯的緣由:List<String>和List<Integer>編譯以後都變成了原生類型List<E>
  • 方法重載要求方法具有不一樣的特徵簽名,返回值並不包含在方法的特徵簽名之中,因此返回值不參與重載選擇,可是在Class文件格式之中,只要描述符不是徹底一致的兩個方法就能夠共存。

10.3.2 自動裝箱、拆箱與遍歷循環

for(int i : list)會變爲 for(Iterator iter = list.iterator();iter.hasNext();){ int i = ((Integer)iter.next()).intValue() }

 

List<Integer> list = Arrays.asList(1,2,3,4);變爲 List list = Arrays.asList(new Integer[]{ Integer.valueof(1),Integer.valueof(2),Integer.valueof(3),Integer.valueof(4) })

10.3.3 條件編譯

  • java語言中條件編譯的實現,也是java語言的一顆語法糖,根據布爾常量值的真假,編譯器將會把分支中不成立的代碼塊消除掉,這一工做將在編譯器解除語法糖階段完成

if(true){ syso("bolck 1"); }else{ syso("bolck 2"); }

優化爲

syso("bolck 1");

10.4 實戰:插入式註解處理器

10.4.1 實戰目標

10.4.2 代碼實現

10.4.3 運行與測試

10.4.4 其它應用案例

10.5 本章小節

 

第11章 晚期(運行期)優化

11.1 概述

  • java程序經過解釋器(Interpreter)進行解釋執行
  • 當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定爲"熱點代碼"(Hot Spot Code)。爲了提升熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與平臺相關的機器碼,並進行各類層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler,即JIT編譯器)

11.2 HotSpot虛擬機內的即時編譯器

11.2.1 解釋器與編譯器

  • 當程序須要迅速啓動和執行的時候,解釋器能夠首先發揮做用,省去編譯的時間,當即執行
  • 在程序運行後,隨着時間的推移,編譯器逐漸發揮做用,把愈來愈多的代碼編譯成本地代碼以後,能夠獲取更高的執行效率
  • 當程序運行環境中內存資源限制較大(如部分嵌入式系統),能夠使用解釋執行節約內存,反之能夠使用編譯執行來提高效率
  • HotSpot虛擬機中內置了兩個即時編譯器,分別稱爲Client Compiler和Server Compiler,或者簡稱爲C1編譯器和C2編譯器。默認採用解釋器與期中一個編譯器直接配合的方式工做,程序使用哪一個編譯器,取決於虛擬機運行的模式,HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也能夠使用"-client"或"-server"參數去強制指定虛擬機運行在Client模式或Server模式
  • 解釋模式(Interpreted Mode):參數"-Xint",編譯器徹底不介入工做,所有代碼都使用解釋方式執行
  • 編譯模式(Compiled Mode):參數"-Xcomp",優先採用編譯方式執行程序,可是解釋器仍然要在編譯沒法進行的狀況下介入執行過程

HotSpot虛擬機採用分層(Tiered Compilation)的策略,分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不一樣的編譯層次

  1. 第0層,程序解釋執行,解釋器不開啓性能監控功能(Profiling),可觸發第1層編譯
  2. 第1層,也成爲C1編譯,將字節碼編譯爲本地代碼,進行簡單、可靠的優化,若有必要將加入性能監控的邏輯
  3. 第2層(或2層以上),也稱爲C2編譯,也是將字節碼編譯爲本地代碼,可是會啓用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化
  • 實施分層編譯後,Client Compiler和Server Compiler將會同時工做,許多代碼均可能會被屢次編譯,用Client Compiler獲取更高的編譯速度,用Server Compiler來獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集性能監控信息的任務

11.2.2 編譯對象與觸發條件

  • 在運行過程當中會被即時編譯器編譯的"熱點代碼"有兩類
  1. 被屢次調用的方法:由方法調用觸發的編譯,所以編譯器理所固然的會以整個方法做爲編譯對象,這種編譯也就是虛擬機中標準的JIT編譯方式
  2. 被屢次執行的循環體:編譯器依然會以整個方法(而不是單獨的循環體)做爲編譯對象。這種編譯方式由於編譯發生在方法執行過程之中,所以形象地稱之爲棧上替換(On Stack Replacement,簡稱爲OSR編譯,即方法棧幀還在棧上,方法就被替換了)
  • 目前主要的熱點探測斷定方式有兩種
  1. 基於採樣的熱點探測:虛擬機會週期性地檢查各個線程的棧頂,若是發現某個(或某些)方法常常出如今棧頂,那這個方法就是"熱點方法"。優勢:實現簡單、高效,很容易獲取方法調用關係(將堆棧展開便可),缺點:很難精確地確認一個方法的熱度,容易收到線程阻塞或其它外界因素的影響
  2. 基於計數器的熱點探測:虛擬機會爲每一個方法(甚至代碼塊)創建計數器,統計方法的執行次數,若是執行次數超過必定的閾值就認爲它是"熱點方法"。優勢:精確嚴謹,缺點:實現麻煩。
  • HotSpot虛擬機中使用的是--基於計數器的熱點探測方法,它爲每一個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)
  1. 方法調用計數器
    1. 當一個方法被調用時,會先檢查該方法是否存在被JIT編譯過的版本,若是存在,則優先使用編譯後的本地代碼來執行。若是不存在已被編譯過的版本,則將此方法的調用計數器值加1,而後判斷方法調用計數器與回邊計數器之和是否超過方法調用計數器的閾值。若是已超過閾值,那麼將會向即時編譯器提交一個該方法的代碼編譯請求
    2. 該計數器並非絕對次數,而是相對的執行次數,即在一段時間內的執行次數,當超過必定的時間限度,若仍是沒有達到閾值,那麼它的計數器會減小一半,此過程被稱爲熱度衰減。可以使用虛擬機參數"-XX:-UseCounterDecay"來關閉熱度衰減

  1. 回邊計數器
    1. 統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲"回邊"(Back Edge)。目的時爲了觸發OSR編譯
    2. 當解釋器遇到一條回邊指令時,會先查找將要執行的代碼片斷是否有已經編譯好的版本,若是有,它將會優先執行已編譯的代碼,不然就把回邊計數器的值加1,而後判斷方法調用計數器與回邊計數器值之和是否超過回邊計數器的閾值。當超過閾值的時候,將會提交一個OSR編譯請求,而且把回邊計數器的值下降一些,以便繼續在解釋器中執行循環,等待編譯器輸出編譯結果,
    3. 和方法計數器執行過程不一樣的是:當兩個計數器之和超過閾值的時候,它向編譯器提交OSR編譯,而且調整回邊計數器值,而後仍舊以解釋方式執行下去。
    4. 虛擬機運行在Client模式下,回邊計數器閾值計算公式爲:

方法調用計數器閾值(CompileThreshold)×OSR比率(OnStackReplacePercentage)/100

其中OnStackReplacePercentage默認值爲933,若是都取默認值,那Client模式虛擬機的回邊計數器的閾值爲13995。

  1. 虛擬機運行在Server模式下,回邊計數器閾值的計算公式爲:

方法調用計數器閾值(CompileThreshold)×(OSR比率(OnStackReplacePercentage)-解釋器監控比率(InterpreterProfilePercentage)/100

其中OnStackReplacePercentage默認值爲140,InterpreterProfilePercentage默認值爲33,若是都取默認值,那Server模式虛擬機回邊計數器的閾值爲10700。

11.2.3 編譯過程

  • 在默認設置下,不管是方法調用產生的即時編譯請求,仍是OSR編譯請求,虛擬機在代碼編譯器還未完成以前,都仍然按照解釋方式繼續執行,而編譯動做則在後臺的編譯線程中進行。可經過參數"-XX:-BackgroundCompilation"來禁止後臺編譯,禁止後,執行線程向虛擬機提交編譯請求後將會一直等待,直到編譯過程完成後再開始執行編譯器輸出的本地代碼
  • Client Compiler編譯器:簡單快速的三段式編譯器,主要關注點在於局部性的優化,放棄了許多耗時較長的全局優化手段

一階段:一個平臺獨立的前端將字節碼構形成一種高級中間代碼表示(HIR)。在此以前編譯器會在字節碼上完成一部分基礎優化,如方法內聯、常量傳播等。

二階段:一個平臺相關的後端從HIR中產生低級中間代碼表示(LIR),而在此以前會在HIR上完成另外一些優化,如空值檢查消除、範圍檢查消除等

最後階段:在平臺相關的後端使用線性掃描算法在LIR上分配寄存器,並在LIR上作窺孔優化,而後產生機器代碼。

  • Server Compiler是專門面向服務端的典型應用併爲服務端的性能配置特別調整過的編譯器,也是一個充分優化過的高級編譯器,它會執行全部經典的優化動做,如無用代碼消除(Dead Code Elimination)、循環展開(Loop Unrolling)、循環表達式外提(Loop Expression Hoisting)、消除公共子表達式(Common Subexpression Elimination)、常量傳播(Constant Propagation)、基本塊重排序(Basic Block Reordering)等,還會實施一些與Java語言特性密切相關的優化技術,如範圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination,不過並不是全部的空值檢查消除都是依賴編譯器優化的,有一些是在代碼運行過程當中自動優化了)等。另外,還可能根據解釋器或Client Compiler提供的性能監控信息,進行一些不穩定的激進優化,如守護內聯(Guarded Inlining)、分支頻率預測(Branch Frequency Prediction)等。
  • 以即時編譯的標準來看,Server Compiler無疑是比較緩慢的,但它的編譯速度依然遠遠超過傳統的靜態優化編譯器,並且它相對於Client Compiler編譯輸出的代碼質量有所提升,能夠減小本地代碼的執行時間,從而抵消了額外的編譯時間開銷,因此也有不少非服務端的應用選擇使用Server模式的虛擬機運行。

11.2.4 查看及分析即時編譯結果

參數-XX:+PrintCompilation要求虛擬機在即時編譯時將被編譯成本地代碼的方法名稱打印出來

參數-XX:+PrintInlining要求虛擬機輸出方法內聯信息

參數-XX:+PrintAssembly要求虛擬機打印編譯方法的彙編代碼(須要Debug或者FastDebug版的虛擬機才能直接支持)

參數-XX:+PrintOptoAssembly輸出僞彙編結果

11.3 編譯優化技術

11.3.1 優化技術概覽

11.3.2 公共子表表達式消除

  • 若是一個表達式E已經計算過了,而且從先前的計算到如今E中全部變量的值都沒發生變化,那麼E的此次出現就成爲了公共子表達式
  • 若是這種優化僅限於程序的基本快內,便稱爲局部公共子表達式消除,若是這種優化的範圍涵蓋了多個基本快,那就稱爲全局公共子表達式消除

11.3.3 數組邊界檢查消除

  • 若是數組下標是一個常量,在編譯器根據數據流分析來肯定數組的值,若是沒有越界,就無需判斷數組邊界
  • 若是編譯器只要經過數據流分析就能夠判斷循環變量的取值範圍永遠在有效區間內,那在整個循環中就能夠把數組的上下界檢查消除,能夠節省不少次判斷條件
  • 隱式異常處理,以下段代碼,當x極少爲空的時候,隱式異常優化是值得的,若是x常常爲空反而會更慢,HotSpot虛擬機會自動選擇最優方案

if(x != null){ return x.value; }else{ throw new NullPointException(); } 隱式異常優化後: try{ return x.value; }catch(segment_fault){ uncommon_trap(); }

11.3.4 方法內聯

  • 編譯器最重要的優化之一,除了消除方法調用成本以外,它更重要的意義是爲其它優化手段創建良好的基礎
  • 穩定的內聯:使用invokespecial指令調用的私有方法、實例構造器、父類方法以及使用invokestatic指令調用的靜態方法。除此4種,其它的java方法都須要在運行時進行方法接收者的多態選擇
  • 類型繼承關係分析(CHA):基於整個應用程序的類型分析技術,它用於確認在目前已加載的類中,某個接口是否有多於一種的實現,某個類是否存在子類、子類是否爲抽象類信息
  • 編譯器在進行內聯時,若是是非虛方法,就直接內聯,這時候內聯是有穩定前提保障的。
  • 若是遇到虛方法,則會向CHA查詢此方法在當前程序下是否有多個版本可供選擇,若是查詢結果只有一個版本,那也能夠進行內聯,不過這種內聯屬於激進優化,須要預留一個"逃生門",稱爲守護內聯
  • 若是想CHA查詢結果有多個版本可供選擇,則編譯器會進行最後一次努力,使用內聯緩存來完成方法內聯,它的原理是:在未發生方法調用以前,內聯緩存狀態爲空,當第一次調用發生後,緩存記錄下方法接收者的版本信息,而且每次進行方法調用時都比較接收者版本,若是之後進來的每次調用的方法接收者版本都是同樣的,那這個內聯還能夠一直用下去。若是發生了方法接收者不一致的狀況,說明程序真正使用了虛方法的多態特性,這是纔會取消內聯,查找虛方法表進行方法分派。

11.3.5 逃逸分析

  • 逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞到其它方法中,稱爲方法逃逸。甚至還有可能被外部線程訪問到,譬如賦值給類變量或能夠在其它線程中訪問的變量實例,稱爲線程逃逸。逃逸分析如今還不夠成熟
  • 若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何路徑訪問到這個對象,則可進行一些高效的優化
  • 棧上分配:對象默認分配在堆中,會增長GC的壓力,若是肯定一個對象不會逃逸出方法以外,則直接在棧內存上進行分配空間,大量對象會隨着方法的結束而自動銷燬,GC壓力會小不少,HotSpot虛擬機中暫時沒做此優化
  • 同步消除:若是逃逸分析能肯定一個變量不會逃逸出線程,沒法被其它線程訪問,那麼這個變量的讀寫確定不會有競爭,對這個變量實施的同步措施能夠消除掉
  • 標量替換:若是逃逸分析證實一個對象不會被外部訪問,而且這個對象能夠被拆散的話,那程序真正執行的時候將可能不建立這個對象,而改成直接建立它的若干個被這個方法使用到的成員變量來代替

-XX:+DoEscapeAnalysis 手動開啓逃逸分析,(大部分虛擬機默認不開啓)

-XX:+PrintEscapeAnalysis 查看分析結果

-XX:+EliminateAllocation 開啓標量替換

-XX:+EliminateLocks 開啓同步消除

-XX:+PrintEliminateAllocations查看標量的替換狀況

11.4 Java與C/C++的編譯器對比

11.5 本章小結

 

 

第五部分 高效併發

第12章 Java內存模型與線程

12.1 概述

 

12.2 硬件效率與一致性

  • 物理機的緩存來源:因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝
  • 物理機的緩存模型:在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(Main Memory)。當多個處理器的運算任務都涉及同一塊主內存區域時,須要各個處理器訪問緩存時都遵循一些協議

12.3 Java內存模型

  • 本章的"內存模型"一詞,能夠理解爲在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象

12.3.1 主內存與工做內存

  • java內存模型的主要目標是定義程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素,不包括線程私有的局部變量與方法參數)的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節
  • java內存模型規定了全部的變量都存儲在主內存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝(不必定是整個對象,可能只是對象中的某個字段),線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。

12.3.2 內存間交互操做

  • lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
  • unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其它線程鎖定。

12.3.3 對於volatile型變量的特殊規則

  • 保障此變量對全部線程的可見性,當一條線程修改了這個變量的值,新值對其餘線程來講是能夠當即得知的。實現方式爲每次使用以前都刷新volatile的值
  • 禁止指令排序優化,對於有volatile修飾的變量,賦值操做時字節碼多執行了一個"lock add1 $0x0,(%esp)"操做,這個操做至關於一個內存屏障(Memory Barrier或Memory Fence),指令排序時不能把後面的指令重排序到內存屏障以前的位置,它的做用是使得本CPU得Cache寫入了內存,該寫入動做還會形成別的CPU或者別得內核無效化其Cache,經過這個空操做,可讓前面volatile變量的修改對其它CPU當即可見。

12.3.4 對於long和double型變量的特殊規則

  • java虛擬機規範容許將沒有被volatile修飾的64位的數據類型(long和double)的讀寫操做劃分爲兩次32位的操做。可是實際開發中,各商用虛擬機都選擇把64位數據的讀寫操做做爲原子操做來對待

12.3.5 原子性、可見性和有序性

  • 原子性(Atomicity):由java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,基本數據類型的訪問讀寫也是具有原子性(long和double在絕大部分虛擬機上也是)。對於更大範圍的原子性應用場景,java提供了lock和unlock,synchronized關鍵字的字節碼指令monitorenter和monitorexit
  • 可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其它線程能當即得知這個修改。
  1. volatile變量,它的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。
  2. synchronized是因爲對一個變量執行unlock操做以前,必須先把此變量同步回主內存(執行store、write操做)來得到的。
  3. 被final修飾的字段在構造器中初始化完成,而且構造器沒有把"this"指針傳遞出去,那麼其它線程能夠看見final的值
  • 有序性(Ordering):若是在本線程內觀察,全部操做都是有序的,若是在一個線程中觀察另外一個線程,全部的操做都是無序的。
  1. 前半句指線程內表現爲串行的語義(Within-Thread As-If-Serial Semantics),普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中執行順序一致
  2. 後半句指"指令重排序"現象和"工做內存與主內存同步延遲"現象

12.3.6 先行發生原則

  • happen-before原則,是java內存模型中定義的兩項操做之間的偏序關係,若是說操做A先行發生於操做B,其實就是說在發生操做B以前,操做A產生的影響能被操做B觀察到,"影響"包括修改了內存中共享變量的值、發送了消息、調用了方法等。
  • 下面是java內存模型下一些"自然的"先行發送關係,無須任何同步器協助就已經存在,可在編碼中直接使用

    程序次序原則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。

    管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調是同一個鎖,而"後面"是指時間上的前後順序。

    volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的"後面"一樣是指時間上的前後順序。

    線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。

    線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。

    線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。

    對線終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。

    傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

12.4 Java與線程

12.4.1 線程的實現

java語言提供了在不一樣硬件和操做系統平臺下對線程操做的統一處理,每一個已經執行start()且還未結束的java.lang.Thread類的實例就表明了一個線程。

實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。

  • 內核線程實現
  1. 內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核(Kernel)支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器(scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。
  2. 程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口--輕量級進程(Light Weight Process,LWP),輕量級進程就是咱們一般意義上所講的線程。
  3. 因爲內核線程的支持,每一個輕量級線程都成爲一個獨立的調度單元,即便有一個輕量級進程在系統調用中阻塞了,也不會影響整個進程繼續工做。侷限性:因爲是基於內核線程實現,因此各類線程操做,如建立、析構及同步,都須要進行系統調用。而系統調用的代價相對較高,須要在用戶態(Use Mode)和內核態(Kernel Mode)中來回切換。其次,每一個輕量級進程都須要有一個內核線程的支持,所以輕量級進程要消耗必定的內核資源(如內核線程的棧空間),所以一個系統支持輕量級進程的數量是有限的。

  • 使用用戶線程實現
  1. 從廣義上來說,一個線程只要不是內核線程,就能夠認爲是用戶線程(User Thread,UT)。
  2. 從狹義上的用戶線程指的是徹底創建在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。這種線程不須要切換到內核態,所以操做很是快速且低消耗,也能夠支持更大的線程數量,部分高性能數據庫中的多線程就是由用戶線程實現的。
  3. 使用用戶線程的優點在於不須要系統內核支援,因此操做很是快速且低消耗,可是因爲沒有系統內核的支援,全部的線程操做都須要用戶程序本身處理,線程的建立、切換和調度都須要處理,而部分"阻塞如何處理""多處理器系統中若是將線程映射到其它處理器上"解決異常困難。除了在不支持多線程的操做系統中有極少數使用,如今大部分語言都放棄使用它。

  • 使用用戶線程加輕量級進程混合實現

    在這種混合實現下,即存在用戶線程,也存在輕量級進程。用戶線程仍是徹底創建在用戶空間中,所以用戶線程的建立、切換、析構等操做依然廉價,而且能夠支持大規模的用戶線程併發。而操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑,這樣能夠使用內核提供的線程調度功能及處理器映射,而且用戶線程的系統調用要經過輕量級進程來完成,大大下降了整個進程被徹底阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即爲N:M的關係。

  • Java線程的實現

    對於Sun JDK來講,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條java線程就映射到一條輕量級進程之中,由於Windows和Linux系統提供的線程模型就是一對一的。

     

12.4.2 Java線程調度

  • 線程調用是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling)
  • 協同式調度的多線程系統:線程的執行時間由線程自己來控制,線程把本身的工做執行完了以後,要主動通知系統切換到另一個線程上。
  1. 優勢:實現簡單,沒有線程同步問題。
  2. 缺點:線程執行時間不可控制,若是一個線程編寫有問題,會致使一直阻塞,至關不穩定甚至致使整個系統崩潰
  • 搶佔式調度的多線程系統:每一個線程由系統來分配執行時間,線程的切換不禁系統自己來決定(在java中,Thread.yield()可讓出執行時間,可是線程沒法獲取執行時間),java使用的線程調度方式是搶佔式調度。
  1. java使用的線程調度方式是搶佔式調度。
  • 線程的優先級只是相對的,並非絕對正確。緣由是java的線程是經過映射到系統的原生線程上來實現的,因此線程調度最終仍是取決於操做系統

12.4.3 狀態轉換

  • Java語言定義了5種線程狀態,在任意一個時間點,一個線程有且只有一種狀態
  • 新建(New):建立後還沒有啓動的線程處於這種狀態。
  • 運行(Runable):Runable包括了操做系統線程狀態種的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正等待着CPU爲它分配執行時間。
  • 無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,它們要等待被其它線程顯示地喚醒。如下方法會讓線程陷入無限期等待狀態:
  1. 沒有設置Timeout參數的Object.wait()方法。
  2. 沒有設置Timeout參數的Thread.join()方法。
  3. LockSupport.park()方法。
  • 限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其它線程顯式地喚醒,在必定時間以後它們會由系統自動喚醒。如下方法會讓線程進入限期等待狀態:
  1. Thread.sleep()方法。
  2. 設置了Timeout參數的Object.wait()方法。
  3. 設置了Timeout參數的Thread.join()方法。
  4. LockSupport.parkNanos()方法。
  5. LockSupport.parkUntil()方法。
  • 阻塞(Blocked):線程被阻塞了,"阻塞狀態"與"等待狀態"的區別是:"阻塞狀態"在等待着獲取到一個排他鎖,這個時間將在另一個線程放棄這個鎖的時候發生;而"等待狀態"則是在等待一段時間,或者喚醒動做的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
  • 結束(Terminated):已終止線程的線程狀態,線程已經結束執行。

12.5 本章小結

 

第13章 線程安全與鎖優化

13.1 概述

13.2 線程安全

13.2.1 Java語言中的線程安全

  • java語言中各類操做共享的數據分爲如下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立
  • 不可變:不可變(Immutable)的對象必定是線程安全的,不管是對象的方法實現仍是方法的調用者,都不須要再採起任何的線程安全保障措施
  1. 如final修飾的對象,Integer構造函數內部狀態變量value就是final類型,還有枚舉類型,Number的部分子類,Long和Double等數值包裝類型,BigInteger和BigDecimal等大數據類型
  2. String類的對象,調用它的substring(),replace()和concat()這些方法都不會影響它原來的值,會返回一個新構造的字符串對象
  • 絕對線程安全:無論運行時環境如何,調用者都不須要任何額外的同步措施
  1. Vector類不是絕對安全,雖然被官方標識爲線程安全,可是同一個對象方法調用間隙也會有線程問題

ead removeThread = new Thread(() -> { for (int i = 0; i < vector.size(); i++) {// 出錯的緣由在這兒,size()方法是同步的,remove方法也是同步的,//可是這兩部之間並不一樣步,數據有可能被其它線程修改,因此須要增長synchronized(對象) vector.remove(i); }

  • 相對線程安全:咱們一般意義上所講的線程安全,它須要保證對這個對象單獨的操做時線程安全的,咱們在調用的時候不須要作額外的保障措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保障調用的正確性
  1. 大部分線程安全類都屬於這種類型,Vector,HashTable,Collections的 synchronizedCollection()方法包裝的集合
  • 線程兼容:指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用
  • 線程對立:線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼。在java中極少見
  1. 一個線程對立的例子是Thread類的suspend()和resume()方法

13.2.2 線程安全的實現方法

互斥同步:同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式

  1. synchronized關鍵字通過編譯以後,會在同步快的先後分別造成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象。若是java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或者Class對象來做爲鎖對象。
  2. synchronized同步快對同一條線程來講是可重入的
  3. 相比synchronized,ReentrantLock增長了一些高級功能,主要有:等待可終端、可實現公平鎖,以及鎖能夠綁定多個條件
    1. 等待可中斷:當前持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其它市區
    2. 公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖
    3. 綁定多個條件:一個ReentrantLock對象能夠同時綁定多個Condition對象

非阻塞同步:

  1. 先進性操做,若是沒有其餘線程爭用共享數據,那操做就成功了;若是共享數據有爭用,產生了衝突,那就再採起其餘的補償措施(常見的是不斷重試,直到成功爲止),這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲非阻塞同步(Non-Blocking Synchronization)。
  2. 互斥同步最主要的問題就是進行線程阻塞和喚醒多帶來的性能問題,所以這種同步也稱爲阻塞同步(Blocking Synchronization)。
  3. CAS指令主要有三個操做數,分別是內存位置(java中能夠簡單理解爲變量的內存地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。指令執行時,當且僅當V符合舊預期值A時,處理器用新值更新V的值,不然它就不執行更新,可是不管是否更新了V的值,都會返回V的舊值,這個處理過程是一個原子操做。

無同步方案:

  1. 同步只是保證共享數據爭用時的正確性手段,若是一個方法原本就不涉及共享數據,它天然舊無須任何同步措施去保證正確性,所以會有一些代碼天才就是線程安全的。
  2. 可重入代碼(Reentrant Code):這種代碼也叫作純代碼(Pure Code),能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。若是一個方法,它的返回結果是能夠預測的,只要輸入了相同的數據,就都能返回相同的結果,那它就知足可重入性的要求,固然也是線程安全的。
  3. 線程本地存儲(Thread Local Storage):若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用問題。
    1. ThreadLocal類可實現線程本地存儲的功能。每個線程的Thread對象中都有一個ThreadLocalMap對象,這個對象存儲了一組以ThreadLocal.threadLocalHashCode爲鍵,以本地線程變量爲值的K-V值對,ThreadLocal對象就是當前線程的ThreadLocalMap的訪問入口,每個ThreadLocal對象都包含了一個獨一無二的threadLocalHashCode值,使用這個值就能夠在線程K-V值對中找回對應的本地線程變量。

13.3 鎖優化

13.3.1 自旋鎖與自適應鎖

  • 互斥同步對性能最大的影響是阻塞的實現,掛起線程和恢復線程都須要轉入內核態中完成,這些操做給系統的併發性能帶來了很大的壓力
  • 自旋鎖:若是物理機有一個以上的處理器,能讓兩個或以上的線程同時並行執行,可讓後面請求鎖的線程"稍等一會",但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。只需讓線程執行一個忙循環(自旋),就是自旋鎖
  • 自旋等待時間必需要有必定的限度,若是超過了必定次數仍然沒成功得到鎖,就使用傳統的方式掛起線程。默認值是10次,可以使用參數-XX:PreBlockSpiin來更改
  • 自適應鎖:自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自選時間及鎖擁有着的狀態來決定。

13.3.2 鎖消除

  • 鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。如StringBuffer的append()方法。

13.3.3 鎖粗化

  • 若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把鎖同步的範圍擴展(粗化)到整個操做序列的外部,一樣如StringBuffer的append()方法。

13.3.4 輕量級鎖

  • JDK1.6中新增,它名字中的"輕量級"是相對於使用操做系統互斥量來實現的傳統鎖而言的,傳統鎖稱爲"重量級"鎖。輕量級鎖不是用來代替重量級鎖,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。
  • HotSpot虛擬機的對象頭(Object Header)分爲兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡(GC Age)等,這部分數據長度在32位和64位虛擬機中分別爲32bit和64bit,官方稱它爲"Mark Word",它是實現輕量級鎖的關鍵。另外一部分用於存儲指向方法區對象類型數據的指針,若是是數組對象的話,還會有一個額外的部分用於存儲數組長度。
  • 執行過程以下:
  1. 在代碼進入同步塊的時候,若是此同步對象沒有被鎖定(鎖標誌位爲"01"狀態),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word)。
  2. 虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針。若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲"00",即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖13-4所示。
  3. 若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是隻說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行,不然說明這個鎖對象已經被其餘線程搶佔了。

輕量級鎖CAS操做以前堆棧與對象的狀態

輕量級鎖CAS操做以後堆棧與對象的狀態

  • 若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲"10",Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

     

13.3.5 偏向鎖

  • 它的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。若是說輕量級鎖是在無競爭的狀況下使用CAS操做去消除同步使用的互斥量,那偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS操做都不作了。
  • 當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲"01",即偏向模式。同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做(例如Locking、Unlocking及對Mark Word的Update等)。
  • 當有另一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位爲"01")或輕量級鎖定(標誌位爲"00")的狀態,後續的同步操做就如上面介紹的輕量級鎖那樣執行。

 

13.4 本章小結

 

 

序言

 

  

 

序言    1

 

第一部分 走進Java    8

 

第1章 走進java    8

 

1.1 概述    8

 

1.2 java技術體系    8

 

1.3 java發展史    9

 

1.4 java虛擬機發展史    9

 

1.5 展望java技術的將來    9

 

1.6 實戰:本身編譯JDK    10

 

1.7 本章小結    10

 

第二部分 自動內存管理機制    10

 

第2章 Java內存區域與內存溢出異常    10

 

2.1 概述    10

 

2.2 運行時數據區域    11

 

2.3 HotSpot虛擬機對象探祕    13

 

2.4 實戰:OutOfMemoryError異常    16

 

2.5 本章小結    17

 

第3章 垃圾收集器與內存分配策略    17

 

3.1 概述    17

 

3.2 對象已死嗎    17

 

3.3 垃圾收集算法    20

 

3.4 HotSpot的算法實現    21

 

3.5 垃圾收集器    23

 

3.6 內存分配與回收策略    28

 

3.7 本章小結    30

 

第4章 虛擬機性能監控與故障處理工具    30

 

4.1 概述    30

 

4.2 JDK的命令行工具    30

 

4.3 JDK的可視化工具    33

 

4.4 本章小結    34

 

第5章 調優案例分析與實戰    34

 

5.1 概述    34

 

5.2 案例分析    34

 

5.3 實戰:Eclipse運行速度調優    36

 

5.4 本章小結    37

 

第三部分 虛擬機執行子系統    38

 

第6章 類文件結構    38

 

6.1 概述    38

 

6.2 無關性的基石    38

 

6.3 Class類文件的結構    38

 

6.3.1 魔數與Class文件的版本    39

 

6.3.2 常量池    39

 

6.3.3 訪問標誌    41

 

6.3.4 類索引、父類索引與接口索引集合    41

 

6.3.5 字段表集合    42

 

6.3.6 方法表集合    42

 

6.3.7 屬性表集合    42

 

6.4 字節碼指令簡介    44

 

6.4.1 字節碼與數據類型    44

 

6.4.2 加載和存儲指令    45

 

6.4.3 運算指令    45

 

6.4.4 類型轉換指令    45

 

6.4.5 對象建立與訪問指令    46

 

6.4.7 控制轉移指令    46

 

6.4.8 方法調用和返回指令    46

 

6.4.9 異常處理指令    46

 

6.4.10 同步指令    46

 

6.5 公有設計和私有實現    47

 

6.6 Class文件結構的發展    47

 

6.7 本章小結    47

 

第7章 虛擬機類加載機制    47

 

7.1 概述    47

 

7.2 類加載的時機    47

 

7.3 類加載的過程    49

 

7.3.1 加載    49

 

7.3.2 驗證    49

 

7.3.3 準備    51

 

7.3.4 解析    51

 

7.3.5 初始化    54

 

7.4 類加載器    55

 

7.4.1 類與類加載器    56

 

7.4.2 雙親委派模型    56

 

7.4.3 破壞雙親委派模型    57

 

第8章 虛擬機字節碼執行引擎    57

 

8.1 概述    57

 

8.2 運行時棧幀結構    57

 

8.2.1 局部變量表    58

 

8.2.2 操做數棧    58

 

8.2.3 動態鏈接    59

 

8.2.4 方法返回地址    59

 

8.2.5 附加信息    59

 

8.3 方法調用    59

 

8.3.1 解析    60

 

8.3.2 分派    60

 

8.3.3 動態類型語言支持    62

 

8.4 基於棧的字節碼解釋執行引擎    63

 

8.4.1 解釋執行    63

 

8.4.2 基於棧的指令集與基於寄存器的指令集    64

 

8.4.3 基於棧的解釋器執行過程    64

 

8.5 本章小結    64

 

第9章 類加載及執行子系統的案例與實戰    65

 

9.1 概述    65

 

9.2 案例分析    65

 

9.2.1 Tomcat:正統的類加載器架構    65

 

9.2.2 OSGi:靈活的類加載器架構    66

 

9.2.3 字節碼生成技術與動態代理的實現    67

 

9.2.4 Retrotranslator:跨越JDK版本    67

 

9.3 實戰:本身動手實現遠程執行功能    67

 

9.3.1 目標    68

 

9.3.2 思路    68

 

9.3.3 實現    68

 

9.3.4 驗證    68

 

9.4 本章小結    68

 

第四部分 程序編譯與代碼優化    68

 

第10章 早期(編譯期)優化    68

 

10.1 概述    68

 

10.2 Javac編譯器    68

 

10.2.1 Javac的源碼與調試    68

 

10.2.2 解析與填充符號表    69

 

10.2.3 註解處理器    70

 

10.2.3 語義分析與字節碼生成    70

 

10.3 Java語法糖味道    71

 

10.3.1 泛型與類型擦除    71

 

10.3.2 自動裝箱、拆箱與遍歷循環    72

 

10.3.3 條件編譯    72

 

10.4 實戰:插入式註解處理器    73

 

10.4.1 實戰目標    73

 

10.4.2 代碼實現    73

 

10.4.3 運行與測試    73

 

10.4.4 其它應用案例    73

 

10.5 本章小節    73

 

第11章 晚期(運行期)優化    73

 

11.1 概述    73

 

11.2 HotSpot虛擬機內的即時編譯器    73

 

11.2.1 解釋器與編譯器    73

 

11.2.2 編譯對象與觸發條件    75

 

11.2.3 編譯過程    78

 

11.2.4 查看及分析即時編譯結果    80

 

11.3 編譯優化技術    80

 

11.3.1 優化技術概覽    80

 

11.3.2 公共子表表達式消除    80

 

11.3.3 數組邊界檢查消除    80

 

11.3.4 方法內聯    81

 

11.3.5 逃逸分析    82

 

11.4 Java與C/C++的編譯器對比    83

 

11.5 本章小結    83

 

第五部分 高效併發    83

 

第12章 Java內存模型與線程    83

 

12.1 概述    83

 

12.2 硬件效率與一致性    83

 

12.3 Java內存模型    84

 

12.3.1 主內存與工做內存    84

 

12.3.2 內存間交互操做    85

 

12.3.3 對於volatile型變量的特殊規則    85

 

12.3.4 對於long和double型變量的特殊規則    85

 

12.3.5 原子性、可見性和有序性    86

 

12.3.6 先行發生原則    87

 

12.4 Java與線程    88

 

12.4.1 線程的實現    88

 

12.4.2 Java線程調度    91

 

12.4.3 狀態轉換    91

 

12.5 本章小結    93

 

第13章 線程安全與鎖優化    93

 

13.1 概述    93

 

13.2 線程安全    93

 

13.2.1 Java語言中的線程安全    93

 

13.2.2 線程安全的實現方法    94

 

13.3 鎖優化    97

 

13.3.1 自旋鎖與自適應鎖    97

 

13.3.2 鎖消除    97

 

13.3.3 鎖粗化    97

 

13.3.4 輕量級鎖    98

 

13.3.5 偏向鎖    99

 

13.4 本章小結    100

相關文章
相關標籤/搜索