這一次,我要系統的瞭解一下JVM

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!


一.JVM的加載機制

1.什麼是類的加載機制

類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的 Class對象, Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。

類加載器並不需要等到某個類被“首次主動使用”時再加載它,JVM規範允許類加載器在預料某個類將要被使用時就預先加載它,如果在預先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤。

2.類的生命週期

類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始,這是爲了支持Java語言的運行時綁定(也成爲動態綁定或晚期綁定)。

另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,因爲這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

3.類加載器

站在java虛擬機的角度來講,只存在兩種不同的類加載器:啓動類加載器和其他的加載器。(因爲啓動類加載器是使用C++語言實現的,它是虛擬機自身的一部分,而其他的加載器則是由java語言實現的,而且都是繼承自抽象類java.lang.ClassLoad,而這些加載器必須由啓動類加載器加載到內存後,才能去加載其他的類。)

但是站在java開發人員來看,類加載器大概分爲四種,啓動類加載器,擴展類加載器,應用程序類加載器,自定義加載器。

  • 啓動類加載器(BootStrap ClassLoad):

負責加載存在在jdk/jre/lib目錄下的能被虛擬機識別的類庫,而且啓動類加載器是無法被java程序直接應用的。

  • 擴展類加載器(Extension ClassLoad):

該加載器是用來加載jdk/jre/lib/ext目錄下的所有類庫,開發者可以直接使用擴展類加載器。

  • 應用程序類加載器(Application ClassLoad):

該類加載器是負責加載用戶類路徑所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的加載器,那麼這個加載器就是程序中默認的加載器。

  • 自定義類加載器(User ClassLoad):

自定義加載器是用戶自己定義的加載器,這種加載器主要注意三點:

(1).在執行非置信代碼之前,自動驗證數字簽名。

(2).動態地創建符合用戶特定需要的定製化構建類。

(3).從特定的場所取得java class,例如數據庫中和網絡中。

4.JVM的類加載機制

全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。

父類委託,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。

緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲什麼修改了Class後,必須重啓JVM,程序的修改纔會生效。

5.雙親委派模型

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

雙親委派模型的意義:

1.系統類防止出現內存中出現多份同樣的字節碼。
2.保證java程序安全穩定運行。

二.JVM內存結構

Jvm的內存結構主要是由java堆,java棧,本地方法棧,方法區,程序計數器組成。

1.Java堆:

Java堆內存,是由所有線程共享的一塊內存區域,這個內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。
從內存回收的角度來講,因爲現在的收集器基本都是採用的分代收集算法,所以java堆中還可以細分爲:新生代和老年代。而新生代,則又進行劃分,分爲了Eden區,From Survivor區域,To Survivor區域。

2. 方法區

方法區和java堆一樣,都是由所有線程共享的內存區域。它主要的作用,是存儲已經被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。雖然在java虛擬機規範裏面,會把方法區當作java堆的一個邏輯部分,但是它卻也有一個別名,叫做Non-Heap(非堆),目的是要與java堆分開來。

但是,在HotSpot虛擬機上,會把方法區稱爲“永久代”,本質上當然兩者並不等價,而出現這種情況的原因是,HotSpot虛擬機的團隊將GC分代收集擴展到了方法區,與java堆聯合在一起,形成了一個GC回收的網絡。

3.程序計數器

當java文件被打包成class文件的時候,裏面的代碼都是被優化過的,但是java程序是多線程的,當一個線程在執行一個程序到一半的時候,忽然被調去執行另外一個程序了,那麼如何保證等它執行另外一個程序,返回過來執行之前的程序的時候,能夠準確的找到之前執行到程序的哪個點呢?這個時候就需要程序計數器了。

每一個線程都會有一個單獨的程序計數器,它會記錄這個線程正在執行的虛擬機字節碼指令的地址,但是隻能記錄java方法的,如果是Native方法,那麼這個計數器的值就是爲空(Undefined)。

這個區域,也是唯一一個在java虛擬機規範中沒有規定任何內存溢出情況的區域。

4.java棧

和程序計數器一樣,java棧也是線程私有的內存區域。

一個線程,當它開始的時候,就會內存中出現一個獨屬於它的java棧,當這個線程在執行一個方法的時候,java棧中會開闢一個棧幀,這個棧幀將會存儲這個方法的局部變量表,操作棧,動態鏈接,方法出口等信息。所以線程會調用一個又一個的方法,那麼也就會在獨屬於它的java棧中開闢出一個又一個的棧幀,而每一個方法被調用直到這個方法被執行完成,就對應着一個棧幀在虛擬機棧中從入棧到出棧的一個過程。

但是java棧,只爲虛擬機執行java方法服務。

5.本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。虛擬機規範中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。

甚至有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

三.GC算法

在jvm中,程序計數器,java棧,本地方法棧都是隨着線程而生也隨着線程而滅,那麼自然就實現了內存的清理。但是java堆和方法區,卻不是這樣,因此我們現在的GC垃圾回收器主要就是集中在java堆和方法區。

1. 如何判斷一個對象是否存活

判斷對象是否存活一般有兩種方式:

引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。

可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。不可達對象。

2. 分代收集算法

分代收集算法的前提,是確定絕大部分對象的生命週期都是非常短暫的,存活時間短。

分代收集算法的特點,是把java堆設置新生代和老年代,這樣就可以根據不同年代的特點來選擇最合適的算法。(現在討論的基本都是HotSpot虛擬機)

新生代:朝生夕滅的對象(例如:方法的局部變量引用的對象等)。

這個是java堆中,java堆主要是分爲新生代和老年代,一般默認的比例是新生代佔據整個java堆的1/3,新生代自己也會再度細分,目的是爲了選擇出更爲合適的GC收集算法,提升回收的效率。

新生代分爲了Eden區,From Survivor區域,To Survivor區域。

當系統創建一個對象的時候,總是會在Eden區操作,但是大部分對象,都是在創建後不久就永遠都不會再使用了,因此也會很快變得不可達,當這個區域快要滿了到時候,就會觸發一次YongGC,將這些不可達的對象清理掉。而剩下的還存活着的對象呢,YongGC就會通過“複製算法”,轉移到From Survivor區域。

當From Survivor區域的對象消亡後,會觸發YongGC來清理掉這些對象,而剩下的對象,YongGC則會全部通過“複製算法”複製到To Survivor區域。而這個時候,To Survivor區域就變成了了From Survivor區域,而之前的From Survivor區域則就會被YongGC全部清空內存,變成了To Survivor。

當一個對象,在年輕代存活了足夠長的時間後,如果沒有被GC清理掉,那麼就會通過“複製算法”複製到老年代去。還有一種情況就是,如果一次GC下來,存活的對象佔據的內存超過了新生代的內存的10%,那麼也會將這部分內存複製到老年代中去。

老年代:存活得比較久,但還是要死的對象(例如:緩存對象、單例對象等)。

老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內存清理時,如果使用停止-複製算法,則相當低效。一般,老年代用的算法是標記-整理(也稱之爲標記-壓縮算法)算法,即標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。

在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC。

永久代:對象生成後幾乎不滅的對象(例如:加載過的類信息)。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-07-05
本文作者:吳里斯蒂亞諾_翀納爾多
本文來自:“掘金”,瞭解相關信息可以關注“掘金”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章