您的位置:首頁>正文

重讀 Java虛擬機器(jvm)

1. Java 記憶體區域與記憶體溢出異常

1.1 運行時資料區域

根據《Java 虛擬機器規範(Java SE 7 版)》規定, Java 虛擬機器所管理的記憶體如下圖所示。

1.1.1 程式計數器

記憶體空間小, 執行緒私有。 位元組碼解譯器工作是就是通過改變這個計數器的值來選取下一條需要執行指令的位元組碼指令, 分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴計數器完成

如果執行緒正在執行一個 Java 方法, 這個計數器記錄的是正在執行的虛擬機器位元組碼指令的位址;如果正在執行的是 Native 方法, 這個計數器的值則為 (Undefined)。 此記憶體區域是唯一一個在 Java 虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域。

1.1.2 Java 虛擬機器棧

執行緒私有, 生命週期和執行緒一致。 描述的是 Java 方法執行的記憶體模型:每個方法在執行時都會床創建一個棧幀(Stack Frame)用於存儲區域變數表、運算元棧、動態連結、方法出口等資訊。 每一個方法從調用直至執行結束,

就對應著一個棧幀從虛擬機器棧中入棧到出棧的過程。

區域變數表:存放了編譯期可知的各種基本類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型)和 returnAddress 類型(指向了一條位元組碼指令的位址)

StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度。

OutOfMemoryError:如果虛擬機器棧可以動態擴展, 而擴展時無法申請到足夠的記憶體。

1.1.3 本地方法棧

區別於 Java 虛擬機器棧的是, Java 虛擬機器棧為虛擬機器執行 Java 方法(也就是位元組碼)服務, 而本地方法棧則為虛擬機器使用到的 Native 方法服務。 也會有 StackOverflowError 和 OutOfMemoryError 異常。

1.1.4 Java 堆

對於絕大多數應用來說, 這塊區域是 JVM 所管理的記憶體中最大的一塊。 執行緒共用, 主要是存放物件實例和陣列。 內部會劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer, TLAB)。

可以位於物理上不連續的空間, 但是邏輯上要連續。

OutOfMemoryError:如果堆中沒有記憶體完成實例分配, 並且堆也無法再擴展時, 拋出該異常。

1.1.5 方法區

屬於共用記憶體區域, 存儲已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。

現在用一張圖來介紹每個區域存儲的內容。

1.1.6 運行時常量池

屬於方法區一部分, 用於存放編譯期生成的各種字面量和符號引用。 編譯器和運行期(String 的 intern() )都可以將常量放入池中。 記憶體有限, 無法申請時拋出 OutOfMemoryError。

1.1.7 直接記憶體

非虛擬機器運行時資料區的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 類, 引入了一種基於通道(Channel)和緩存(Buffer)的 I/O 方式, 它可以使用 Native 函式程式庫直接分配堆外記憶體, 然後通過一個存儲在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。 可以避免在 Java 堆和 Native 堆中來回的資料耗時操作。

OutOfMemoryError:會受到本機記憶體限制, 如果記憶體區域總和大於實體記憶體限制從而導致動態擴展時出現該異常。

1.2 HotSpot 虛擬機器物件探秘

主要介紹資料是如何創建、如何佈局以及如何訪問的。

1.2.1 物件的創建

創建過程比較複雜, 建議看書瞭解, 這裡提供個人的總結。

遇到 new 指令時, 首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用, 並且檢查這個符號引用代表的類是否已經被載入、解析和初始化過。 如果沒有, 執行相應的類載入。

類載入檢查通過之後, 為新物件分配記憶體(記憶體大小在類載入完成後便可確認)。 在堆的空閒記憶體中劃分一塊區域(‘指標碰撞-記憶體規整’或‘空閒清單-記憶體交錯’的分配方式)。

前面講的每個執行緒在堆中都會有私有的分配緩衝區(TLAB), 這樣可以很大程度避免在併發情況下頻繁創建物件造成的執行緒不安全。

記憶體空間分配完成後會初始化為 0(不包括物件頭), 接下來就是填充物件頭,把物件是哪個類的實例、如何才能找到類的中繼資料資訊、物件的雜湊碼、物件的 GC 分代年齡等資訊存入物件頭。

執行 new 指令後執行 init 方法後才算一份真正可用的物件創建完成。

1.2.2 物件的記憶體佈局

在 HotSpot 虛擬機器中,分為 3 塊區域:物件頭(Header)、實例資料(Instance Data)和對齊填充(Padding)

對象頭(Header):包含兩部分,第一部分用於存儲物件自身的運行時資料,如雜湊碼、GC 分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳記等,32 位虛擬機器占 32 bit,64 位虛擬機器占 64 bit。官方稱為 ‘Mark Word’。第二部分是類型指標,即物件指向它的類的中繼資料指標,虛擬機器通過這個指標確定這個物件是哪個類的實例。另外,如果是 Java 陣列,物件頭中還必須有一塊用於記錄陣列長度的資料,因為普通物件可以通過 Java 物件中繼資料確定大小,而陣列物件不可以。

實例資料(Instance Data):程式碼中所定義的各種類型的欄位內容(包含父類繼承下來的和子類中定義的)。

對齊填充(Padding):不是必然需要,主要是占位元,保證物件大小是某個位元組的整數倍。

1.2.3 物件的訪問定位

使用物件時,通過棧上的 reference 資料來操作堆上的具體物件。

通過控制碼訪問

Java 堆中會分配一塊記憶體作為控制碼池。reference 存儲的是控制碼位址。詳情見圖。

使用直接指標訪問

reference 中直接存儲物件位址

比較:使用控制碼的最大好處是 reference 中存儲的是穩定的控制碼位址,在物件移動(GC)是只改變實例資料指標位址,reference 自身不需要修改。直接指標訪問的最大好處是速度快,節省了一次指針定位的時間開銷。如果是物件頻繁 GC 那麼控制碼方法好,如果是物件頻繁訪問則直接指標訪問好。

1.3 實戰

// 待填

2. 垃圾回收器與記憶體分配策略

2.1 概述

程式計數器、虛擬機器棧、本地方法棧 3 個區域隨執行緒生滅(因為是執行緒私有),棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。而 Java 堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於運行期才知道那些物件會創建,這部分記憶體的分配和回收都是動態的,垃圾回收期所關注的就是這部分記憶體。

2.2 對象已死嗎?

在進行記憶體回收之前要做的事情就是判斷那些物件是‘死’的,哪些是‘活’的。

2.2.1 引用計數法

給物件添加一個引用計數器。但是難以解決迴圈引用問題。

從圖中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。則在 Java 堆當中的兩塊記憶體依然保持著互相引用無法回收。

2.2.2 可達性分析法

通過一系列的 ‘GC Roots’ 的對象作為起始點,從這些節點出發所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連的時候說明物件不可用。

可作為 GC Roots 的對象:

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

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

方法區中常量引用的物件

本地方法棧中 JNI(即一般說的 Native 方法) 引用的物件

2.2.3 再談引用

前面的兩種方式判斷存活時都與‘引用’有關。但是 JDK 1.2 之後,引用概念進行了擴充,下面具體介紹。

下面四種引用強度一次逐漸減弱

強引用

類似於 Object obj = new Object(); 創建的,只要強引用在就不回收。

軟引用

SoftReference 類實現軟引用。在系統要發生記憶體溢出異常之前,將會把這些物件列進回收範圍之中進行二次回收。

弱引用

WeakReference 類實現弱引用。物件只能生存到下一次垃圾收集之前。在垃圾收集器工作時,無論記憶體是否足夠都會回收掉只被弱引用關聯的物件。

虛引用

PhantomReference 類實現虛引用。無法通過虛引用獲取一個物件的實例,為一個物件設置虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

2.2.4 生存還是死亡

即使在可達性分析演算法中不可達的物件,也並非是“facebook”的,這時候它們暫時出於“緩刑”階段,一個物件的真正死亡至少要經歷兩次標記過程:如果物件在進行中可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那他將會被第一次標記並且進行一次篩選,篩選條件是此物件是否有必要執行 finalize() 方法。當物件沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機器調用過,虛擬機器將這兩種情況都視為“沒有必要執行”。

如果這個物件被判定為有必要執行 finalize() 方法,那麼這個物件竟會放置在一個叫做 F-Queue 的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的 Finalizer 執行緒去執行它。這裡所謂的“執行”是指虛擬機器會出發這個方法,並不承諾或等待他運行結束。finalize() 方法是物件逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,如果物件要在 finalize() 中成功拯救自己 —— 只要重新與引用鏈上的任何一個物件簡歷關聯即可。

finalize() 方法只會被系統自動調用一次。

2.2.5 回收方法區

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠低於此。

永久代垃圾回收主要兩部分內容:廢棄的常量和無用的類。

判斷廢棄常量:一般是判斷沒有該常量的引用。

判斷無用的類:要以下三個條件都滿足

該類所有的實例都已經回收,也就是 Java 堆中不存在該類的任何實例

載入該類的 ClassLoader 已經被回收

該類對應的 java.lang.Class 物件沒有任何地方唄引用,無法在任何地方通過反射訪問該類的方法

2.3 垃圾回收演算法

僅提供思路

2.3.1 標記 —— 清除演算法

直接標記清除就可。

兩個不足:

效率不高

空間會產生大量碎片

2.3.2 複製演算法

把空間分成兩塊,每次只對其中一塊進行 GC。當這塊記憶體使用完時,就將還存活的物件複製到另一塊上面。

解決前一種方法的不足,但是會造成空間利用率低下。因為大多數新生代物件都不會熬過第一次 GC。所以沒必要 1 : 1 劃分空間。可以分一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的物件一次性複製到另一塊 Survivor 上,最後清理 Eden 和 Survivor 空間。大小比例一般是 8 : 1 : 1,每次浪費 10% 的 Survivor 空間。但是這裡有一個問題就是如果存活的大於 10% 怎麼辦?這裡採用一種分配擔保策略:多出來的物件直接進入老年代。

2.3.3 標記-整理演算法

不同於針對新生代的複製演算法,針對老年代的特點,創建該演算法。主要是把存活物件移到記憶體的一端。

2.3.4 分代回收

根據存活物件劃分幾塊記憶體區,一般是分為新生代和老年代。然後根據各個年代的特點制定相應的回收演算法。

新生代

每次垃圾回收都有大量物件死去,只有少量存活,選用複製演算法比較合理。

老年代

老年代中物件存活率較高、沒有額外的空間分配對它進行擔保。所以必須使用 標記 —— 清除 或者 標記 —— 整理 演算法回收。

2.4 HotSpot 的演算法實現

// 待填

2.5 垃圾回收器

收集演算法是記憶體回收的理論,而垃圾回收器是記憶體回收的實踐。

說明:如果兩個收集器之間存在連線說明他們之間可以搭配使用。

2.5.1 Serial 收集器

這是一個單執行緒收集器。意味著它只會使用一個 CPU 或一條收集執行緒去完成收集工作,並且在進行垃圾回收時必須暫停其它所有的工作執行緒直到收集結束。

2.5.2 ParNew 收集器

可以認為是 Serial 收集器的多執行緒版本。

並行:Parallel

指多條垃圾收集執行緒並行工作,此時用戶執行緒處於等候狀態

併發:Concurrent

指用戶執行緒和垃圾回收執行緒同時執行(不一定是並行,有可能是交叉執行),使用者進程在運行,而垃圾回收執行緒在另一個 CPU 上運行。

2.5.3 Parallel Scavenge 收集器

這是一個新生代收集器,也是使用複製演算法實現,同時也是並行的多執行緒收集器。

CMS 等收集器的關注點是盡可能地縮短垃圾收集時用戶執行緒所停頓的時間,而 Parallel Scavenge 收集器的目的是達到一個可控制的輸送量(Throughput = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))。

作為一個輸送量優先的收集器,虛擬機器會根據當前系統的運行情況收集性能監控資訊,動態調整停頓時間。這就是 GC 的自我調整調整策略(GC Ergonomics)。

2.5.4 Serial Old 收集器

收集器的老年代版本,單執行緒,使用 標記 —— 整理。

2.5.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多執行緒,使用 標記 —— 整理

2.5.6 CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標的收集器。基於 標記 —— 清除 演算法實現。

運作步驟:

初始標記(CMS initial mark):標記 GC Roots 能直接關聯到的物件

併發標記(CMS concurrent mark):進行 GC Roots Tracing

重新標記(CMS remark):修正併發標記期間的變動部分

併發清除(CMS concurrent sweep)

缺點:對 CPU 資源敏感、無法收集浮動垃圾、標記 —— 清除 演算法帶來的空間碎片

2.5.7 G1 收集器

面向服務端的垃圾回收器。

優點:並行與併發、分代收集、空間整合、可預測停頓。

運作步驟:

初始標記(Initial Marking)

併發標記(Concurrent Marking)

最終標記(Final Marking)

篩選回收(Live Data Counting and Evacuation)

2.6 記憶體分配與回收策略

2.6.1 對象優先在 Eden 分配

對象主要分配在新生代的 Eden 區上,如果啟動了本地執行緒分配緩衝區,將執行緒優先在 (TLAB) 上分配。少數情況會直接分配在老年代中。

一般來說 Java 堆的記憶體模型如下圖所示:

新生代 GC (Minor GC)

發生在新生代的垃圾回收動作,頻繁,速度快。

老年代 GC (Major GC / Full GC)

發生在老年代的垃圾回收動作,出現了 Major GC 經常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度一般會比 Minor GC 慢十倍以上。

2.6.2 大物件直接進入老年代

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

2.6.4 動態物件年齡判定

2.6.5 空間分配擔保

3. Java 記憶體模型與執行緒

3.1 Java 記憶體模型

遮罩掉各種硬體和作業系統的記憶體訪問差異。

3.1.1 主記憶體和工作記憶體之間的交互

操作作用物件解釋lock主記憶體把一個變數標識為一條執行緒獨佔的狀態unlock主記憶體把一個處於鎖定狀態的變數釋放出來,釋放後才可被其他執行緒鎖定read主記憶體把一個變數的值從主記憶體傳輸到執行緒工作記憶體中,以便 load 操作使用load工作記憶體把 read 操作從主記憶體中得到的變數值放入工作記憶體中use工作記憶體把工作記憶體中一個變數的值傳遞給執行引擎,

每當虛擬機遇到一個需要使用到變數值的位元組碼指令時將會執行這個操作

assign工作記憶體把一個從執行引擎接收到的值賦接收到的值賦給工作記憶體的變數,

每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作

store工作記憶體把工作記憶體中的一個變數的值傳送到主記憶體中,以便 write 操作write工作記憶體把 store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中

3.1.2 對於 volatile 型變數的特殊規則

關鍵字 volatile 是 Java 虛擬機器提供的最羽量級的同步機制。

一個變數被定義為 volatile 的特性:

保證此變數對所有執行緒的可見性。但是操作並非原子操作,併發情況下不安全。

如果不符合 運算結果並不依賴變數當前值,或者能夠確保只有單一的執行緒修改變數的值 和 變數不需要與其他的狀態變數共同參與不變約束 就要通過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。

禁止指令重排序優化。

通過插入記憶體屏障保證一致性。

3.1.3 對於 long 和 double 型變數的特殊規則

Java 要求對於主記憶體和工作記憶體之間的八個操作都是原子性的,但是對於 64 位的資料類型,有一條寬鬆的規定:允許虛擬機器將沒有被 volatile 修飾的 64 位元資料的讀寫操作劃分為兩次 32 位的操作來進行,即允許虛擬機器實現選擇可以不保證 64 位資料類型的 load、store、read 和 write 這 4 個操作的原子性。這就是 long 和 double 的非原子性協定。

3.1.4 原子性、可見性與有序性

回顧下併發下應該注意操作的那些特性是什麼,同時加深理解。

原子性(Atomicity)

由 Java 記憶體模型來直接保證的原子性變數操作包括 read、load、assign、use、store 和 write。大致可以認為基底資料型別的操作是原子性的。同時 lock 和 unlock 可以保證更大範圍操作的原子性。而 synchronize 同步塊操作的原子性是用更高層次的位元組碼指令 monitorenter 和 monitorexit 來隱式操作的。

可見性(Visibility)

是指當一個執行緒修改了共用變數的值,其他執行緒也能夠立即得知這個通知。主要操作細節就是修改值後將值同步至主記憶體(volatile 值使用前都會從主記憶體刷新),除了 volatile 還有 synchronize 和 final 可以保證可見性。同步塊的可見性是由“對一個變數執行 unlock 操作之前,必須先把此變數同步會主記憶體中( store 、write 操作)”這條規則獲得。而 final 可見性是指:被 final 修飾的欄位在構造器中一旦完成,並且構造器沒有把 “this” 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其他執行緒有可能通過這個引用訪問到“初始化了一半”的物件),那在其他執行緒中就能看見 final 欄位的值。

有序性(Ordering)

如果在被執行緒內觀察,所有操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句指“執行緒內表現為串列的語義”,後半句是指“指令重排”現象和“工作記憶體與主記憶體同步延遲”現象。Java 語言通過 volatile 和 synchronize 兩個關鍵字來保證執行緒之間操作的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由“一個變數在同一時刻指允許一條執行緒對其進行 lock 操作”這條規則獲得,這條規則決定了持有同一個鎖的兩個同步塊只能串列的進入。

3.1.5 先行發生原則

也就是 happens-before 原則。這個原則是判斷資料是否存在競爭、執行緒是否安全的主要依據。先行發生是 Java 記憶體模型中定義的兩項操作之間的偏序關係。

天然的先行發生關係

規則解釋程式次序規則在一個執行緒內,代碼按照書寫的控制流循序執行管程鎖定規則一個 unlock 操作先行發生於後面對同一個鎖的 lock 操作volatile 變數規則volatile 變數的寫操作先行發生於後面對這個變數的讀操作執行緒啟動規則Thread 物件的 start() 方法先行發生於此執行緒的每一個動作執行緒終止規則執行緒中所有的操作都先行發生於對此執行緒的終止檢測

(通過 Thread.join() 方法結束、 Thread.isAlive() 的返回值檢測)

執行緒中斷規則對執行緒 interrupt() 方法調用優先發生於被中斷執行緒的代碼檢測到中斷事件的發生

(通過 Thread.interrupted() 方法檢測)

物件終結規則一個物件的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始傳遞性如果操作 A 先於 操作 B 發生,操作 B 先於 操作 C 發生,那麼操作 A 先於 操作 C

3.2 Java 與執行緒

3.2.1 執行緒的實現

使用內核執行緒實現

直接由作業系統內核支援的執行緒,這種執行緒由內核完成切換。程式一般不會直接去使用內核執行緒,而是去使用內核執行緒的一種高級介面 —— 羽量級進程(LWP),羽量級進程就是我們通常意義上所講的執行緒,每個羽量級進程都有一個內核級執行緒支持。

使用用戶執行緒實現

廣義上來說,只要不是內核執行緒就可以認為是用戶執行緒,因此可以認為羽量級進程也屬於使用者執行緒。狹義上說是完全建立在用戶空間的執行緒庫上的並且內核系統不可感知的。

使用使用者執行緒夾加羽量級進程混合實現

直接看圖

Java 執行緒實現

平臺不同實現方式不同,可以認為是一條 Java 執行緒映射到一條羽量級進程。

3.2.2 Java 執行緒調度

協同式執行緒調度

執行緒執行時間由執行緒自身控制,實現簡單,切換執行緒自己可知,所以基本沒有執行緒同步問題。壞處是執行時間不可控,容易阻塞。

搶佔式執行緒調度

每個執行緒由系統來分配執行時間。

3.2.3 狀態轉換

五種狀態:

新建(new)

創建後尚未啟動的執行緒。

運行(Runable)

Runable 包括了作業系統執行緒狀態中的 Running 和 Ready,也就是出於此狀態的執行緒有可能正在執行,也有可能正在等待 CPU 為他分配時間。

無限期等待(Waiting)

出於這種狀態的執行緒不會被 CPU 分配時間,它們要等其他執行緒顯示的喚醒。

以下方法會然執行緒進入無限期等候狀態:

1.沒有設置 Timeout 參數的 Object.wait() 方法。

2.沒有設置 Timeout 參數的 Thread.join() 方法。

3.LookSupport.park() 方法。

限期等待(Timed Waiting)

處於這種狀態的執行緒也不會分配時間,不過無需等待配其他執行緒顯示地喚醒,在一定時間後他們會由系統自動喚醒。

以下方法會讓執行緒進入限期等候狀態:

1.Thread.sleep() 方法。

2.設置了 Timeout 參數的 Object.wait() 方法。

3.設置了 Timeout 參數的 Thread.join() 方法。

4.LockSupport.parkNanos() 方法。

5.LockSupport.parkUntil() 方法。

阻塞(Blocked)

執行緒被阻塞了,“阻塞狀態”和“等候狀態”的區別是:“阻塞狀態”在等待著獲取一個排他鎖,這個時間將在另外一個執行緒放棄這個鎖的時候發生;而“等候狀態”則是在等待一段時間,或者喚醒動作的發生。在程式等待進入同步區域的時候,執行緒將進入這種狀態。

結束(Terminated)

已終止執行緒的執行緒狀態。

4. 執行緒安全與鎖優化

// 待填

5. 類檔結構

// 待填

有點懶了。。。先貼幾個網址吧。

1. Official:The class File Format

2.亦山: 《Java虛擬機器原理圖解》 1.1、class檔基本組織結構

6. 虛擬機器類載入機制

虛擬機器把描述類的資料從 Class 檔載入到記憶體,並對資料進行校驗、裝換解析和初始化,最終形成可以被虛擬機器直接使用的 Java 類型。

在 Java 語言中,類型的載入、連接和初始化過程都是在程式運行期間完成的。

6.1 類載入時機

類的生命週期( 7 個階段)

其中載入、驗證、準備、初始化和卸載這五個階段的順序是確定的。解析階段可以在初始化之後再開始(運行時綁定或動態繫結或晚期繫結)。

以下五種情況必須對類進行初始化(而載入、驗證、準備自然需要在此之前完成):

遇到 new、getstatic、putstatic 或 invokestatic 這 4 條位元組碼指令時沒初始化觸發初始化。使用場景:使用 new 關鍵字產生實體物件、讀取一個類的靜態欄位(被 final 修飾、已在編譯期把結果放入常量池的靜態欄位除外)、調用一個類的靜態方法。

使用 java.lang.reflect 包的方法對類進行反射調用的時候。

當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需先觸發其父類的初始化。

當虛擬機器啟動時,用戶需指定一個要載入的主類(包含 main() 方法的那個類),虛擬機器會先初始化這個主類。

當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制碼,並且這個方法控制碼所對應的類沒有進行過初始化,則需先觸發其初始化。

前面的五種方式是對一個類的主動引用,除此之外,所有引用類的方法都不會觸發初始化,佳作被動引用。舉幾個例子~

public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 1127;}public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); }}public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world!"}public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); /** * output : SuperClass init! * * 通過子類引用父類的靜態物件不會導致子類的初始化 * 只有直接定義這個欄位的類才會被初始化 */ SuperClass[] sca = new SuperClass[10]; /** * output : * * 通過陣列定義來引用類不會觸發此類的初始化 * 虛擬機器在運行時動態創建了一個陣列類 */ System.out.println(ConstClass.HELLOWORLD); /** * output : * * 常量在編譯階段會存入調用類的常量池當中,本質上並沒有直接引用到定義類常量的類, * 因此不會觸發定義常量的類的初始化。 * “hello world” 在編譯期常量傳播優化時已經存儲到 NotInitialization 常量池中了。 */ }}

6.2 類的載入過程

6.2.1 載入

通過一個類的全限定名來獲取定義次類的二進位流(ZIP 包、網路、運算生成、JSP 生成、資料庫讀取)。

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

在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法去這個類的各種資料的訪問入口。

陣列類的特殊性:陣列類本身不通過類載入器創建,它是由 Java 虛擬機器直接創建的。但陣列類與類載入器仍然有很密切的關係,因為陣列類的元素類型最終是要靠類載入器去創建的,陣列創建過程如下:

如果陣列的元件類型是參考類型,那就遞迴採用類載入載入。

如果陣列的元件類型不是參考類型,Java 虛擬機器會把陣列標記為引導類載入器關聯。

陣列類的可見性與他的元件類型的可見性一致,如果元件類型不是參考類型,那陣列類的可見性將默認為 public。

記憶體中實例的 java.lang.Class 物件存在方法區中。作為程式存取方法區中這些類型資料的外部介面。

載入階段與連接階段的部分內容是交叉進行的,但是開始時間保持先後順序。

6.2.2 驗證

是連接的第一步,確保 Class 檔的位元組流中包含的資訊符合當前虛擬機器要求。

檔案格式驗證

是否以魔數 0xCAFEBABE 開頭

主、次版本號是否在當前虛擬機器處理範圍之內

常量池的常量是否有不被支援常量的類型(檢查常量 tag 標誌)

指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量

CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的資料

Class 檔中各個部分集檔本身是否有被刪除的附加的其他資訊

……

只有通過這個階段的驗證後,位元組流才會進入記憶體的方法區進行存儲,所以後面 3 個驗證階段全部是基於方法區的存儲結構進行的,不再直接操作位元組流。

中繼資料驗證

這個類是否有父類(除 java.lang.Object 之外)

這個類的父類是否繼承了不允許被繼承的類(final 修飾的類)

如果這個類不是抽象類別,是否實現了其父類或介面之中要求實現的所有方法

類中的欄位、方法是否與父類產生矛盾(覆蓋父類 final 欄位、出現不符合規範的重載)

這一階段主要是對類的中繼資料資訊進行語義校驗,保證不存在不符合 Java 語言規範的中繼資料資訊。

位元組碼驗證

保證任意時刻運算元棧的資料類型與指令代碼序列都鞥配合工作(不會出現按照 long 類型讀一個 int 型資料)

保證跳轉指令不會跳轉到方法體以外的位元組碼指令上

保證方法體中的類型轉換是有效的(子類物件賦值給父類資料類型是安全的,反過來不合法的)

……

這是整個驗證過程中最複雜的一個階段,主要目的是通過資料流程和控制流分析,確定程式語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證校驗類的方法在運行時不會做出危害虛擬機器安全的事件。

符號引用驗證

符號引用中通過字元創描述的全限定名是否能找到對應的類

在指定類中是否存在符方法的欄位描述符以及簡單名稱所描述的方法和欄位

符號引用中的類、欄位、方法的訪問性(private、protected、public、default)是否可被當前類訪問

……

最後一個階段的校驗發生在迅疾將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,還有以上提及的內容。

符號引用的目的是確保解析動作能正常執行,如果無法通過符號引用驗證將拋出一個 java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

6.2.3 準備

這個階段正式為類分配記憶體並設置類變數初始值,記憶體在方法去中分配(含 static 修飾的變數不含執行個體變數)。

public static int value = 1127;

這句代碼在初始值設置之後為 0,因為這時候尚未開始執行任何 Java 方法。而把 value 賦值為 1127 的 putstatic 指令是程式被編譯後,存放於 clinit() 方法中,所以初始化階段才會對 value 進行賦值。

基底資料型別的零值

資料類型零值資料類型零值int0booleanfalselong0Lfloat0.0fshort(short) 0double0.0dchar' 接下來就是填充物件頭,把物件是哪個類的實例、如何才能找到類的中繼資料資訊、物件的雜湊碼、物件的 GC 分代年齡等資訊存入物件頭。

執行 new 指令後執行 init 方法後才算一份真正可用的物件創建完成。

1.2.2 物件的記憶體佈局

在 HotSpot 虛擬機器中,分為 3 塊區域:物件頭(Header)、實例資料(Instance Data)和對齊填充(Padding)

對象頭(Header):包含兩部分,第一部分用於存儲物件自身的運行時資料,如雜湊碼、GC 分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳記等,32 位虛擬機器占 32 bit,64 位虛擬機器占 64 bit。官方稱為 ‘Mark Word’。第二部分是類型指標,即物件指向它的類的中繼資料指標,虛擬機器通過這個指標確定這個物件是哪個類的實例。另外,如果是 Java 陣列,物件頭中還必須有一塊用於記錄陣列長度的資料,因為普通物件可以通過 Java 物件中繼資料確定大小,而陣列物件不可以。

實例資料(Instance Data):程式碼中所定義的各種類型的欄位內容(包含父類繼承下來的和子類中定義的)。

對齊填充(Padding):不是必然需要,主要是占位元,保證物件大小是某個位元組的整數倍。

1.2.3 物件的訪問定位

使用物件時,通過棧上的 reference 資料來操作堆上的具體物件。

通過控制碼訪問

Java 堆中會分配一塊記憶體作為控制碼池。reference 存儲的是控制碼位址。詳情見圖。

使用直接指標訪問

reference 中直接存儲物件位址

比較:使用控制碼的最大好處是 reference 中存儲的是穩定的控制碼位址,在物件移動(GC)是只改變實例資料指標位址,reference 自身不需要修改。直接指標訪問的最大好處是速度快,節省了一次指針定位的時間開銷。如果是物件頻繁 GC 那麼控制碼方法好,如果是物件頻繁訪問則直接指標訪問好。

1.3 實戰

// 待填

2. 垃圾回收器與記憶體分配策略

2.1 概述

程式計數器、虛擬機器棧、本地方法棧 3 個區域隨執行緒生滅(因為是執行緒私有),棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。而 Java 堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於運行期才知道那些物件會創建,這部分記憶體的分配和回收都是動態的,垃圾回收期所關注的就是這部分記憶體。

2.2 對象已死嗎?

在進行記憶體回收之前要做的事情就是判斷那些物件是‘死’的,哪些是‘活’的。

2.2.1 引用計數法

給物件添加一個引用計數器。但是難以解決迴圈引用問題。

從圖中可以看出,如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。則在 Java 堆當中的兩塊記憶體依然保持著互相引用無法回收。

2.2.2 可達性分析法

通過一系列的 ‘GC Roots’ 的對象作為起始點,從這些節點出發所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連的時候說明物件不可用。

可作為 GC Roots 的對象:

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

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

方法區中常量引用的物件

本地方法棧中 JNI(即一般說的 Native 方法) 引用的物件

2.2.3 再談引用

前面的兩種方式判斷存活時都與‘引用’有關。但是 JDK 1.2 之後,引用概念進行了擴充,下面具體介紹。

下面四種引用強度一次逐漸減弱

強引用

類似於 Object obj = new Object(); 創建的,只要強引用在就不回收。

軟引用

SoftReference 類實現軟引用。在系統要發生記憶體溢出異常之前,將會把這些物件列進回收範圍之中進行二次回收。

弱引用

WeakReference 類實現弱引用。物件只能生存到下一次垃圾收集之前。在垃圾收集器工作時,無論記憶體是否足夠都會回收掉只被弱引用關聯的物件。

虛引用

PhantomReference 類實現虛引用。無法通過虛引用獲取一個物件的實例,為一個物件設置虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

2.2.4 生存還是死亡

即使在可達性分析演算法中不可達的物件,也並非是“facebook”的,這時候它們暫時出於“緩刑”階段,一個物件的真正死亡至少要經歷兩次標記過程:如果物件在進行中可達性分析後發現沒有與 GC Roots 相連接的引用鏈,那他將會被第一次標記並且進行一次篩選,篩選條件是此物件是否有必要執行 finalize() 方法。當物件沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機器調用過,虛擬機器將這兩種情況都視為“沒有必要執行”。

如果這個物件被判定為有必要執行 finalize() 方法,那麼這個物件竟會放置在一個叫做 F-Queue 的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的 Finalizer 執行緒去執行它。這裡所謂的“執行”是指虛擬機器會出發這個方法,並不承諾或等待他運行結束。finalize() 方法是物件逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的對象進行第二次小規模的標記,如果物件要在 finalize() 中成功拯救自己 —— 只要重新與引用鏈上的任何一個物件簡歷關聯即可。

finalize() 方法只會被系統自動調用一次。

2.2.5 回收方法區

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空間,而永久代的垃圾收集效率遠低於此。

永久代垃圾回收主要兩部分內容:廢棄的常量和無用的類。

判斷廢棄常量:一般是判斷沒有該常量的引用。

判斷無用的類:要以下三個條件都滿足

該類所有的實例都已經回收,也就是 Java 堆中不存在該類的任何實例

載入該類的 ClassLoader 已經被回收

該類對應的 java.lang.Class 物件沒有任何地方唄引用,無法在任何地方通過反射訪問該類的方法

2.3 垃圾回收演算法

僅提供思路

2.3.1 標記 —— 清除演算法

直接標記清除就可。

兩個不足:

效率不高

空間會產生大量碎片

2.3.2 複製演算法

把空間分成兩塊,每次只對其中一塊進行 GC。當這塊記憶體使用完時,就將還存活的物件複製到另一塊上面。

解決前一種方法的不足,但是會造成空間利用率低下。因為大多數新生代物件都不會熬過第一次 GC。所以沒必要 1 : 1 劃分空間。可以分一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的物件一次性複製到另一塊 Survivor 上,最後清理 Eden 和 Survivor 空間。大小比例一般是 8 : 1 : 1,每次浪費 10% 的 Survivor 空間。但是這裡有一個問題就是如果存活的大於 10% 怎麼辦?這裡採用一種分配擔保策略:多出來的物件直接進入老年代。

2.3.3 標記-整理演算法

不同於針對新生代的複製演算法,針對老年代的特點,創建該演算法。主要是把存活物件移到記憶體的一端。

2.3.4 分代回收

根據存活物件劃分幾塊記憶體區,一般是分為新生代和老年代。然後根據各個年代的特點制定相應的回收演算法。

新生代

每次垃圾回收都有大量物件死去,只有少量存活,選用複製演算法比較合理。

老年代

老年代中物件存活率較高、沒有額外的空間分配對它進行擔保。所以必須使用 標記 —— 清除 或者 標記 —— 整理 演算法回收。

2.4 HotSpot 的演算法實現

// 待填

2.5 垃圾回收器

收集演算法是記憶體回收的理論,而垃圾回收器是記憶體回收的實踐。

說明:如果兩個收集器之間存在連線說明他們之間可以搭配使用。

2.5.1 Serial 收集器

這是一個單執行緒收集器。意味著它只會使用一個 CPU 或一條收集執行緒去完成收集工作,並且在進行垃圾回收時必須暫停其它所有的工作執行緒直到收集結束。

2.5.2 ParNew 收集器

可以認為是 Serial 收集器的多執行緒版本。

並行:Parallel

指多條垃圾收集執行緒並行工作,此時用戶執行緒處於等候狀態

併發:Concurrent

指用戶執行緒和垃圾回收執行緒同時執行(不一定是並行,有可能是交叉執行),使用者進程在運行,而垃圾回收執行緒在另一個 CPU 上運行。

2.5.3 Parallel Scavenge 收集器

這是一個新生代收集器,也是使用複製演算法實現,同時也是並行的多執行緒收集器。

CMS 等收集器的關注點是盡可能地縮短垃圾收集時用戶執行緒所停頓的時間,而 Parallel Scavenge 收集器的目的是達到一個可控制的輸送量(Throughput = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間))。

作為一個輸送量優先的收集器,虛擬機器會根據當前系統的運行情況收集性能監控資訊,動態調整停頓時間。這就是 GC 的自我調整調整策略(GC Ergonomics)。

2.5.4 Serial Old 收集器

收集器的老年代版本,單執行緒,使用 標記 —— 整理。

2.5.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多執行緒,使用 標記 —— 整理

2.5.6 CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標的收集器。基於 標記 —— 清除 演算法實現。

運作步驟:

初始標記(CMS initial mark):標記 GC Roots 能直接關聯到的物件

併發標記(CMS concurrent mark):進行 GC Roots Tracing

重新標記(CMS remark):修正併發標記期間的變動部分

併發清除(CMS concurrent sweep)

缺點:對 CPU 資源敏感、無法收集浮動垃圾、標記 —— 清除 演算法帶來的空間碎片

2.5.7 G1 收集器

面向服務端的垃圾回收器。

優點:並行與併發、分代收集、空間整合、可預測停頓。

運作步驟:

初始標記(Initial Marking)

併發標記(Concurrent Marking)

最終標記(Final Marking)

篩選回收(Live Data Counting and Evacuation)

2.6 記憶體分配與回收策略

2.6.1 對象優先在 Eden 分配

對象主要分配在新生代的 Eden 區上,如果啟動了本地執行緒分配緩衝區,將執行緒優先在 (TLAB) 上分配。少數情況會直接分配在老年代中。

一般來說 Java 堆的記憶體模型如下圖所示:

新生代 GC (Minor GC)

發生在新生代的垃圾回收動作,頻繁,速度快。

老年代 GC (Major GC / Full GC)

發生在老年代的垃圾回收動作,出現了 Major GC 經常會伴隨至少一次 Minor GC(非絕對)。Major GC 的速度一般會比 Minor GC 慢十倍以上。

2.6.2 大物件直接進入老年代

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

2.6.4 動態物件年齡判定

2.6.5 空間分配擔保

3. Java 記憶體模型與執行緒

3.1 Java 記憶體模型

遮罩掉各種硬體和作業系統的記憶體訪問差異。

3.1.1 主記憶體和工作記憶體之間的交互

操作作用物件解釋lock主記憶體把一個變數標識為一條執行緒獨佔的狀態unlock主記憶體把一個處於鎖定狀態的變數釋放出來,釋放後才可被其他執行緒鎖定read主記憶體把一個變數的值從主記憶體傳輸到執行緒工作記憶體中,以便 load 操作使用load工作記憶體把 read 操作從主記憶體中得到的變數值放入工作記憶體中use工作記憶體把工作記憶體中一個變數的值傳遞給執行引擎,

每當虛擬機遇到一個需要使用到變數值的位元組碼指令時將會執行這個操作

assign工作記憶體把一個從執行引擎接收到的值賦接收到的值賦給工作記憶體的變數,

每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作

store工作記憶體把工作記憶體中的一個變數的值傳送到主記憶體中,以便 write 操作write工作記憶體把 store 操作從工作記憶體中得到的變數的值放入主記憶體的變數中

3.1.2 對於 volatile 型變數的特殊規則

關鍵字 volatile 是 Java 虛擬機器提供的最羽量級的同步機制。

一個變數被定義為 volatile 的特性:

保證此變數對所有執行緒的可見性。但是操作並非原子操作,併發情況下不安全。

如果不符合 運算結果並不依賴變數當前值,或者能夠確保只有單一的執行緒修改變數的值 和 變數不需要與其他的狀態變數共同參與不變約束 就要通過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。

禁止指令重排序優化。

通過插入記憶體屏障保證一致性。

3.1.3 對於 long 和 double 型變數的特殊規則

Java 要求對於主記憶體和工作記憶體之間的八個操作都是原子性的,但是對於 64 位的資料類型,有一條寬鬆的規定:允許虛擬機器將沒有被 volatile 修飾的 64 位元資料的讀寫操作劃分為兩次 32 位的操作來進行,即允許虛擬機器實現選擇可以不保證 64 位資料類型的 load、store、read 和 write 這 4 個操作的原子性。這就是 long 和 double 的非原子性協定。

3.1.4 原子性、可見性與有序性

回顧下併發下應該注意操作的那些特性是什麼,同時加深理解。

原子性(Atomicity)

由 Java 記憶體模型來直接保證的原子性變數操作包括 read、load、assign、use、store 和 write。大致可以認為基底資料型別的操作是原子性的。同時 lock 和 unlock 可以保證更大範圍操作的原子性。而 synchronize 同步塊操作的原子性是用更高層次的位元組碼指令 monitorenter 和 monitorexit 來隱式操作的。

可見性(Visibility)

是指當一個執行緒修改了共用變數的值,其他執行緒也能夠立即得知這個通知。主要操作細節就是修改值後將值同步至主記憶體(volatile 值使用前都會從主記憶體刷新),除了 volatile 還有 synchronize 和 final 可以保證可見性。同步塊的可見性是由“對一個變數執行 unlock 操作之前,必須先把此變數同步會主記憶體中( store 、write 操作)”這條規則獲得。而 final 可見性是指:被 final 修飾的欄位在構造器中一旦完成,並且構造器沒有把 “this” 的引用傳遞出去( this 引用逃逸是一件很危險的事情,其他執行緒有可能通過這個引用訪問到“初始化了一半”的物件),那在其他執行緒中就能看見 final 欄位的值。

有序性(Ordering)

如果在被執行緒內觀察,所有操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句指“執行緒內表現為串列的語義”,後半句是指“指令重排”現象和“工作記憶體與主記憶體同步延遲”現象。Java 語言通過 volatile 和 synchronize 兩個關鍵字來保證執行緒之間操作的有序性。volatile 自身就禁止指令重排,而 synchronize 則是由“一個變數在同一時刻指允許一條執行緒對其進行 lock 操作”這條規則獲得,這條規則決定了持有同一個鎖的兩個同步塊只能串列的進入。

3.1.5 先行發生原則

也就是 happens-before 原則。這個原則是判斷資料是否存在競爭、執行緒是否安全的主要依據。先行發生是 Java 記憶體模型中定義的兩項操作之間的偏序關係。

天然的先行發生關係

規則解釋程式次序規則在一個執行緒內,代碼按照書寫的控制流循序執行管程鎖定規則一個 unlock 操作先行發生於後面對同一個鎖的 lock 操作volatile 變數規則volatile 變數的寫操作先行發生於後面對這個變數的讀操作執行緒啟動規則Thread 物件的 start() 方法先行發生於此執行緒的每一個動作執行緒終止規則執行緒中所有的操作都先行發生於對此執行緒的終止檢測

(通過 Thread.join() 方法結束、 Thread.isAlive() 的返回值檢測)

執行緒中斷規則對執行緒 interrupt() 方法調用優先發生於被中斷執行緒的代碼檢測到中斷事件的發生

(通過 Thread.interrupted() 方法檢測)

物件終結規則一個物件的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始傳遞性如果操作 A 先於 操作 B 發生,操作 B 先於 操作 C 發生,那麼操作 A 先於 操作 C

3.2 Java 與執行緒

3.2.1 執行緒的實現

使用內核執行緒實現

直接由作業系統內核支援的執行緒,這種執行緒由內核完成切換。程式一般不會直接去使用內核執行緒,而是去使用內核執行緒的一種高級介面 —— 羽量級進程(LWP),羽量級進程就是我們通常意義上所講的執行緒,每個羽量級進程都有一個內核級執行緒支持。

使用用戶執行緒實現

廣義上來說,只要不是內核執行緒就可以認為是用戶執行緒,因此可以認為羽量級進程也屬於使用者執行緒。狹義上說是完全建立在用戶空間的執行緒庫上的並且內核系統不可感知的。

使用使用者執行緒夾加羽量級進程混合實現

直接看圖

Java 執行緒實現

平臺不同實現方式不同,可以認為是一條 Java 執行緒映射到一條羽量級進程。

3.2.2 Java 執行緒調度

協同式執行緒調度

執行緒執行時間由執行緒自身控制,實現簡單,切換執行緒自己可知,所以基本沒有執行緒同步問題。壞處是執行時間不可控,容易阻塞。

搶佔式執行緒調度

每個執行緒由系統來分配執行時間。

3.2.3 狀態轉換

五種狀態:

新建(new)

創建後尚未啟動的執行緒。

運行(Runable)

Runable 包括了作業系統執行緒狀態中的 Running 和 Ready,也就是出於此狀態的執行緒有可能正在執行,也有可能正在等待 CPU 為他分配時間。

無限期等待(Waiting)

出於這種狀態的執行緒不會被 CPU 分配時間,它們要等其他執行緒顯示的喚醒。

以下方法會然執行緒進入無限期等候狀態:

1.沒有設置 Timeout 參數的 Object.wait() 方法。

2.沒有設置 Timeout 參數的 Thread.join() 方法。

3.LookSupport.park() 方法。

限期等待(Timed Waiting)

處於這種狀態的執行緒也不會分配時間,不過無需等待配其他執行緒顯示地喚醒,在一定時間後他們會由系統自動喚醒。

以下方法會讓執行緒進入限期等候狀態:

1.Thread.sleep() 方法。

2.設置了 Timeout 參數的 Object.wait() 方法。

3.設置了 Timeout 參數的 Thread.join() 方法。

4.LockSupport.parkNanos() 方法。

5.LockSupport.parkUntil() 方法。

阻塞(Blocked)

執行緒被阻塞了,“阻塞狀態”和“等候狀態”的區別是:“阻塞狀態”在等待著獲取一個排他鎖,這個時間將在另外一個執行緒放棄這個鎖的時候發生;而“等候狀態”則是在等待一段時間,或者喚醒動作的發生。在程式等待進入同步區域的時候,執行緒將進入這種狀態。

結束(Terminated)

已終止執行緒的執行緒狀態。

4. 執行緒安全與鎖優化

// 待填

5. 類檔結構

// 待填

有點懶了。。。先貼幾個網址吧。

1. Official:The class File Format

2.亦山: 《Java虛擬機器原理圖解》 1.1、class檔基本組織結構

6. 虛擬機器類載入機制

虛擬機器把描述類的資料從 Class 檔載入到記憶體,並對資料進行校驗、裝換解析和初始化,最終形成可以被虛擬機器直接使用的 Java 類型。

在 Java 語言中,類型的載入、連接和初始化過程都是在程式運行期間完成的。

6.1 類載入時機

類的生命週期( 7 個階段)

其中載入、驗證、準備、初始化和卸載這五個階段的順序是確定的。解析階段可以在初始化之後再開始(運行時綁定或動態繫結或晚期繫結)。

以下五種情況必須對類進行初始化(而載入、驗證、準備自然需要在此之前完成):

遇到 new、getstatic、putstatic 或 invokestatic 這 4 條位元組碼指令時沒初始化觸發初始化。使用場景:使用 new 關鍵字產生實體物件、讀取一個類的靜態欄位(被 final 修飾、已在編譯期把結果放入常量池的靜態欄位除外)、調用一個類的靜態方法。

使用 java.lang.reflect 包的方法對類進行反射調用的時候。

當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需先觸發其父類的初始化。

當虛擬機器啟動時,用戶需指定一個要載入的主類(包含 main() 方法的那個類),虛擬機器會先初始化這個主類。

當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法控制碼,並且這個方法控制碼所對應的類沒有進行過初始化,則需先觸發其初始化。

前面的五種方式是對一個類的主動引用,除此之外,所有引用類的方法都不會觸發初始化,佳作被動引用。舉幾個例子~

public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 1127;}public class SubClass extends SuperClass { static { System.out.println("SubClass init!"); }}public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world!"}public class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); /** * output : SuperClass init! * * 通過子類引用父類的靜態物件不會導致子類的初始化 * 只有直接定義這個欄位的類才會被初始化 */ SuperClass[] sca = new SuperClass[10]; /** * output : * * 通過陣列定義來引用類不會觸發此類的初始化 * 虛擬機器在運行時動態創建了一個陣列類 */ System.out.println(ConstClass.HELLOWORLD); /** * output : * * 常量在編譯階段會存入調用類的常量池當中,本質上並沒有直接引用到定義類常量的類, * 因此不會觸發定義常量的類的初始化。 * “hello world” 在編譯期常量傳播優化時已經存儲到 NotInitialization 常量池中了。 */ }}

6.2 類的載入過程

6.2.1 載入

通過一個類的全限定名來獲取定義次類的二進位流(ZIP 包、網路、運算生成、JSP 生成、資料庫讀取)。

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

在記憶體中生成一個代表這個類的 java.lang.Class 物件,作為方法去這個類的各種資料的訪問入口。

陣列類的特殊性:陣列類本身不通過類載入器創建,它是由 Java 虛擬機器直接創建的。但陣列類與類載入器仍然有很密切的關係,因為陣列類的元素類型最終是要靠類載入器去創建的,陣列創建過程如下:

如果陣列的元件類型是參考類型,那就遞迴採用類載入載入。

如果陣列的元件類型不是參考類型,Java 虛擬機器會把陣列標記為引導類載入器關聯。

陣列類的可見性與他的元件類型的可見性一致,如果元件類型不是參考類型,那陣列類的可見性將默認為 public。

記憶體中實例的 java.lang.Class 物件存在方法區中。作為程式存取方法區中這些類型資料的外部介面。

載入階段與連接階段的部分內容是交叉進行的,但是開始時間保持先後順序。

6.2.2 驗證

是連接的第一步,確保 Class 檔的位元組流中包含的資訊符合當前虛擬機器要求。

檔案格式驗證

是否以魔數 0xCAFEBABE 開頭

主、次版本號是否在當前虛擬機器處理範圍之內

常量池的常量是否有不被支援常量的類型(檢查常量 tag 標誌)

指向常量的各種索引值中是否有指向不存在的常量或不符合類型的常量

CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的資料

Class 檔中各個部分集檔本身是否有被刪除的附加的其他資訊

……

只有通過這個階段的驗證後,位元組流才會進入記憶體的方法區進行存儲,所以後面 3 個驗證階段全部是基於方法區的存儲結構進行的,不再直接操作位元組流。

中繼資料驗證

這個類是否有父類(除 java.lang.Object 之外)

這個類的父類是否繼承了不允許被繼承的類(final 修飾的類)

如果這個類不是抽象類別,是否實現了其父類或介面之中要求實現的所有方法

類中的欄位、方法是否與父類產生矛盾(覆蓋父類 final 欄位、出現不符合規範的重載)

這一階段主要是對類的中繼資料資訊進行語義校驗,保證不存在不符合 Java 語言規範的中繼資料資訊。

位元組碼驗證

保證任意時刻運算元棧的資料類型與指令代碼序列都鞥配合工作(不會出現按照 long 類型讀一個 int 型資料)

保證跳轉指令不會跳轉到方法體以外的位元組碼指令上

保證方法體中的類型轉換是有效的(子類物件賦值給父類資料類型是安全的,反過來不合法的)

……

這是整個驗證過程中最複雜的一個階段,主要目的是通過資料流程和控制流分析,確定程式語義是合法的、符合邏輯的。這個階段對類的方法體進行校驗分析,保證校驗類的方法在運行時不會做出危害虛擬機器安全的事件。

符號引用驗證

符號引用中通過字元創描述的全限定名是否能找到對應的類

在指定類中是否存在符方法的欄位描述符以及簡單名稱所描述的方法和欄位

符號引用中的類、欄位、方法的訪問性(private、protected、public、default)是否可被當前類訪問

……

最後一個階段的校驗發生在迅疾將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,還有以上提及的內容。

符號引用的目的是確保解析動作能正常執行,如果無法通過符號引用驗證將拋出一個 java.lang.IncompatibleClass.ChangeError 異常的子類。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

6.2.3 準備

這個階段正式為類分配記憶體並設置類變數初始值,記憶體在方法去中分配(含 static 修飾的變數不含執行個體變數)。

public static int value = 1127;

這句代碼在初始值設置之後為 0,因為這時候尚未開始執行任何 Java 方法。而把 value 賦值為 1127 的 putstatic 指令是程式被編譯後,存放於 clinit() 方法中,所以初始化階段才會對 value 進行賦值。

基底資料型別的零值

資料類型零值資料類型零值int0booleanfalselong0Lfloat0.0fshort(short) 0double0.0dchar'
同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示