秋招開始了,讀《深刻理解JVM虛擬機》總結

重讀 JVM

秋招開始了,前面因爲作別的事耽誤了半個月,之前學的東西不用就很容易忘記。因此,此次從新閱讀《深刻理解 JVM 虛擬機》時,想作一個記錄。將碎片的知識整合,方便本身之後閱讀,同時也和你們一塊兒分享。內容中會添加我本身的理解,其中若是有錯誤,歡迎你們指正。html

1. Java 內存區域與內存溢出異常

1.1 運行時數據區域

根據《Java 虛擬機規範(Java SE 7 版)》規定,Java 虛擬機所管理的內存以下圖所示。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 對象元數據肯定大小,而數組對象不能夠。點擊免費「領取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 方法) 引用的對象
  • 點擊免費「領取Java架構資料

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) 收集器是一種以獲取最短回收停頓時間爲目標的收集器。基於 標記 —— 清除 算法實現。

運做步驟: 

  1. 初始標記(CMS initial mark):標記 GC Roots 能直接關聯到的對象
  2. 併發標記(CMS concurrent mark):進行 GC Roots Tracing
  3. 從新標記(CMS remark):修正併發標記期間的變更部分
  4. 併發清除(CMS concurrent sweep)

 

 

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

2.5.7 G1 收集器

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

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

運做步驟: 

  1. 初始標記(Initial Marking)
  2. 併發標記(Concurrent Marking)
  3. 最終標記(Final Marking)
  4. 篩選回收(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 空間分配擔保

點擊免費「領取Java架構資料

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 的特性:

  1. 保證此變量對全部線程的可見性。可是操做並不是原子操做,併發狀況下不安全。

若是不符合 運算結果並不依賴變量當前值,或者可以確保只有單一的線程修改變量的值 和 變量不須要與其餘的狀態變量共同參與不變約束 就要經過加鎖(使用 synchronize 或 java.util.concurrent 中的原子類)來保證原子性。

  1. 禁止指令重排序優化。

經過插入內存屏障保證一致性。

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.一、class文件基本組織結構

點擊免費「領取Java架構資料

6. 虛擬機類加載機制

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

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

6.1 類加載時機

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

 

 

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

如下五種狀況必須對類進行初始化(而加載、驗證、準備天然須要在此以前完成):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時沒初始化觸發初始化。使用場景:使用 new 關鍵字實例化對象、讀取一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)、調用一個類的靜態方法。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候。
  3. 當初始化一個類的時候,若是發現其父類尚未進行初始化,則需先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需指定一個要加載的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用 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 加載

  1. 經過一個類的全限定名來獲取定義次類的二進制流(ZIP 包、網絡、運算生成、JSP 生成、數據庫讀取)。
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法去這個類的各類數據的訪問入口。

數組類的特殊性:數組類自己不經過類加載器建立,它是由 Java 虛擬機直接建立的。但數組類與類加載器仍然有很密切的關係,由於數組類的元素類型最終是要靠類加載器去建立的,數組建立過程以下:

  1. 若是數組的組件類型是引用類型,那就遞歸採用類加載加載。
  2. 若是數組的組件類型不是引用類型,Java 虛擬機會把數組標記爲引導類加載器關聯。
  3. 數組類的可見性與他的組件類型的可見性一致,若是組件類型不是引用類型,那數組類的可見性將默認爲 public。

內存中實例的 java.lang.Class 對象存在方法區中。做爲程序訪問方法區中這些類型數據的外部接口。
加載階段與鏈接階段的部份內容是交叉進行的,可是開始時間保持前後順序。

6.2.2 驗證

是鏈接的第一步,確保 Class 文件的字節流中包含的信息符合當前虛擬機要求。

文件格式驗證

  1. 是否以魔數 0xCAFEBABE 開頭
  2. 主、次版本號是否在當前虛擬機處理範圍以內
  3. 常量池的常量是否有不被支持常量的類型(檢查常量 tag 標誌)
  4. 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量
  5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 編碼的數據
  6. Class 文件中各個部分集文件自己是否有被刪除的附加的其餘信息
  7. ……

只有經過這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,因此後面 3 個驗證階段所有是基於方法區的存儲結構進行的,再也不直接操做字節流。

元數據驗證

  1. 這個類是否有父類(除 java.lang.Object 以外)
  2. 這個類的父類是否繼承了不容許被繼承的類(final 修飾的類)
  3. 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法
  4. 類中的字段、方法是否與父類產生矛盾(覆蓋父類 final 字段、出現不符合規範的重載)

這一階段主要是對類的元數據信息進行語義校驗,保證不存在不符合 Java 語言規範的元數據信息。

字節碼驗證

  1. 保證任意時刻操做數棧的數據類型與指令代碼序列都鞥配合工做(不會出現按照 long 類型讀一個 int 型數據)
  2. 保證跳轉指令不會跳轉到方法體之外的字節碼指令上
  3. 保證方法體中的類型轉換是有效的(子類對象賦值給父類數據類型是安全的,反過來不合法的)
  4. ……

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

符號引用驗證

  1. 符號引用中經過字符創描述的全限定名是否能找到對應的類
  2. 在指定類中是否存在符方法的字段描述符以及簡單名稱所描述的方法和字段
  3. 符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問
  4. ……

最後一個階段的校驗發生在迅疾將符號引用轉化爲直接引用的時候,這個轉化動做將在鏈接的第三階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用)的信息進行匹配性校驗,還有以上說起的內容。
符號引用的目的是確保解析動做能正常執行,若是沒法經過符號引用驗證將拋出一個 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 進行賦值。

基本數據類型的零值

數據類型 零值 數據類型 零值
int 0 boolean false
long 0L float 0.0f
short (short) 0 double 0.0d
char '\u0000' reference null
byte (byte) 0  

特殊狀況:若是類字段的字段屬性表中存在 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 1127。

6.2.4 解析

這個階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

  1. 符號引用
    符號引用以一組符號來描述所引用的目標,符號可使任何形式的字面量。
  2. 直接引用
    直接引用可使直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用和迅疾的內存佈局實現有關

解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行,分別對應於常量池的 7 中常量類型。

6.2.5 初始化

前面過程都是以虛擬機主導,而初始化階段開始執行類中的 Java 代碼。

6.3 類加載器

經過一個類的全限定名來獲取描述此類的二進制字節流。

6.3.1 雙親委派模型

從 Java 虛擬機角度講,只存在兩種類加載器:一種是啓動類加載器(C++ 實現,是虛擬機的一部分);另外一種是其餘全部類的加載器(Java 實現,獨立於虛擬機外部且全繼承自 java.lang.ClassLoader)

  1. 啓動類加載器
    加載 lib 下或被 -Xbootclasspath 路徑下的類

  2. 擴展類加載器
    加載 lib/ext 或者被 java.ext.dirs 系統變量所指定的路徑下的類

  3. 引用程序類加載器
    ClassLoader負責,加載用戶路徑上所指定的類庫。

 


除頂層啓動類加載器以外,其餘都有本身的父類加載器。
工做過程:若是一個類加載器收到一個類加載的請求,它首先不會本身加載,而是把這個請求委派給父類加載器。只有父類沒法完成時子類纔會嘗試加載。

 

6.3.2 破壞雙親委派模型

keyword:線程上下文加載器(Thread Context ClassLoader)

相關文章
相關標籤/搜索