如圖所示
JVM的主要元件:執行時資料區、執行引擎、本地介面、本地方法庫等。
本文主要分享類JVM記憶體的記憶體結構。
JVM記憶體結構是指JVM執行時的資料區域結構,主要由以下幾部分組成:
堆:執行緒共享。
方法區域:執行緒共享。
VM 堆疊:執行緒是專用的。
程式計數器暫存器:執行緒專用。
本機方法堆疊:執行緒是私有的。
如圖所示
JVM 堆 (HEAP) 是 J**A 虛擬機器中的記憶體區域(由所有執行緒共享),主要用於儲存物件例項和陣列。 堆分為三個部分:年輕一代、老一代和永久一代(永久一代在 JDK8 中被取消),其中年輕一代分為 Eden 區域和倖存者區域(包括:S0 和 S1)。
如圖所示
年輕一代和老一代
在年輕一代中,大多數物體在生命的盡頭死亡,因此年輕一代被設計為最小化物體的存活時間,以便更快地**記憶。
預設情況下,年輕一代與老一代的比例為1:2,即年輕一代佔據整個堆空間的1 3,老一代佔據整個堆空間的2 3。
年輕一代與老一代的比例可以通過引數 -xx:newratio= 進行調整。 這表示老一代與年輕一代的比例,預設值為 2。 例如,如果設定了 -xx:newratio=2,則表示老一代佔據了整個堆空間的 2 3,年輕一代佔據了整個堆空間的 1 3。
您可以使用引數 -xx:survivorratio= 來調整 EDEN 區域和倖存者區域的比率。 這表示伊甸園區域與倖存者區域的比率,預設值為 8。 例如,如果設定了 -xx:survivorratio=8,則伊甸園區域與倖存者區域的比例為 8:1:1。
物件分配過程
在分析物件分配過程之前,我們先簡單解釋一下 GC 型別:
Minor GC(又稱Young GC):主要用於收集年輕一代中非倖存的物品。 當 Eden 區域中沒有足夠的空間時,會觸發次要 GC。 注意:次要 GC 可能會丟擲 stw(停止世界)。
主要GC:主要用於收集老年時無法倖存的物品。 當老年沒有足夠的空間時,會觸發主要的 GC。 注意:大GC一般伴有小GC,一般比小GC慢10倍,所以STW時間會更長(盡量避免小GC)。 現時,只有承包會單獨收取老年費。
混合GC:主要用於收集年輕一代和一些老一代中非倖存的物體。 目前只有 G1 GC 具有此行為。
全 GC:主要用於收集整個堆(包括:年輕一代和老一代)中不可存活的物件,即:主 GC+次要 GC 組合。 全 GC 觸發條件:
呼叫系統gc() 方法,您可以使用引數 -xx:+ disableexplicitgc 來禁用對系統的呼叫gc()。
當方法區域中沒有足夠的空間時。
小GC,倖存物體的大小超過了老年的剩餘空間。
在未成年人GC的情況下,年輕一代的倖存者區域空間不足,如果允許保證失敗,則觸發全GC,如果不允許,則觸發全GC。 允許並且每次提公升到舊時代物件的平均大小時,舊時代的最大可用連續記憶體空間也將觸發完整的 gc。
如果CMS GC異常,CMS執行過程中預留的記憶體不能滿足程式需求,則丟擲Concurrent Mode Failure異常,導致CMS退化為Serial Old,觸發Full GC。
受試者進入老年的觸發條件:
1)受試者年滿15歲。預設情況下,物件在 15 次次要 GC 後轉移到老年。 物件進入舊次要 GC 的次數可以通過 JVM 引數 -xx:maxtenuringthreshold 設定,預設為 15 次。
2)動態年齡判斷。當一批倖存物件的總大小超過倖存者區域記憶體大小的50%時,部分倖存物件根據其年齡轉移到老年(較老的倖存物件優先轉移)。
3)大物直接進入老年。當你需要建立乙個大於年輕一代剩餘空間的物件(例如,乙個非常大的陣列)時,該物件將直接儲存在老一代中,可以使用引數 -xx:pretenuresizethreshold 進行設定(預設值為 0,即任何物件都會先在年輕一代中分配記憶體)。
4)當小GC之後的倖存物體太多,無法放入倖存者區域時,這些物體將直接轉移到老年。
物件分配過程
當新生成的物件已滿時,觸發次要 gc,傳輸其他物件不再引用的物件,並將倖存的物件轉移到 survivor0 區域。
倖存者0區域滿後,觸發次要GC,將倖存者0區域中的倖存物件轉移到倖存者1區域,並清除倖存者0區域,確保乙個倖存者區域始終為空。
經過多次小 GC 後,倖存的物件轉移到老年,進入老年的小 GC 數量可以通過引數 -xx:maxtenuringthreshold= 設定,預設為 15 倍。
主要 GC 是在老年滿時觸發的(即全 GC,因此當執行主要 GC 時,將先執行次要 GC)。
分代採集的原因:物件按照存活概率進行分類,主要是為了減小掃瞄範圍和GC執行頻率,同時針對不同區域採用不同的演算法,提高效率。
年輕一代之所以存在兩個相同大小的倖存者區域,是為了解決記憶體碎片化問題,即保證有足夠的連續記憶體空間來分配物件(例如,大物件)。
字串常量池
字串常量池是 J**A 中用於儲存字串常量的特殊儲存區域。 在 j**a 中,字串常量是不可變的,因此可以共享它們。 這樣可以減少記憶體使用量並提高程式的效能。 在 JDK8 中,字串常量池儲存在堆中。
靜態變數
靜態變數是在類中定義的變數,其值在程式的整個執行生命週期中不會更改。 在 jdk8 中,永久生成被取消,方法區域成為邏輯區域,因此靜態變數的內存在堆中分配(在 jdk7 及之前,靜態變數的內存在永久生成中分配)。 他們的壽命與乙個班級的壽命相同。
執行緒本地分配緩衝區 (TLAB)。
TLAB(Thread Local Allocation Buffer)是J**A虛擬機器中的一種優化技術,主要用於提高物件的分配效率。 每個執行緒都有自己的 tlab,用於分配物件。 當執行緒需要分配乙個物件時,它首先在自己的 tlab 中分配它,如果 tlab 中沒有足夠的空間,它就會請求堆中的空間。
TLAB相關引數:
-xx:usetlab:啟用或禁用 tlab,預設開啟。
-xx:TlabWasteTargetPercent:指定 TLAB 占用的 Eden 區域空間的百分比。
使用tlab的原因:
1)建立物件時確保執行緒安全。堆 (HEAP) 是執行緒的共享區域,在併發環境中,物件在堆中分配記憶體時具有最好的程式安全問題。 TLAB(lock-free mode)用於解決多個執行緒同時操作同一位址導致的執行緒安全問題。
2)提高物件的記憶體分配效率。物件在堆(HEAP)中建立的頻率非常高,在併發環境中,當物件通過載入機制在堆中分配記憶體時,記憶體分配的速度會受到影響。 通過使用 TLAB(無鎖模式),可以提高物件的記憶體分配效率。
堆記憶體常用引數
在 jdk8 及之前,方法區域是永久的,但在 JDK8 之後,刪除了永久生成,並將方法區域移動到本地記憶體,即元空間。
從邏輯上講,元宇宙是堆的一部分,但為了將其與堆區分開來,它通常被稱為非堆。 元宇宙是由執行緒共享的記憶體區域,它儲存內容的兩個主要部分:
類資訊。 執行恆定計量池。
方法區與堆相同,如圖所示
元宇宙相關引數:
MetaspaceSize:初始化元空間大小並控制 GC 閾值,預設值為 20MB。
maxmetaspacesize:限制元空間大小上限,防止異常占用過多的物理記憶體,預設值為 -1(表示無限制)。
類資訊。
類檔案資訊除了包含版本、字段、方法、介面等描述資訊外,還包含常量池表(也稱為靜態常量池),主要用於儲存編譯器生成的各種靜態常量(也稱為文字常量或文字)和符號引用。 哪裡:
靜態常量:指由字母和數字組成的字串或數字常量。 靜態常量只能出現在右值中,右值是指賦值時等號右邊的值,例如:int a=1,a 是左值,1 是右值。
符號引用:符號引用主要包括以下三種型別的常量:
類和介面的完全限定名稱。
欄位的名稱和描述符。
方法的名稱和描述符。
執行恆定計量池。
執行時池主要用於在程式執行時為靜態資訊(如靜態變數和常量池表中的符號引用)分配記憶體位址。
虛擬機器堆疊是執行緒專用的,它描述了由 j**a 方法執行的記憶體模型。 每個堆疊由多個堆疊幀組成,當每個方法執行時,j**a 虛擬機會同步建立乙個堆疊幀來儲存該方法的區域性變數表、運算元堆疊、動態鏈結、方法返回位址等資訊。
堆疊和堆疊框架
從被呼叫到每個方法的執行過程,對應乙個棧幀進入棧到離開棧在虛擬機器棧中的過程(注:不了解棧的同學,請移至個人主頁參考文章->講解堆的資料結構和演算法)。 堆疊頂部的堆疊幀是當前執行方法,當該方法呼叫其他方法時,會建立乙個新的堆疊幀,這個新的堆疊幀會放在虛擬機器堆疊的頂部,成為當前活動的堆疊幀,所有指令完成後,堆疊幀會從堆疊中移除, 上乙個堆疊幀將成為當前活動堆疊幀,上乙個刪除的堆疊幀的返回值將成為當前活動堆疊幀的運算元。
如圖所示
堆疊幀包含區域性變數表、運算元堆疊、動態聯接、方法返回位址等。
區域性變數表
區域性變數表是一組變數值的儲存空間,用於儲存方法引數和方法中定義的區域性變數。 將 j**a 檔案編譯為類檔案時,需要分配給區域性變數表的最大容量在方法的 code 屬性的 max locals 資料項中確定。
區域性變數表的大小基於槽的最小大小,32 位 VM 中的槽可以儲存 32 位(4 位元組)以內的資料型別(boolean、byte、char、short、int、float、reference 和 returnaddress)。
對於 64 位資料型別(長型、雙精度型),VM 以高位對齊方式為它們分配兩個連續的槽(即將長型和雙精度型資料型別的讀取和寫入拆分為兩個 32 位讀取和寫入)。
對於引用型別,虛擬機器規範沒有指定其長度,但虛擬機器一般可以直接或間接地從該引用的 j**a 堆中物件的起始位址索引和方法區域中找到物件型別資料。
為了盡可能多地節省堆疊幀空間,插槽被設計為可重用的,即當程式計數器的指令超出變數的範圍時,與該變數對應的插槽可以被其他變數使用。 但是,這種機制會影響 GC,例如,如果某個方法占用了大量 slot,並且在執行該方法的作用域後沒有分配或清除該 slot,則垃圾製造者將無法及時**該 slot 的記憶體。
區域性變數不被賦予初始值(例項變數和類變數都被賦予初始值)。
運算元堆疊
除了區域性變數表外,每個獨立的堆疊都包含乙個後進先出運算元堆疊,主要用於儲存計算過程的中間結果,並作為計算過程中變數的臨時儲存空間。 當方法開始執行時,方法中的運算元堆疊是空的,在方法執行過程中,會從運算元堆疊中寫入和提取各種位元組碼指令(即堆疊的內部和外部)。 例如,算術運算是通過運算元堆疊執行的,引數傳輸是在通過運算元堆疊呼叫方法時執行的。
運算元堆疊與區域性變數表一樣,確定在將 j**a 檔案編譯為類檔案時,需要在方法的 code 屬性的 max stack 資料項中分配的最大容量。
每個運算元堆疊都有乙個定義的堆疊深度來儲存值,其中 32 位資料型別占用乙個堆疊單元深度,64 位資料型別占用兩個堆疊單元深度。
運算元堆疊和區域性變數表有很大的區別,運算元堆疊不使用訪問索引進行資料訪問,而是通過標準的入站和出站操作來完成資料訪問。
如果呼叫的方法有返回值,則將返回值壓入當前堆疊幀的運算元堆疊中,並更新需要在 PC 暫存器中執行的下乙個位元組碼指令。
在概念模型中,堆疊幀是相互獨立的,大多數虛擬機會做一些優化,使區域性變數表和兩個堆疊幀的運算元堆疊之間有一些重疊,這樣在進行方法呼叫時可以直接共享引數,而不需要額外的引數複製。
動態鏈結
每個堆疊都包含對執行時池中堆疊所屬方法的引用,此引用的目的是支援當前方法實現動態鏈結的能力。
將 j**a 檔案編譯為類檔案時,所有變數和方法引用都作為符號引用儲存在類檔案的常量池中。 例如,描述呼叫其他方法的方法由常量池中對該方法的符號引用表示,動態鏈結的作用是將這些符號引用轉換為對呼叫方法的直接引用。
如圖所示
方法引用:靜態鏈結:在JVM內部載入位元組碼檔案時,如果要呼叫的目標方法在編譯時確定,並且執行時保持不變,則將被呼叫方法的符號引用轉換為直接引用的過程稱為靜態鏈結。
動態鏈結:如果在編譯時無法確定被呼叫的方法,則被呼叫方法的符號引用只能在程式執行時轉換為直接引用,由於此引用轉換過程的動態性質,也稱為動態鏈結。
繫結:繫結:繫結是指將字段、方法或類替換為符號引用的直接引用的過程,該過程僅發生一次。 靜態鏈結和動態鏈結對應的方法的繫結機制為:早期繫結和後期繫結。
早期繫結:早期繫結意味著,如果所呼叫的目標方法在編譯時是已知的,並且執行時保持不變,則該方法可以繫結到它所屬的型別。 由於要呼叫的目標方法是已知的,因此還可以使用靜態鏈結將符號引用轉換為直接引用。
後期繫結:如果在編譯時無法確定被呼叫的方法,則只能在程式執行時根據實際型別繫結相關方法,也稱為後期繫結。
堆疊頂部快取:由於運算元儲存在記憶體中,因此頻繁的記憶體中讀寫將不可避免地影響執行速度。 為了解決這個問題,Hotspot JVM的設計者提出了top-stack caching technology,該技術將所有top-ofstack元素快取在物理CPU的暫存器中,從而減少對記憶體的讀寫次數,提高執行引擎的執行效率。
方法返回位址
儲存呼叫該方法的 PC 暫存器的值。 一種方法的結束,有兩種方法:
正常執行完成。
發生未處理的異常,並發生異常退出。
無論採用哪種方式,在方法退出後,您都會返回到呼叫該方法的位置。 當方法正常退出時,呼叫方的 PC 計數器的值用作返回位址,即呼叫該方法的下一條指令的位址。 當方法異常退出時,返回位址需要通過異常表來確定,這部分資訊一般不會儲存在堆疊幀中。
正常完成退出和異常完成退出的區別在於,異常退出的方法不會向上層呼叫方生成任何返回值。
程式計數器是一小塊記憶體空間,可以將其視為當前執行緒執行的位元組碼的行號指示符。 在虛擬機器的概念模型中,位元組碼直譯器的工作原理是改變這個計數器的值,選擇下乙個要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基本功能都需要依靠程式計數器來完成。
程式計數器功能:
每個執行緒都有乙個獨立的程式計數器,每個執行緒之間的計數器互不影響,獨立儲存。
執行 j**a 方法時,程式計數器的值不為空;執行本機本地方法時,程式計數器的值未定義。
程式計數器是 j**a 虛擬機器規範中未指定記憶體溢位條件的唯一區域。
本地方法堆疊與虛擬機器堆疊類似,不同之處在於虛擬機器堆疊為虛擬機器執行 j**a 方法服務,而本地方法堆疊為虛擬機器使用的本機方法提供服務。 當 j**a 程式呼叫本機方法時,本機方法所需的記憶體空間會在本地方法棧中開啟。
本地方法棧功能:
本地方法堆疊是執行緒專用的。
本地方法棧允許固定或動態可擴充套件的記憶體大小
固定記憶體大小,當執行緒請求分配的堆疊容量超過本地方法堆疊允許的最大記憶體大小時,j**a 虛擬機器將丟擲 stackoverflowerror 異常。
記憶體大小可以動態擴充套件,當嘗試擴充套件時無法應用本地方法棧以獲得足夠的記憶體,或者在建立新執行緒時沒有足夠的記憶體來建立相應的本地方法棧時,j**a 虛擬機器將丟擲 outofmemoryerror 異常。
原生方法一般用 C C++ 實現,這是通過在本地方法棧中註冊原生方法,然後在執行引擎中載入本地方法庫並執行它們來完成的。
閱讀推薦]更多精彩內容,如:
Redis 系列。
資料結構和演算法。
NACOS系列。
MySQL系列。
JVM 系列。
卡夫卡系列。
併發程式設計系列。
請移至【南秋】個人主頁參考。 內容不斷更新。
關於作者]熱愛科技、熱愛生活的老寶貝,專注J**A領域,關注【南秋同學】帶你一起學習、共同成長