1. 程式人生 > >深入理解java虛擬機器讀書筆記(推薦指數:☆☆☆☆☆)

深入理解java虛擬機器讀書筆記(推薦指數:☆☆☆☆☆)

深入理解Java虛擬機器讀書筆記

Java虛擬機器的發展史(略)

  1. SunClassic/Exact VM

    只能用純解釋方式來執行Java程式碼,如果使用JIT編譯器,就必須使用外掛。但是如果外掛了JIT編譯器,JIT編譯器完全接管了虛擬機器的執行系統,直譯器便不再工作了,即直譯器和編譯器不能配合工作。編譯器和解釋其的區別

  2. HotSpot VM

    JDK1.3後,HotSpot VM就成為預設的虛擬機器,其中HotSpot是指熱點探測技術,它通過計數器找出最具有價值的程式碼,然後通知JIT編譯器以方法為單位進行編譯

  3. 嵌入式的 VM和Meta-Circular VM(元迴圈VM)

  4. JRockit和IBM J9 VM

    JRockit專門為伺服器硬體和伺服器端應用場景高度優化的虛擬機器,因此內部不包含解析器的實現。J9會一款高效能的虛擬機器

自動記憶體管理機制

執行時資料區域

程式計數器

執行時的資料區可以分為執行緒之間共享的資料區和執行緒隔離的資料區,其中程式計數器是執行緒隔離的資料區,每個執行緒通過程式計數器來記錄當前執行的指令,或者說行號。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。需要注意的是執行native方法時,計數器值為空(Undefined)。

Java虛擬機器棧

通常把虛擬機器分為堆記憶體和棧記憶體,這裡的虛擬機器棧就是指棧記憶體。虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同,它描述的是Java方法執行的記憶體模型;每個方法在執行的時候都會建立一個棧幀,它是一種資料結構,每一個方法的從呼叫直至執行完成,就對應著一個棧幀在虛擬機器棧中的入棧和出棧的過程。如果執行緒請求的棧的深度大於所允許的深度,將丟擲StackOverflowError異常;如果虛擬機器棧在動態擴充套件時無法申請足夠的記憶體,將會丟擲OOM異常。

本地方法棧

本地方法棧和虛擬機器棧類似,只不過它是為本地方法服務的。

Java堆

Java堆是迅疾所管理的記憶體中最大的一塊,它能夠被所有的執行緒共享。此記憶體區域的唯一目的就是存放物件的例項,幾乎所有的物件例項都在這裡分配記憶體。Java堆是垃圾收集器管理的主要區域,也稱為“GC堆”。Java堆可以分為:新生代和老生代,再細緻點可以分為Eden空間、From Survivor空間、To Survivor空間。Java堆無法擴充套件時會丟擲OOM異常。

方法區

方法區也是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器就載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料(修飾符、常量池、欄位描述、方法描述),雖然Java虛擬機器把它描述為堆的一個邏輯部分,但是它卻有一個別名叫Non-Heap,目的是為了和Java堆區分開來。很多虛擬機器使用“永久代”來實現方法區,因此也稱為“永久代”。當方法區無法滿足記憶體分配的需求時,將丟擲OOM異常

常量池

執行時常量池是方法區的一部分,它是編譯期生成的各種字面量和符號引用,在類載入後進入常量池。同時執行時期間也能夠將新的常量放入常量池,比如呼叫String.intern()方法。由於受方法區的限制,因此也能丟擲OOM異常

直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁的使用,因此也可能會丟擲OOM異常。比如NIO會直接使用native方法分配對外記憶體。

HotSpot虛擬機器物件探祕

物件的建立過程

  1. 當虛擬機器遇到一條new指令時,會先去檢查這個指令的引數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用是否被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
  2. 類載入後虛擬機器將為新生物件分配記憶體,物件所需大小在類載入完成後便可確定。堆空間有兩種分配方式,一種是“指標碰撞”(注:其實翻譯為指標跳躍更恰當):也就是堆的記憶體分配是規整的,用過的記憶體放一邊,空閒的記憶體放一邊,分配的時候只需要移動中間的分界點指示器即可。還有一個分配方式稱為”空閒列表“,也就是虛擬機器內部維護一張表,記錄那些記憶體是使用的,哪些是空閒的。
  3. 為了保證併發分配記憶體的記憶體空間的安全性,虛擬機器採用CAS加失敗重試的方法保證更新操作的原子性。另一種方式是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,成為本地執行緒分配緩衝(TLAB),只有TLAB用完,才需要同步鎖定。
  4. 記憶體分配完後,虛擬機器需要將分配到的記憶體空間都初始化為零值。
  5. 接下來,虛擬機器要對物件進行必要的設定(設定物件頭),例如這個物件是哪個類的例項,如何才能找到類的元資料信心、物件的雜湊碼、物件的GC分代年齡資訊,這些資訊存放在物件的物件頭之中。
  6. 上面的工作完成後,從虛擬機器的角度來開,一個新的物件已經產生了,當從Java程式設計師的視角看,物件建立才剛剛開始,因為還要執行init方法來執行初始化的動作。

物件的記憶體佈局

物件在記憶體中儲存的佈局可以分為三塊區域:物件頭、例項資料和對齊填充

  1. 物件頭包括兩個部分:第一個部分儲存物件自身執行時資料:雜湊碼、GC分代年齡、鎖狀態標誌、偏向執行緒ID、偏向時間戳等,也稱為”Mark Word“。Mark Word被設計成一個非固定的資料結構以便在極小的空間儲存更多的資訊。

    另一個部分是型別指標,即物件指向它的類元資料的指標,可以通過這個指標來確定是哪個類的例項。如果物件是一個數據,那麼物件頭中還必須有一塊用於記錄陣列長度的資料。

  2. 接下來是物件真正儲存的例項資料部分,這部分的儲存順序受虛擬機器分配策略引數和再短在Java原始碼中的定義順序有關。HotSpot預設分配策略為longs/doubles,ints,shorts/chars,bytes/booleawns,oops(普通物件指標),也就是相同字寬的欄位總是放在一起。在滿足這個前提下,父類中定義的變數會出現在子類之前。

  3. 對齊填充並不是必然存在的,它的目的是保證物件的大小必須是8位元組的整數倍。

物件的訪問定位

棧上是通過引用來操作堆上的具體物件。引用型別在Java虛擬機器規範沒有指定具體實現,目前有兩種方式通過引用訪問物件:控制代碼和直接引用

控制代碼方式:堆中會劃分出一部分記憶體作為控制代碼池,引用實際是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料(指向方法區)各自的具體地址資訊。

直接引用方式:直接引用就是能夠直接訪問物件,但是必須也能同時訪問物件型別資料(型別資料在方法區)。

這兩種方法各有優勢,使用控制代碼的好處就是儲存的是穩定的控制代碼,在物件被移動時只會改變控制代碼中的例項資料指標。使用直接引用的好處就是速度更快,它節省了一次指標定位的時間開銷。對於HotSpot而言,它也是使用第二種方式進行物件訪問的

實戰:OOM異常

堆溢位

只要在程式碼中不斷地建立物件,並保證GC Roots到物件之間有可達路徑來避免垃圾回收機制清除這些物件,那麼在物件數量達到最大堆的容量限制後就會產生OOM異常。

可以通過-Xms和-Xmx引數設定最小最大堆和最大堆的數值,通過-XX:+HeapDumpOnOutOfMemoryError引數可以在OOM異常時Dump出記憶體快照。

丟擲OOM異常後,會列印是否為堆異常,在出現堆OOM異常時要區分是下面的那種情況:

  1. 記憶體洩漏:可以分析記憶體快照中的洩漏物件的GC Roots的引用鏈判斷
  2. 記憶體太小:這時候就需要調整前面提到的最小堆和最大堆的引數

虛擬機器棧和本地方法溢位

HotSpot不區分虛擬機器棧和本地方法棧,因此相關的引數設定命令(-Xoss)無效,只能用-Xss設定棧容量

在單執行緒情況下,一般只會丟擲StackOverFlow異常,因為記憶體太小和棧空間無法分配本質上是一個概念,在丟擲該異常後會列印棧深度。在多執行緒不斷建立執行緒的情況,會出現OOM異常,而且為每個執行緒分配的記憶體越大,越容易出現該異常。

方法區和執行時常量池溢位

可以通過-XX:PerSize和-XX:MaxPermSize來限制方法區大小,從而間接限制其中的常量池的容量

可以呼叫intern方法不斷將字串加入常量池

對於HotSpot虛擬機器和使用JDK1.6來說,常量池OOM會顯示PermGen space OOM,因為常量池屬於方法區的一部分,而方法區又是用永久代實現的。

但是JDK1.7開始逐步“去永久代”,因此使用JDK試驗會得出不同的結果。這同時引出了一個更有意思的案例:

//對於1.6會返回false,對於jdk1.7返回true
String str = new StringBuilder("計算機").append("軟體").toString();
System.out.println(str.intern() == str);
//對於jdk1.6會返回false,對於jdk1.7返回fasle
String str3 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str3);

JDK1.7的intern不會在複製例項,而只是在常量池中記錄首次出現的例項引用,因此intern返回的引用和由StringBuilder建立的那個字串例項是同一個,但是由於“java”這個常量已經由其他類載入到了常量池中,所以返回的false。

測試方法區OOM可以使用CGLib不斷的建立增強類,因為這類位元組碼技術需要足夠容量的方法區來保證動態生成的Class可以載入到記憶體中。

本機直接記憶體溢位

直接記憶體溢位常常和NIO的使用有關,因為它會佔用Java堆以外的記憶體。直接記憶體如果不指定預設和Java堆的最大值一樣,可以通過使用Unsafe類進行直接記憶體的分配來驗證OOM異常。

垃圾收集器與記憶體分配策略

概述

GC需要完成3件事情:

哪些記憶體需要回收

什麼時候回收

如何回收

瞭解GC是為了能夠排查各種記憶體溢位、記憶體洩漏問題,當垃圾收整合為系統達到更高併發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。

程式計數器、虛擬機器棧、本地方法棧這個三個區域隨執行緒而生,隨執行緒而滅。棧中的棧幀隨著方法的進入和退出進行出棧和入棧,每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的。因此GC主要是指Java堆和方法區的垃圾回收。

物件已死嗎

引用計數法

引用計數法就是給物件中的引用新增一個引用計數器,每當有一個地方引用它時,計數器的值就加1,當引用失效時,計數器值就減1,當引用計數為0就表示物件可以回收 。

引用計數法的效率很高,但是它不能解決物件之間迴圈依賴的問題。

可達性分析方法

可達性分析是主流的GC方法,基本思想就是通過一系列成為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明物件是不可用的。在Java中,可作為GC Roots的物件包括下面幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件

方法區中類靜態屬性引用的物件

方法區中常量引用的物件

本地方法棧中JNI引用的物件

在JDK1.2之後,Java對引用的概念進行了擴充:

  1. 強引用:通過new出來的引用,只要強引用還存在,垃圾收集器永遠不會回收掉引用的物件
  2. 軟引用:描述一些還有用但是非必須的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲異常
  3. 弱引用:一次GC就會回收
  4. 虛引用:幽靈引用,它的目的就是能在這個物件被回收時收到一個系統通知。

生存還是死亡

即使不可達的物件,也並不是非死不可的,要宣告一個物件死亡,至少經歷兩次標記過程:

  1. 第一次是可達性分析標記的物件,標記後還要進行篩選,篩選的條件是此物件有必要執行finalize方法(重寫了該方法並沒有被虛擬機器呼叫過)
  2. 有必要執行finalize的物件將會被放入F-Queue中,GC稍後會對該佇列中的物件進行第二次標記,如果仍未可達,物件將會被回收

需要注意的是:

  1. finalize方法不會被承諾執行並等待其結束,因為該方法可能執行比較緩慢,並且可能會出現死迴圈。
  2. 任何一個物件的finalize方法都只會被系統自動呼叫一次,如果物件第一次標記後在finalize中逃脫了,下一次回收時,它的finalize方法不會被執行。
  3. 儘量不要依賴finalize方法,因為它的不確定性大,且無法保證各個物件的呼叫順序

回收方法區

方法區垃圾回收的價效比很低,在堆中,尤其是新生代中,一次垃圾回收一般可以回收70%-95%,而永久代的垃圾回收率遠低於此。

永久帶的垃圾回收主要回收兩部分:廢棄常量和無用的類。廢棄常量的判斷比較簡單,就是沒有指向該常量的引用,對於無用的類來說,需要滿足三個條件:

  1. 該類的所有例項都已經被回收,也就是Java堆中不存在該類的任何實現
  2. 載入該類的ClassLoader已經被回收
  3. 該類對應的Class物件沒有任何地方被引用,無法在任何地方通過反射方位該類的方法

虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和物件一樣,不使用了就必然回收。HotSpot對是否進行回收提供了引數進行控制。

垃圾收集演算法

標記-清除演算法

首先標記需要回收的物件,然後進行回收,它的缺點:

  1. 標記和清除的效率都不高
  2. 清除後會產生大量不連續的記憶體碎片,空間碎片太多可能導致之後再分配較大物件時,無法找到最後的連續記憶體而不得不提前出發另一次垃圾回收動作

複製演算法

複製演算法會分配兩個記憶體塊,當GC後,仍存活的物件複製到另一個記憶體塊,然後把已用過的記憶體塊清理掉,這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等情況。同時由於新生代的物件大部分是要被GC的,因此不需要1:1的比例劃分兩個記憶體空間,而是將記憶體劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survior,且HotSpot的兩者的記憶體容量之比為8:1。

需要注意的是:

如果另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活的物件時,這些物件將通過分配擔保機制進入老年代。

標記-整理演算法

根據老年代的特點,有人提出了另一種標記-整理演算法那,標記過程和上面的一樣,但後續步驟不是直接對可回收u物件進行整理,而是讓存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

HotSpot的演算法實現

列舉根節點

可作為GC Root的物件主要在全域性性的引用和執行上下文中。可達性分析或者說列舉根節點是對時間敏感的,主要體現在下面兩個方面:

  1. 現在應用近方法區就有幾百兆記憶體,因此要逐個檢查這裡面的引用會消耗很多時間
  2. 可達性分析或者說列舉根節點時,需要確保快照是一致性的,也就是在整個分析期間整個執行系統看起來是被凍結起來的,不可以出現分析過程中物件引用關係還在不斷的變化情況。這導致GC時必須停頓所有的Java執行執行緒。

當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性引用位置,這是通過一個叫OopMap的資料結構實現的,它儲存了引用的位置(把棧上代表引用的位置全部記錄下來,從而實現準確式GC)。

安全點

在OopMap的幫助下,虛擬機器可以快速且準確的完成GC Roots列舉,但一個很現實的問題:如果未每一個指令都生成對應的OopMap,那將會需要大量的額外空間。實際上HotSpot並沒有為每條指令都生成OopMap,只是在特定的位置記錄了這些資訊,這些位置成為安全點。即程式執行時並非在所有的地方都停頓開始GC,即程式執行時並非在所有地方都能停頓下來開始GC,只有在達到安全點時才能暫停。安全點的選定不能太少(GC等待時間長),也不能太多(增大執行時負荷)。

對於安全點,另一個需要考慮的問題是如何在GC發生時讓所有執行緒都跑到最近的安全點上,有兩種方案可以選:

  1. 搶先式中斷:它不需要執行緒的執行程式碼主動去配合,在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它跑到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件。
  2. 主動式中斷:執行緒在安全點輪詢,發現當前中斷標誌位為真時就進行中斷掛起。

安全區域

在實際情況,執行緒可能會Sleep或者Bolcked,這時候執行緒就無法響應JVM的中斷請求,這種情況就需要安全區域來解決 :

安全區域是指在一段程式碼中,引用關係不會發生變化,在這個區域中的任意地方開始GC都是安全的。我們也可以把安全區域看成是安全點的擴充套件。

當執行緒執行到安全區域時,首先標識自己進入了安全區域。那樣,在當前這段時間發生GC,就不用管標識自己為安全區域狀態的執行緒了。

線上程要離開安全區域時,它要檢查系統是否已經完成了根節點列舉,如果完成了,那執行緒就繼續執行,否則它就必須等待知道收到可以安全離開安全區域的訊號為止。

垃圾收集器

收集演算法是記憶體回收的方法論,而垃圾收集器是記憶體回收的具體實現。虛擬機器包含的所有收集器如圖所示:

新生代:Serial ParNew Parallel Scavenge

————————————————G1————

老年代:CMS Serial Old Parallel Old

Serial收集器

重點:

  1. 歷史悠久的收集器,採用複製演算法的新生代收集器
  2. 完全單執行緒,收集時會停止到其他的執行緒(“Stop The World”)
  3. 注意:之後發展的收集器也不能完全消除暫停執行緒,只能不斷縮短暫停的時間
  4. 它是虛擬機器在執行在Client模式下的預設新生代收集器

ParNew 收集器

重點:

  1. Serial收集器的多執行緒版本
  2. 除Serial外,只有他能夠CMS收集器配合(不幸的是,JDK1.5提出的CMS作為老年代的收集器,卻無法與JDK1.4中已經存在的Parallel Scavenge配合工作)
  3. 在單核環境下,效能不會超過Serial收集器
  4. 預設開啟的收集執行緒和CPU的數量一樣多,也可以通過引數限制執行緒數

Parallel Scavenge收集器

重點:

  1. 新生代收集器,也是採用複製演算法,JDK1.4中已經存在

  2. 它的目標是達到一個可控制的吞吐量,所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值

  3. 停頓時間短—>適合需要與使用者互動的程式,響應快;高吞吐量–>可以高效率的利用CPU,適合後臺運算而不需要太多互動的任務

  4. 相關控制引數:

    -XX:MaxGCPauseMillis:控制停頓時間,注意GC停頓時間越短,吞吐量越小,新生代的空間越需要的越多

    -XX:GCTimeRatio:控制垃圾收集時間佔總時間的比率(比如該值為19則,GC時間佔比1/20),相當於(約等)吞吐量的倒數

    -XX:UseAdaptiveSizePolicy:開啟後不用手動同時指定上面兩個引數(可以指定單個),收集器會自適應改變上面兩個引數

Serial Old

重點:

  1. 是Serial收集器的老年版,使用標記-整理演算法

  2. 在Server模式下,它主要有兩個作用:

    在JDK1.5以及之前的版本與Parallel Scavenge收集器搭配使用

    作為CMS收集器的後備預案

Parallel Old收集器

重點:

  1. Parallel Old是Parallel Scavenge收集器的老年代版本,採用多執行緒和複製整理演算法,JDK1.6中才開始提供的
  2. 它出來之前,除了Serial Old外,PS收集器別無其他可以合作的老年代收集器

CMS收集器

重點:

  1. 以獲取最短回收停頓時間為目標的收集器,看重服務的響應速度,採用標記-清除演算法,收集的過程分為4個過程:

    初始標記:僅標記GC Roots能直接關聯的物件

    併發標記:併發進行GC Roots Tracing

    重新標記:修正併發標記期間因程式的繼續執行產生的變動

    併發清除:

  2. 初始標記、重新標記仍需要“Stop The World”;併發標記、併發清除時間耗時最長

  3. 缺點:

    1. CMS收集器對CPU資源非常敏感,CPU個數越少,CMS對使用者程式的影響就可能變得很大
    2. CMS收集器無法處理浮動垃圾:併發標記時新產生的垃圾只能在下一次清理,因此,CMS收集器不能像其他老年代收集器在老年代幾乎填滿了在進行收集,可以通過引數來設定觸發比。如果CMS期間記憶體不夠用,將會臨時啟用Serial Old收集器重新收集
    3. 採用標記-清除演算法,因此會有空間碎片產生,如果無法找到足夠大的的連續空間來分配物件,會提前觸發Full GC。提供了一個引數來開啟在Full GC之前進行空間整理

G1 收集器

重點:

  1. 當今發展最前沿的成果之一,JDK1.7提供,它是面向服務端應用的垃圾收集器

  2. G1能充分利用多CPU,縮短StopTheWorld的時間

  3. G1也是能分代收集的,雖然它能管理整個堆。它能夠採用不同的方式處理新生代和老年代物件

  4. G1從整理上看是標記整理演算法,從區域性上看是複製演算法

  5. 能夠預測停頓

  6. G1將記憶體劃分為多個Region,新生代和老生代不在是物理隔離,按照Region回收價值最大的先回收策略

  7. 需要處理的問題:

    多個Region會互相關聯的引用,怎麼來避免全部掃描堆記憶體:採用Remembered Set來避免

理解GC日誌

注意點:

  1. 會顯示Full GC(會StopTheWorld)還是Minor GC
  2. 會顯示GC發生的區域、時間、GC前和後的記憶體

記憶體分配與回收策略

物件的記憶體分配往大方向講,就是在堆上分配。物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配快取,則優先在tlab上分配,少數情況下也可能直接分配在老年代中。

物件優先在Eden分配

  1. 大多數情況,物件的新生代在Eden區中分配
  2. 當Eden區不足時,會發起Minor GC
  3. 當Survivor不足時會分配到老年代中(分配擔保機制)

大物件直接進入老年代

  1. 大且短命的大物件對虛擬機器的記憶體分配來說就是一個壞訊息
  2. -XX:PretenureSizeThreshold引數可以令物件大小大於該值的物件直接分配在老年代中

長期存活的物件將進入老年代

  1. 虛擬機器給每個物件定義了一個物件年齡計數器,物件在Survivor中每熬過一次Minor GC,年齡都會增加一歲
  2. 年齡增加到一定的程度,就會晉升到老年代中,這個程度也可以通過引數設定

物件動態年齡的判斷

  1. 虛擬機器並不是永遠要求物件的年齡必須達到某個程度才會晉升老年代,如果在Survior空間中相同的年齡所有物件的大小總和大於Survivor空間的一半,年齡大於或者等於該年齡的物件就直接進入老年代,無需等到某個歲數

空間擔保分配

  1. 在Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的
  2. 如果不成立,則虛擬機器會檢視是否允許擔保失敗
  3. 如果允許,虛擬機器會檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於會嘗試著進行一次Minor GC
  4. 如果小於或者不允許冒險,那麼這時也要改為進行一次Full GC
  5. 擔保失敗也會觸發Full GC

虛擬機器效能監控與故障處理工具

JDK命令列工具

主要有:

  1. jps:虛擬機器程序狀況工具
  2. jstat:虛擬機器統計資訊工具:類裝載、記憶體、垃圾收集、JIT編譯等資料
  3. jinfo:Java配置資訊工具:實時地檢視和調整虛擬機器各項引數
  4. jmap:Java記憶體對映工具:用於生成堆轉儲快照
  5. jhat:虛擬機器堆轉儲快照分析工具
  6. jstack:Java堆疊跟蹤工具:生成執行緒快照
  7. HSDIS:JIT生成程式碼反彙編

JDK的視覺化工具

JConsole和VisualVM

Integer.valueOf會快取[-128,127]的整數

調優案例分析與實戰

案列分析

高效能硬體上的程式部署策略

問題:高效能硬體上的超大堆記憶體,Full GC能有十幾秒,會造成服務停頓。

如果是通過64位JDK使用大記憶體的缺點:

大記憶體GC停頓時間長,64位JDK沒有32位快,如果仍溢位,dump出的堆轉儲快照很大無法分析,64JDK消耗較大(指標膨脹,資料型別對齊等造成)

解決辦法:使用若干個32位虛擬機器建立邏輯叢集來利用硬體資源(無Session複製的親合式叢集),但可能會遇到的問題:

  1. 儘量避免節點競爭全域性的資源
  2. 很難最高效地利用某些資源池
  3. 各個節點仍面臨32位的記憶體的限制
  4. 大量使用本地快取,比如HashMap快取導致較大的記憶體浪費

叢集間同步導致的記憶體溢位

問題:一個BS系統,採用叢集部署,需要各個節點共享資料,不定期出現記憶體洩漏

原因:使用JBossCache構建全域性快取,會向所有節點同步操作時間,導致網路互動繁忙,從而會導致訊息重發,大量的重發訊息會在記憶體快取,從而導致OOM

堆外記憶體導致的溢位錯誤

問題:使用NIO導致直接記憶體溢位

引申出類似的非常見非堆記憶體過大問題:

  1. Directr Memory:可以通過引數控制大小
  2. 執行緒堆疊:可以通過引數控制大小
  3. Socket緩衝區:每個Socket連線都有接收和傳送快取,可能會導致溢位
  4. JNI程式碼:本地記憶體也不再堆中,可能會溢位
  5. 虛擬機器和GC:虛擬機器和GC的程式碼也要消耗一定的記憶體,因此需要預留一定的空間

外部命令導致系統緩慢

問題:java調動shell命令,會克隆執行緒導致大量佔用CPU資源

解決:使用Java API實現

伺服器JVM程序崩潰

問題:出現叢集虛擬機器自動關閉的情況

原因:非同步任務返回時間過長導致Socket連線越來越多,最終是JVM崩潰

不恰當的資料結構導致記憶體佔用過大

問題:在記憶體中載入大資料會造成GC長時間停頓

解決:考慮將Survivor空間去掉,大資料直接進入老年代

Windows虛擬記憶體導致的常見停頓

問題:準備開始GC到開始GC之間消耗了大部分時間

原因:GUI程式在最小化的時候,工作記憶體被自動交換到磁碟的頁面檔案之中了,發生GC時就有可能因為恢復頁面檔案的操作導致不正常的GC停頓

類檔案結構

Class類檔案的結構

  1. 任何一個Class檔案都對應著唯一一個類或介面的定義資訊,但反過來,類或介面並不一定都得定義在檔案裡(譬如類或介面也可以通過類載入器直接生成)。
  2. Class檔案是一組8位位元組為基礎單位的二進位制流,它只包含兩種型別:無符號數和表,無符號數u1,u2,u3,u4分表表示1,2,3,4個位元組

魔數與Class檔案的版本

  1. 每個Class檔案的頭4個位元組成為魔數,確定該Class檔案是否能夠被虛擬機器接受

  2. 魔數後面的4個位元組是Class檔案的版本號,虛擬機器會校驗是否是JDK支援的版本

    4個位元組魔數->4個位元組版本號->

常量池

  1. 常量池是Class檔案之中的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是Class檔案空間最大的資料專案之一,同時它還是在Class檔案中第一個出現的表型別資料專案

  2. 開頭是兩個位元組是常量池數量。常量池主要存放兩個類常量:字面量(文字字串、宣告為final的常量值等)和符號引用。符號引用則屬於編譯方面的概念,包括:

    類和介面的全限定名

    欄位的名稱和描述符

    方法的名稱和描述符

  3. 常量池的每一項常量都是一個表,常量之間可以互相引用,也就是常量表之間可以關聯。

  4. 每個表開始的第一位是一個u1型別的標誌位,表示是14張常量表中的哪一個

  5. CONSTANT_Utf8_info型別的常量一般儲存類的限定名,因此很多常量都是引用該型別的常量

    4個位元組魔數->4個位元組版本號->連續出現的常量表

訪問標誌

  1. 在常量池之後,緊接著的兩個位元組代表訪問標誌,識別類或介面層次的訪問資訊,包括:這個Class是類還是介面;是否定義為public型別;是否為abstract型別;是否為final等

    4個位元組魔數->4個位元組版本號->連續出現的常量表->類訪問標誌

類索引、父類索引、介面索引集合

  1. 類索引和父類索引都是一個u2型別的資料,而介面是一組u2型別的資料的集合

    4個位元組魔數->4個位元組版本號->連續出現的常量表->類訪問標誌->類索引->父類索引->介面索引

欄位的集合

  1. 欄位表用於描述介面或者類中宣告的變數,欄位包括類級變數以及例項級變數,但不包括在方法內部宣告的區域性變數

  2. 欄位表由access_flags、name_index、descriptor_index、attributes_count、attributes組成

  3. 欄位表不會列出從超類或者父介面中繼承而來的欄位,編譯器可能會自定新增欄位,比如在內部類中為了保持對外部類的訪問性,最自動新增指向外部類的例項的欄位

    4個位元組魔數->4個位元組版本號->連續出現的常量表->類訪問標誌->類索引->父類索引->介面索引->欄位表集合

方法表集合

  1. 和欄位表類似,不同的是方法中的程式碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中的一個名為“Code”的屬性裡面

  2. 與欄位表集合相對應的,如果父類方法在子類中沒有重寫,方法表集合中就不會出現來自父類的方法資訊。同樣的,有可能也會出現編譯器自動新增的方法,最典型的便是類構造器和例項構造器方法

    4個位元組魔數->4個位元組版本號->連續出現的常量表->類訪問標誌->類索引->父類索引->介面索引->欄位表集合->方法表集合

屬性表集合

  1. 在Class檔案、欄位表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的資訊。屬性值長度不一,資料自定義結構,只要指出佔用多少位元組就可以了

  2. Java虛擬機器執行位元組碼是基於棧的體系結構

  3. Code屬性:

    Java程式方法體中的程式碼經過Javac編譯器處理後,最終變為位元組碼指令儲存在Code屬性內,注意介面或者抽象類的方法不存在Code屬性

    位元組碼之後的是這個方法的顯示異常處理表,異常表對於Code屬性來說並不是必須存在的

    編譯器使用的異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制,編譯器會自動在每段可能的分支路徑之後都將finally語句塊的內容冗餘生成一遍實現finally語義

  4. Exceptions屬性:

    與Code屬性平級的一項屬性,Exceptions屬性的作用是列舉出方法中可能丟擲的受檢查異常,也就是方法描述時在throws關鍵字後面列舉的異常

  5. LineNumberTable屬性

    用於描述Java原始碼行號與位元組碼行號之間的對應關係,並不是執行時必須的屬性

  6. LocalVariableTable屬性

    用於描述棧幀中區域性變量表中的變數與Java原始碼中定義的變數之間的關係

  7. SourceFile屬性

    用於記錄生成這個Class檔案的原始碼檔名稱

  8. ConstantValue屬性

    該屬性的作用是通知虛擬機器自動為靜態變數賦值,只有被static關鍵字修飾的變數才可以使用這項屬性。對於非static型別的變數的賦值的賦值只在例項構造器中進行的,對於類變數(static)來說,如果使用了final或者資料型別為基本型別或者String的話,就生成ConstantValue屬性來進行初始化,如果沒有final,或者並非基本型別及字串,會在類構造器中進行初始化

  9. InnerClass屬性

    用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成該屬性。

  10. Deprecated以及Synthetic屬性

    這兩個屬性都屬於標誌型別的布林屬性,只存在有和沒有。前者表示某個類、欄位、或者方法已經被程式作者定位不再推薦使用;後者表示此欄位或者方法並不是由Java原始碼直接產生,而是由編譯器自動產生的,最常見的是Bridge Method,但是除init和clinit方法之外

  11. StackMapTable屬性

    位於Code屬性的屬性表中,這個屬性會在虛擬機器類載入的位元組碼驗證階段被新型別檢查驗證器使用,目的是在於代替比較消耗效能的基於資料流的類推導驗證器

  12. Signature屬性

    該屬性會記錄類、介面、初始化方法或者成員的泛型簽名信息

  13. BootstrapMethods屬性

    是一個複雜的變長屬性,用於儲存invokedynamic指令引用的引導方法限定符

4個位元組魔數->4個位元組版本號->連續出現的常量表->類訪問標誌->類索引->父類索引->介面索引->欄位表集合->方法表集合->屬性表集合

虛擬機器類載入機制

概述

  1. 虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制
  2. Java的類的載入、連線和初始化過程都是在執行期間完成的,因此Java天生可以動態擴充套件

類載入的時機

  1. 類的宣告週期:載入-{(連線)驗證-準備-解析}-初始化-使用-解除安裝
  2. 解析在某些時候可能會出現在初始化解讀之後,比如執行時繫結
  3. 虛擬機器規範嚴格規定了有且只有5種情況必須立即對類進行初始化(如果類還沒有初始化):
    1. 遇到new getstatic putstatic或invokestatic者4條位元組碼指令時,如果類沒有初始化則進行初始化
    2. 使用reflect包的方法對類進行反射呼叫的時候
    3. 當初始化一個類的時候,其父類還沒有進行過初始化,則需要先觸發其父類的初始化,注意的是,一個介面在初始化時,並不要求其父類介面全部都完成了初始化
    4. 當虛擬機器啟動時,使用者需要制定一個執行的主類,虛擬機器會先初始化這個主類
    5. 當使用JDK1.7的動態語言支援時,如果一個MethodHandle例項最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化時,則需要先進行初始化
  4. 上面的5種成為對類的一個主動引用,除此之外,所有引用類的方式都不會觸發初始化,也稱為被動引用:
    1. 通過子類引用父類的靜態欄位,不會導致子類初始化
    2. 通過陣列定義來引用類,不會觸發此類的初始化,陣列在虛擬機器中其實是虛擬機器自己創造的一個類
    3. 常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化

類載入的過程

載入

  1. 載入需要完成三件事:

    通過一個類的全限定名獲取此類的二進位制位元組流

    將這個位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構

    在記憶體生成該類的Class物件,作為方法區這個類的各種資料的訪問入口

  2. 相對於其他階段,一個非陣列類的載入階段是開發人員可控性最強的,我們可以自己定義類載入器來完成載入行為

  3. 對於陣列類,它的載入過程:

    1. 如果陣列的元件型別是引用型別,那就遞迴採用上面提到的載入過程載入這個元件型別
    2. 如果不是引用型別,會把陣列標記為與引導類載入器關聯
    3. 陣列類的可見性與它的元件型別的可見性一致
  4. 載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需要的格式儲存在方法區中,方法區中的資料儲存格式虛擬機器自行定義,然後在記憶體中例項化一個Class類的物件

驗證

  1. 驗證的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求
  2. 驗證包括:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證

準備

  1. 準備階段說是正式為類變數分配記憶體並設定變數初始值階段,這些變數所使用的記憶體都將在方法區中進行分配,注意這時候分配的是類變數不是例項變數,例項變數會分配在java堆中,另外這裡所說的初始值是指對應型別的零值
  2. 如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數value就會被初始化為ConstantValue屬性所指定的值

解析

  1. 解析階段是虛擬機器將常量池的符號引用替換為直接引用的過程
  2. 符號引用是以一組符號來描述所引用的目標,直接引用則是直接指向目標的指標
  3. 解析包括:類或介面的解析,欄位 解析,類方法解析,介面方法解析

初始化

  1. 初始化是載入過程的最後一步,初始化是執行clinit方法的過程,ciinit方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序和類中出現的順序一致,因此靜態語句塊中只能訪問到定義在靜態語句塊之前的變數
  2. 虛擬機器保證在子類的clinit方法執行之前,父類的clinit方法已經執行完畢,因此父類中定義的靜態語句塊要優先於子類的變數賦值操作
  3. clinit方法不是必須的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不生成該方法
  4. 虛擬機器會保證一個類的clinit方法在多執行緒環境中被正確的加鎖、同步

類載入器

“通過一個類的全限定名稱來獲取描述此類的二進位制位元組流”這個動作成為類載入

類與類載入器

一個類是由它的類載入器和這個類本身一起確立其在Java虛擬機器中的唯一性。兩個相同限定名的類,經過不同的類載入器載入也是代表兩個不同的類,而且Class的equals,isAssignableFrom,isInstance方法返回的結果也會不一致

雙親委派模型

從虛擬機器的角度來說,有兩種不同的類載入器:一種是啟動類載入器(Bootstrap Classloader),是C++實現的,它是虛擬機器的一部分;另一個部分就是所有其他的類載入器,是由Java實現的,且使用者可以自定義

從開發人員的角度可以分為三種類載入器:

啟動類載入器:負責載入JAVA_HOME/lib目錄中的被虛擬機器識別的類,無法被Java直接引用,使用者在編寫自定義的類載入器的時候,如果需要把類載入請求委託給引導類載入器,直接給載入器賦值為null就行

擴充套件列載入器:負責載入JAVA_HOME/lib/ext目錄中的類

應用程式載入器:由AppClassLoader實現,一般稱為系統類載入器,負載加使用者的ClassPath上說指定的類,開發者可以直接使用這個類載入器,也是預設使用的類載入器

優先順序:啟動類載入器->擴充套件類載入器->應用程式類載入器->自定義類載入器

類載入器的雙親委派模型:

要求除了頂層啟動類載入器外,其餘的類載入器都應當有自己的父類載入器

過程: 如果一個類載入器收到了類載入的請求,它首先不會嘗試載入這個類,而是把請求往上傳遞,只有當父載入器反饋無法載入的時候,子載入器才會嘗試載入

好處:載入器有優先順序關係,對於那些公用的類來說,都可以委託優先順序高的類統一載入

破壞雙親委派模型:

第一次: 由於JDK1.2之後才引入的雙親委派模式,因此為了前向相容,允許使用者自定義loadClass的程式碼,從而可以使用自定的載入類載入程式碼。JDK1.2之後,建議通過findClass來定義自己的類載入器

第二次:JNDI,JDBC等需要呼叫獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI)的程式碼,因此需要委託子載入器載入程式碼,可以通過Thread類的setContextClassLoader()來設定載入器,預設是應用程式類載入器

第三次:像OSGi的熱程式碼替換技術重新構建了自己的類載入邏輯,沒有采用雙親委派模式,而是引入了Bundle的概念,Bundle類似於模組的概念,當更換一個Bundle的時候,就把Bundle連通類載入器一起更換

虛擬機器位元組碼執行引擎

概述

執行引擎是Java虛擬機器的最核心組成部分之一,在不同的虛擬機器實現裡面,執行引擎在執行Java程式碼的時候可能會有解釋執行和編譯執行

執行時棧幀結構

棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,它是虛擬機器執行時資料區中的虛擬機器的棧元素,每一個方法從呼叫開始至執行完成的過程,都對應著一個棧幀在虛擬機器裡面從入棧到出棧的過程。

棧幀中的組成:

區域性變量表、運算元棧、動態連結、返回地址等資訊

在編譯的時候,棧幀中需要多大的區域性變量表,多深的運算元棧都已經完全確定了,並寫入了方法表的Code屬性中,因此一個棧幀需要分配多少記憶體,是編譯時確定的

在活動的執行緒中,只有位於棧頂的棧幀才是有效的,稱為當前棧,這個棧幀關聯的方法稱為當前方法。執行引擎執行的所有位元組碼指令都只針對當前棧幀進行操作。

區域性變量表

  1. 區域性表量表用來儲存方法引數和方法內定義的區域性變數,在編譯時,Code屬性中的max_local就定義了其最大容量。
  2. 區域性變量表的基本儲存單位是Slot,Slot的長度和虛擬機器相關,但是要滿足儲存一些基本的資料型別(像int這種32位的資料型別)。對於64位的基本資料型別,虛擬機器會以高位對齊的方式分配兩個連續地Slot空間
  3. 虛擬機器通過索引定位的方式使用區域性變量表,索引值得範圍從0開始至區域性表量表最大的Slot數量
  4. 在方法執行的時候,虛擬機器是使用區域性變量表完成引數值到引數變數列表的傳遞過程,如果執行的是例項方法,區域性變量表的第0位索引預設是this變數
  5. 區域性變量表的Slot是可以重用的,當超出變數作用域,且後面又有新的變量出現就會重用之前變數的Slot
  6. 需要注意的是,區域性變數不像之前介紹的類變數一樣存在準備階段,類變數會經過兩次初始化過程,一次是在準備階段,賦予系統初始值,另一次是在初始化階段,賦予程式定義的初始值。但是區域性變數沒有這些,沒有賦值的區域性變數是無法引用的

運算元棧

  1. 運算元棧的最大深度也是編譯時確定好的,存於Code屬性表中
  2. 運算元棧的每一個元素可以是任意的Java資料型別,包括long和double
  3. 當一個方法開始執行的時候,運算元棧是空的,在方法執行的過程中,不斷的會有入棧和出棧的操作
  4. 運算元棧的資料型別必須和當前要執行的指令型別嚴格匹配,不然會報錯
  5. 棧幀中,為了減少額外的引數賦值傳遞,為讓不同棧幀的區域性變量表共享區域和運算元棧共享區域重疊
  6. Java虛擬機器是基於棧的執行引擎,其中棧就是指運算元

動態連結

  1. 每個棧幀都持有一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連結
  2. 符號引用一部分會在類載入階段直接化為直接引用,這稱為靜態解析,另一部分會在執行時進行動態解析

方法返回地址

  1. 有兩種方式退出方法:

    第一種:遇到了返回的位元組碼指令,正常退出時,呼叫者的PC計數器的值可以作為返回地址

    第二種:遇到了異常且異常表中沒有匹配的異常處理器,就會導致方法退出。異常退出時,返回地址是要通過異常處理器表來確定

附加資訊

虛擬機器規範允許具體的虛擬機器實現增加一些規範裡沒有描述的資訊到棧幀之中,例如與除錯相關的資訊,這部分完全取決於具體的虛擬機器實現

方法呼叫

方法的呼叫只是確定呼叫方法的版本,不涉及方法內部的執行過程。Class檔案的編譯過程中不包含傳統編譯中的連線步驟,一切方法呼叫在Class檔案裡面儲存的都只是符號引用,而不是方法在實際執行時記憶體佈局中的入口地址。因此Java需要在類載入階段甚至是執行時才能決定所呼叫目標方法的直接引用

解析

  1. 如果方法在真正執行之前就可以確定呼叫的版本,並且在執行時是不可變的,則在類載入的解析階段就會轉轉換為直接引用,採用這種方式的方法一般是靜態方法和私有方法,因為沒法重寫和改變

  2. 虛擬機器呼叫方法有5中指令:

    invokestatic:呼叫靜態方法

    inivokespecial:呼叫例項構造器方法,私有方法和父類方法

    invokevirtual:呼叫所有的虛方法

    invokeinterface:呼叫介面方法

    invokedynamic:現在執行時動態解析出呼叫點限定符所引用的方法,然後在執行該方法

    前面的4種指令的方法分派邏輯是固化在虛擬機器內部的,而invokedynamic指令的分派邏輯是由使用者所設定的引導方法決定的

    invokestatic和invokespecial都可以在解析階段確定,除此之外的方法都是虛方法。一個例外是雖然final方法也是通過invokevirtual呼叫的,但是它也是非虛方法

分派

  1. 靜態分派:實際上就是方法的過載,此時方法是依賴靜態型別來判斷和執行方法的

  2. 動態分派:動態分派實際指的就是多型性,也就是方法的重寫 ,動態分派會呼叫invokevirtual指令,其解析過程如下:

    找到運算元棧頂的第一個元素所指向的物件的實際型別,記做C

    如果在型別C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回直接引用,不如不通過則返回異常

    如果沒找到,按照繼承關係從下往上依次對C的各個父類進行第2步的搜尋和驗證過程

    如果沒有找到合適的方法則丟擲異常

  3. 單分派和多分派

    方法接收者和方法的引數統稱為方法的宗量,根據分派基於多少種宗量,可以將分派劃為單分派和多分派。

    Java語言是一門靜態多分派,動態單分派的語言

  4. 虛擬機器動態分派的實現:

    動態分派的方法版本選擇過程需要執行時在類的方法元資料中搜索合適的目標方法,因此在虛擬機器的實際實現中基於效能的考慮,大部分實現都不會真正地進行如此頻繁的搜尋,常用的優化方法有:虛方法表(如果子類重寫了某個方法,子類方法表中的地址將會替換為指向子類實現版本入口地址)。

動態語言的支援

JDK1.7新增了invokedynamic指令和invoke包來支撐動態語言。動態語言的一個特徵是:變數無型別而變數值才有型別。

invoke包的使用案例:

public class MethodHandleTest {

    static class ClassA{
        public void println(String s) {
            System.out.println(s);
        }
    }
    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        getPrintlnMH(obj).invokeExact("hi");
    }
    //MethodHandle可以動態確定方法
    private static MethodHandle getPrintlnMH(Object obj) throws NoSuchMethodException, IllegalAccessException {
        //MethodType代表方法型別,包括方法的返回值和引數
        MethodType methodType = MethodType.methodType(void.class, String.class);
        //bindTo用來繫結java方法的第一個隱式this引數
        return MethodHandles.lookup().findVirtual(obj.getClass(), "println", methodType).bindTo(obj);
    }
}

MethodHandle和Reflection的卻別:

反射是在java程式碼層次模擬方法的呼叫,而MethodHandle是在位元組碼層面模擬方法的呼叫

反射是重量級的,而MethodHandle是輕量級的

MethodHandle可以享有呼叫類似位元組碼指令時的虛擬機器優化,同時它可以不進針對java語言

invokedynamic指令:

其分派邏輯不是由虛擬機器決定的,而是由程式決定

引入了CONSTANT_InvokeDynamic_info常量,它包含引導方法,MethodType和名稱資訊

使用invoke包實現子類呼叫組類的方法:

public class TestInvoke { 
    class GrandFather{
        void thinking() {
            System.out.println("i am grandfather");
        }
    }
    class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }
    class Son extends Father {
        @Override
        void thinking() {
            MethodType mt = MethodType.methodType(void.class);
            try {
                MethodHandle special = MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                special.invoke(this);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        (new TestInvoke().new Son()).thinking();
    }
}

基於棧的位元組碼解釋執行引擎

  1. Java編譯器輸出的指令流,基本上是一種基於棧的指令集架構,與此相對的是基於暫存器的指令結構
  2. 基於棧的指令結構是可有移植,因為暫存器是和硬體強相關的
  3. 基於棧架構指令的主要缺點是執行速度相對較慢一點

類載入及執行子系統的案例與實戰

概述

對於JVM的編譯執行,使用者能通過程式操作的主要是位元組碼生成與類載入器

案例分析

Tomcat:正統的類載入架構

tomcat的要求:

  1. 不同的web應用程式使用的類庫可以實現相互隔離
  2. 不同的web應用程式使用的類庫也可以互相共享
  3. 伺服器要儘可能保證自身的安全不受部署的web應用程式影響
  4. 可能要支援HotSwap功能

因此,一個classpath在web伺服器是滿足不了要求的。Tomcat自定義了以下的類載入器:

Common類載入器:它包含兩個子類載入器:

Catalina類載入器

Shared類載入器–>WebApp類載入器–>Jsp類載入器

Tomcat使用了正統的雙親委派模式

OSGI:靈活的類載入架構

OSGI的每個模組成為Bundle,與普通的類庫區別不大。但是Bundle類載入器之間只有規則,沒有固定的委派關係

早期編譯期優化

Javac編譯器

Javac編譯器是由Java編寫的,編譯過程大致可以分為3個過程:

解析和填充符號表:解析又包括語法、詞法分析

插入式註解處理器的註解處理過程

分析與位元組碼生成過程:分析又包括標註檢查、解語法糖

語法糖的味道

泛型與型別擦除

Java的泛型其