JVM相關知識點總結

1445701567發表於2020-11-28

1、執行時資料區域

本地方法棧、虛擬機器棧、程式計數器
方法區、堆

1.1、執行緒私有

1.1.1、程式計數器

當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器通過改變這個值來選取下一個要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復的功能需要程式計數器來完成,java方法這個計數器才有值,native方法這個計數器是空的。唯一一個沒有任何OutOfMemoryError情況的區域

1.1.2、虛擬機器棧

 執行緒私有,生命和執行緒相同,每個方法執行的同時都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結(每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線(Dynamic Linking))、方法出口等資訊,每一個方法從呼叫直到執行完畢的過程,就對應一個棧幀從虛擬機器中入棧到出棧的過程,棧的大小和具體的jvm實現有關。

  • StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度。
  • OutOfMemoryError:如果虛擬機器棧可以動態擴充套件,而擴充套件是無法申請到足夠的記憶體

1.1.3、本地方法棧

和虛擬機器棧作用一樣,只不過方法棧為虛擬機器使用到的native方法服務,Hotspot沒有此塊區域,和虛擬機器棧放一起

  • StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度。
  • OutOfMemoryError:如果虛擬機器棧可以動態擴充套件,而擴充套件是無法申請到足夠的記憶體

1.2、執行緒共享
1.2.1、 堆

用於存放物件例項,一般是java虛擬機器所管理的最大的一塊區域,在虛擬機器啟動時建立。堆還可以分為新生代和老年代,再細緻一點還有Eden區、From Survivior區、To Servivor區

  • OutOfMemoryError:如果堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,丟擲該異常。

1.2.2、方法區

用於儲存虛擬機器載入的類的資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。虛擬機器規範中把它描述為堆的一個邏輯部分,在分代收集演算法角度,Hotspot中方法區≈永久代,jdk7之後Hotspot就沒有永久代這個概念了,會採用Native Memory 來實現方法區的規劃了

1.2.3、執行時常量池

方法區的一部分。class檔案中除了有類的版本資訊、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池,用於存放編譯期間生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中,另外翻譯出來的直接引用也會儲存在這個區域中。另外一個特點是動態性,java並不要求常量就一定要在編譯期間才產生,執行期間也可以在這個區域中放入新內容,
String.inten()方法就是這個特性的應用
    記憶體有限,無法申請時丟擲 OutOfMemoryError。

1.3、直接記憶體

並不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。但這部分記憶體也被頻繁的使用,而且也可能導致記憶體溢位。jdk1.4增加的NIO,引入了一個基於管道和緩衝區的I/O方式,他可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作

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

2、物件建立

方式:克隆、反序列化、反射、new關鍵字

2.1、虛擬機器建立物件過程:

  1. 虛擬機器遇到一條new指令,首先去檢查這個指令的引數是否在常量池中定位一個類的符號引用,並檢查這個符號引用代表的類是否已被載入、解析和初始化。如果沒有,那必須先執行累的初始化過程。
  2. 類載入檢查通過後,虛擬機器將為新生物件分配記憶體。物件所需記憶體大小在類載入完成後便可以完全確定,為物件分配空間無非是從java堆中劃分一個快確定大小的記憶體而已。注意兩個問題:
    1. 如果記憶體是規整的,虛擬機器會採用的是指標碰撞發來為物件分配記憶體。意思是所有用過的記憶體在一邊,空閒的記憶體在另一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標像空閒那邊挪動一段和物件大小相等的距離罷了。如果垃圾收集器選擇的是Serial、ParNew這種基於壓縮演算法的,虛擬機器採用這種分配方式;如果記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,那麼虛擬機器將採用空閒列表法來為物件分配記憶體。虛擬機器維護一個列表,記錄上哪些記憶體塊是可用的,在分配到時候從列表中找到一個足夠大的空間劃分給物件例項,並更新列表上的內容。如果垃圾收集齊選擇的是CMS這種基於標記-清除演算法的,虛擬機器會採用這種分配方式
    2. 另外一個問題是及時保證new物件時候的執行緒安全性。虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性和TLAB兩種方式解決這個問題;TLAB的全稱是Thread Local Allocation Buffer,即執行緒本地分配快取區,這是一個執行緒專用的記憶體分配區域。 
  3. 記憶體分配結束,虛擬機器將分配的記憶體空間初始化為零(不包括物件頭)。這一步保證了物件的例項欄位在java程式碼中可以不用賦初始值就可以直接使用,程式能訪問到這些欄位的資料型別所對應的零值
  4. 對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到了類的後設資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊,這些資料存放在物件頭中
  5. 執行<init>方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可以用的物件就算完全產生了

2.2、物件記憶體佈局

物件頭(Head)、例項資料(Instance Data)、對齊填充(Padding)

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

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

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

2.3、物件定位方式

java程式需要通過棧上的reference(引用)資料來操作堆上的具體物件

2.3.1、物件訪問方式主流有兩種:

  1. 控制程式碼訪問:java堆中劃分出一塊控制程式碼池,reference 儲存的是控制程式碼地址,obj指向的是物件的控制程式碼地址,控制程式碼中則包含了類資料的地址和例項資料地址
  2. 指標訪問:物件中儲存所有的例項資料和資料地址,reference 中直接儲存物件地址,obj指向的是這個物件

2.3.2、比較

使用控制程式碼的最大好處是reference中儲存的是穩定的控制程式碼地址,在物件移動(GC)是隻改變例項資料指標地址,reference自身不需要修改。直接指標訪問的最大好處是速度快,節省了一次指標定位的時間開銷。物件訪問頻繁則指標訪問好,物件GC頻繁則控制程式碼好

3、垃圾回收器與記憶體分配策略

3.1、判斷物件是否可回收

引用計數法、 可達性分析法 GC Roots

3.1.1、引用計數法

給物件新增一個引用計數器,每當一個地方引用這個物件時,計數器+1;當引用失效時,計數器-1.任何時刻計數器為零的物件就是不可能在被使用的。但java未採用,因為難以解決迴圈引用問題

3.1.2、可達性分析法

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

  • 虛擬機器棧(棧幀中的本地變數表)中的引用物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧JNI引用的物件

4、四種引用狀態(強引用、軟引用、弱引用、虛引用)

當記憶體還充足時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是非常緊張,則拋棄這些物件

4.1、強引用

只要強引用還存在,垃圾收集器永遠不會回收掉被引用物件

4.2、軟引用(SoftReference)

描述有些還有用但並非必須的物件。在系統將要發生記憶體溢位異常之前,將會把在這些物件進回收範圍進行二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲異常

4.3、弱引用(WeakReference)

描述非必須物件。被弱引用關聯的物件只能生存到下次垃圾回收之前,垃圾收集器工作之後,無論當前記憶體是否足夠,都會回收掉只有被弱引用關聯的物件。

4.4、虛引用(PhantomReference)

這個引用存在的唯一目的就是在這個物件收集器回收時收到一個系統通知,被虛引用關聯的物件,和其生存時間完全沒關係

5、真正GC之前

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

反之,如果這個物件覆蓋finalize()方法並且finalize()方法沒被虛擬機器呼叫過,那麼這個物件就會放置在一個叫F-Queue的佇列,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行他。這裡所謂的“執行”是指虛擬機器會觸發這個方法,並不承諾或等待他執行結束。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行二次小規模標記,如果物件在finalize()方法中成功解救自己--只要重新和引用鏈上的任何一個物件建立聯絡即可(finalize()方法只會被系統自動呼叫一次)

6、回收方法區

永久代垃圾回收主要兩部分內容:廢棄的常量和無用的類
判斷廢棄常量:一般是判斷有沒有該常量的引用
判斷無用的類:

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

大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛擬機器具備類的解除安裝功能,以確保方法區不溢位

7、垃圾回收演算法

7.1、標記-清除演算法(Mark-Sweep)

這是最基礎的演算法,標記-清除演算法就如同它的名字一樣,分為標記和清除兩個階段:首先標記出所有需要回收的物件,標記完成後統一回收所有被標記的物件。

這種演算法的不足之處主要體現在效率和空間上,從效率的角度講,標記和清除兩個階段的效率都不高;從空間的角度講,標記清除後會產生大量不連續的記憶體碎片,記憶體碎片太多可能導致以後程式執行過程中需要分配大物件時,無法找到足夠的連續記憶體而不得不提前觸發一次垃圾收集動作。

7.2、複製演算法(copying)    

複製演算法是為了解決效率問題而出現的,它將可用記憶體分為兩塊,每一次只使用其中一塊,當這一塊記憶體使用完,就將還存活的物件複製到另一塊上面,然後再把已經使用過的記憶體空間一次清理掉。這樣每次只需要對整個搬去進行記憶體回收,記憶體分配時也不需要考慮記憶體碎片等複雜情況,只需要移動指標,按照順序分配即可。複製演算法的執行過程如圖:  

不過這種演算法有個缺點,記憶體縮小為原來的一半,這樣的代價也太高了。現在的商用虛擬機器都採用這種演算法來回收新生代,不過研究表明1:1的比例並不科學,因此新生代的記憶體被劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor中還存活的物件一次性複製到另一塊Survivor空間上,最後清理掉Eden 和剛才使用過的Survivor空間。HotSpot虛擬機器預設的Eden和Survivor的比例為8:1,意思是每次新生代中可用記憶體為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的存活物件,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)

7.3、標記-整理演算法(Mark-Compact)

複製演算法在物件存活率較高的場景下進行大量的複製操作,效率很低。萬一物件100%存活,那麼需要有額外的空間進行分配擔保。老年代都是不易被回收的物件,物件存活率高,因此一般不能直接選用複製演算法。

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

7.4、分代收集演算法

根據物件的生命週期不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適當的收集演算法。新生代,大批物件死去,少量物件存活,使用複製演算法,複製成本低;老年代物件存活率高、沒有額外空間進行分配擔保的,採用標記清除演算法或標記整理演算法。

7.5、垃圾回收器

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

7.5.1、Serial 收集器

單執行緒收集器,
 

7.5.2、ParNew收集器

是Serial收集器的多執行緒版本
 

7.5.3、Parallel Scavenge 收集器

新生代收集器,複製演算法,並行的多執行緒收集器
CMS 等收集器的關注點是儘可能地縮短垃圾收集時使用者執行緒所停頓的時間,而 Parallel Scavenge 收集器的目的是達到一個可控制的吞吐量(Throughput = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間))。
作為一個吞吐量優先的收集器,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整停頓時間。這就是 GC 的自適應調整策略(GC Ergonomics)。

7.5.4、Serial Old 收集器

Serial 收集器老年代版本,單執行緒,使用標記-整理演算法

7.5.5、Parallel Old 收集器

Parallel Scavenge 老年代版本,多執行緒,使用標記整理演算法

7.5.6、CMS收集器

以獲取最短回收停頓時間為目的的收集器,基於標記-清除演算法
缺點:對 CPU 資源敏感、無法收集浮動垃圾、標記 —— 清除 演算法帶來的空間碎片
運作步驟:
初始標記(CMS initial mark):標記GC Roots能直接關聯到的物件
併發標記(CMS concurrent mark):進行GC Roots Tracing
重新標記(CMS remark):修正並標記期間的變動部分
併發清除(CMS concurrent sweep)

7.5.7、G1收集器

面向服務端的,並行併發、分代收集、空間整合、可預測停頓
運作步驟:
初始標記(Initial Marking)
併發標記(Concurrent Marking)
最終標記(Final Marking)
篩選回收(Live Data Counting and Evacuation)

8、記憶體分配與回收策略

TLAB(Thread Local Allcation Buffer,本地執行緒分配快取)。記憶體分配的動作,可以按照執行緒劃分在不同空間中進行,即每個執行緒在Java堆中預先分配一塊記憶體,稱為本店執行緒分配快取。哪個執行緒需要分配記憶體就在那個執行緒TLAB上分配。這麼做的目的之一,也是為了併發建立一個物件時,保證建立物件的執行緒安全性。TLAB比較小,直接在TLAB中分配記憶體的方式稱為快速分配方式,而TLAB大小不夠,導致記憶體別分配在Eden區的記憶體分配方式稱為慢速分配方式

8.1、物件優先分配在Eden區上

物件通常在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC,與Minor GC 對應的是Major GC、Full GC

  • Minor GC:只發生在新生代的垃圾收集動作,非常頻繁,速度較快
  • Major GC:指發生在老年代的GC,出現Major GC,經常會伴隨一次Minor GC,Minor GC同時也會引起Major GC,一般在GC日誌中統稱為GC,不頻繁
  • Full GC:指發生在老年代和新生代的GC,速度很慢,需要Stop The World

8.2、大物件直接進入老年代

需要大量連續記憶體空間的Java物件成為大物件,大物件的出現會導致提前出發垃圾收集以獲取更大的連續的空間進行大物件的分配,虛擬機器提供了-XX:PretenureSizeThreshold引數來設定大物件的閾值,超過閾值的物件直接分配到老年代。

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

每個物件有一個物件年齡計數器,與前面物件的儲存佈局中的GC分代年齡對應。物件出生在Eden區。經過一次Minor GC後仍然存活,並且被Survivor容納,設定年齡為1,物件在Survivor區每經過一次Minor GC,年齡就加1,當年齡達到一定程度(預設15),就晉升到老年代,虛擬機器提供了-XX:MaxTenuringThreshold來進行設定

8.4、動態物件年齡分配

物件的年齡到達了MaxTenuringThreshold可以進入老年代,同時,如果在Survivor區中相同年齡所有物件大小的綜合大於survivor區的一半,年齡大於等於該年齡的物件可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡

8.5、空間分配擔保

在發生Minor GC時,虛擬機器會檢查老年代連續的空閒區域是否大於新生代所有物件的總和,若成立,則說明Minor GC是安全的,否則,虛擬機器需要檢視HandlePromotionFailure的值,檢視是否允許擔保失敗,若允許,則虛擬機器繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,若大於,則進行一次Minor GC;若小於或者HandlerPromotionFailure設定不執行冒險,那此時將改成一次Full GC,以上是JDK Update 24之前的策略,之後策略改變了只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小就會進行Minor GC,否則進行Full GC。

冒險是指經過一次Minor GC後有大量物件存活,而新生代的survivor區很小,放不下這些大量存活的物件,所以需要老年代進行分配擔保,把survivor區無法容納的物件直接進入老年代。

9、類載入機制

9.1、類載入過程

類從被載入到虛擬機器中開始,到解除安裝出記憶體,整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和解除安裝(Unloading)這七個階段。其中驗證、準備、解析3部分統稱為連線(Linking),這七個階段的發生順序如下圖:

類載入的過程包括了載入、驗證、準備、解析、初始化五個階段。在這五個階段中,載入、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段不一定,它在某些情況下可以再初始化階段之後開始,這是為了支援java語言的執行時繫結(也稱為動態繫結或晚期繫結)。另外注意這裡的幾個階段是按順序開始的,而不是按順序進行或完成的,因為這些階段通常是相互交叉的混合進行的,通常在一個階段執行過程中啟用或呼叫另一個階段。

9.1.1、載入

載入是類載入的第一個階段,載入階段做了三件事:

  1. 獲取.class檔案的二進位制流
  2. 將類資訊、靜態變數、位元組碼、常量這些.class檔案中的內容放入方法區中
  3. 在記憶體中生成一個代表這個.class檔案的java.lang.class物件,作為方法區這個類的各種資料問問入口,一般這個class是在堆中,不過Hotspot虛擬機器比較特殊,放在方法區中

二進位制流來源:

  • 從zip包中獲取,這就是jar、ear、war格式的基礎
  • 從網路中獲取,典型應用是Applet
  • 執行時計算生成,典型應用就是動態代理
  • 有其他檔案生成,典型應用就是JSP,即由JSP中生成對應的.class檔案
  • 從資料庫中讀取

9.1.2、驗證

驗證階段會完成以下4個檢驗動作:檔案格式驗證、後設資料驗證、位元組碼驗證、符號引用驗證。

對於虛擬機器的類載入機制來說,驗證階段是非常重要的,但是不一定必要(因為對程式執行期沒有影響)的階段。如果全部程式碼都已經被反覆使用和驗證過,那麼在實施階段就可以考慮使用Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間    

9.1.3、準備

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中分配。對於該階段有以下幾點需要注意:

  1. 這時候進行記憶體分配的僅包括類變數(static),而不包括例項變數,例項變數會在物件例項化時隨著物件一塊分配在Java堆中。
  2. 這裡所設定的初始值通常情況下是資料型別預設的零值(如0、0L、null、false等),而不是被在Java程式碼中被顯式地賦予的值。這裡還需要注意如下幾點:
    1. 對基本資料型別來說,對於類變數(static)和全域性變數,如果不顯式地對其賦值而直接使用,則系統會為其賦予預設的零值,而對於區域性變數來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
    2. 對於同時被static和final修飾的常量,必須在宣告的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在宣告時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予預設零值。
    3. 對於引用資料型別reference來說,如陣列引用、物件引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予預設的零值,即null。
    4. 如果在陣列初始化時沒有對陣列中的各元素賦值,那麼其中的元素將根據對應的資料型別而被賦予預設的零值。
  3.  如果類欄位的欄位屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變數value就會被初始化為ConstValue屬性所指定的值。

9.1.4、解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。符號引用和直接引用區別:

1、符號引用。這個其實是屬於編譯原理方面的概念,符號引用包括了下面三類常量:

  •  類和介面的全限定名
  • 欄位的名稱和描述符
  • 方法的名稱和描述符

2、直接引用

直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器示例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經存在在記憶體中了。

9.1.5、初始化

初始化階段是類載入過程的最後一步,初始化階段是真正執行類中定義的Java程式程式碼(或者說是位元組碼)的過程。初始化過程是一個執行類構造器<clinit>()方法的過程,根據程式設計師通過程式制定的主觀計劃去初始化類變數和其它資源。把這句話說白一點,其實初始化階段做的事就是給static變數賦予使用者指定的值以及執行靜態程式碼塊。

注意一下,虛擬機器會保證類的初始化在多執行緒環境中被正確地加鎖、同步,即如果多個執行緒同時去初始化一個類,那麼只會有一個類去執行這個類的<clinit>()方法,其他執行緒都要阻塞等待,直至活動執行緒執行<clinit>()方法完畢。因此如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個程式阻塞。不過其他執行緒雖然會阻塞,但是執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒不會再次進入<clinit>()方法了,因為同一個類載入器下,一個類只會初始化一次。實際應用中這種阻塞往往是比較隱蔽的,要小心。

初始化,為類的靜態變數賦予正確的初始值,JVM負責對類進行初始化,主要對類變數進行初始化。在Java中對類變數進行初始值設定有兩種方式:

  1. 宣告類變數時指定初始值
  2. 使用靜態程式碼塊為類變數指定初始值

JVM初始化步驟

  1. 假如這個類還沒有被載入和連線,則程式先載入並連線該類。
  2. 假如該類的直接父類還沒有被初始化,則先初始化其直接父類。
  3. 假如類中有初始化語句,則系統依次執行這些初始化語句。

類初始化時機:只有當對類主動使用的時候才會導致類的初始化,類的主動使用包括以下四種:

  • 使用new關鍵字例項化物件、讀取或者設定一個類的靜態欄位(被final修飾的靜態欄位除外)、呼叫一個類的靜態方法的時候。
  • 使用java.lang.reflect包中的方法對類進行反射呼叫的時候。
  • 初始化一個類,發現其父類還沒有初始化過的時候。
  • 虛擬機器啟動的時候,虛擬機器會先初始化使用者指定的包含main()方法的那個類。

以上四種情況稱為主動使用,其他的情況均稱為被動使用,被動使用不會導致初始化。

9.1.6、結束生命週期

在如下幾種情況下,Java虛擬機器將結束生命週期

  • 執行了System.exit()方法
  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致Java虛擬機器程式終止

9.2、類載入器

虛擬機器設計團隊把類載入階段張的"通過一個類的全限定名來獲取此類的二進位制位元組流"這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組稱為"類載入器"。類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠遠不限定於類載入階段。對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話表達地再簡單一點就是:比較兩個類是否"相等",只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則即使這兩個類來源於同一個.class檔案,被同一個虛擬機器載入,只要載入它們的類載入器不同,這兩個類必定不相等。

關於這張圖首先說兩點:

  1. 這三個層次的類載入器並不是繼承關係,而只是層次上的定義
  2. 它並不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類載入器實現方式

9.2.1、啟動類載入器Bootstrap ClassLoader

這是一個嵌在JVM核心中的載入器。它負責載入的是JAVA_HOME/lib下的類庫,系統類載入器無法被Java程式直接應用

9.2.2、擴充套件類載入器Extension ClassLoader

這個類載入器由sun.misc.Launcher$ExtClassLoader實現,它負責用於載入JAVA_HOME/lib/ext目錄中的,或者被java.ext.dirs系統變數指定所指定的路徑中所有類庫,開發者可以直接使用擴充套件類載入器。java.ext.dirs系統變數所指定的路徑的可以通過程式來檢視

9.2.3、應用程式類載入器Application ClassLoader

這個類載入器由sun.misc.Launcher$AppClassLoader實現。這個類也一般被稱為系統類載入器;應用程式都是由這三種類載入器互相配合進行載入的,如果有必要,我們還可以加入自定義的類載入器。因為JVM自帶的ClassLoader只是懂得從本地檔案系統載入標準的java class檔案,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:

  1. 在執行非置信程式碼之前,自動驗證數字簽名。
  2. 動態地建立符合使用者特定需要的定製化構建類。
  3. 從特定的場所取得java class,例如資料庫中和網路中。

9.2.4、JVM類載入機制

  • 全盤負責,當一個類載入器負責載入某個Class時,該Class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入。
  • 父類委託,先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。
  • 快取機制,快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效。

類載入有三種方式:

  1. 命令列啟動應用時候由JVM初始化載入
  2. 通過Class.forName()方法動態載入
  3. 通過ClassLoader.loadClass()方法動態載入

9.2.5、雙親委派模型      

雙親委派模型的工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把請求委託給父載入器去完成,依次向上,因此,所有的類載入請求最終都應該被傳遞到頂層的啟動類載入器中,只有當父載入器在它的搜尋範圍中沒有找到所需的類時,即無法完成該載入,子載入器才會嘗試自己去載入該類。

雙親委派機制:

  1. 當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  2. 當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  4. 若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。

雙親委派模型意義:

  • 系統類防止記憶體中出現多份同樣的位元組碼
  • 保證Java程式安全穩定執行

 

相關文章