深入學習Java虛擬機:內存區域

在Java中,分配內存和回收內存都由JVM自動完成。

 

內容:首先從操作系統層面簡單介紹物理內存的分配和Java運行的內存分配之間的關係,明白在Java中使用的內存與物理內存區別。Java如何使用從物理內存申請下來的內存,以及如何來劃分它們,如何分配和回收內存。最如何解決OutOfMemoryError,並提供一些處理這類問題的常用手段

 

 

內存的不同形態-物理內存和虛擬內存:

物理內存(RAM (隨機存儲器)):

計算機中,有一個存儲單元叫寄存器,用於存儲計算單元執行指令(如浮點、整數等運算時)的中間結果。

寄存器的大小決定了一次計算可使用的最大數值連接處理器和RAM或者處理器和寄存器的是地址總線

總線的寬度影響了物理地址的索引範圍,因爲總線的寬度決定了處理器一次可以從寄存器或者內存中獲取多少個bit。也決定了處理器最大可以尋址的地址空間,如32位地址總線可尋址範圍爲0x0000 0000~0xffffffff。這個範圍是232=4 294 967 296個內存位置,每個地址會引用一個字節,所以32位總線寬度可以有4GB的內存空間。

通常情況下,地址總線和寄存器或RAM有相同的位數,這樣更容易傳輸數據,但是也有不一致的情況,如x86的32位寄存器寬度的物理地址可能有兩種大小,分別是32位物理地址和36位物理地址,擁有36位物理地址的是Pentium Pro和更高型號。

 

除了在學校的編譯原理的實踐課或者要開發硬件程序的驅動程序時需要直接通過程序訪問存儲器外,大部分情況下都調用操作系統提供的接口來訪問內存,在Java中甚至不需要寫和內存相關的代碼。

 

要運行程序,都要向操作系統先申請內存地址。通常操作系統管理內存的申請空間是按照進程來管理的,每個進程擁有一段獨立的地址空間,每個進程之間不會相互重合,操作系統也會保證每個進程只能 訪問自己的內存空間。這主要是從程序的安全性來考慮的,也便於操作系統來管理物理內存。

進程的內存空間的獨立主要是指邏輯上獨立,由操作系統來保證的,但是真正的物理空間是不是隻能由一個進程來使用就不一定了。因爲隨着程序越來越龐大和設計的多任務性,物理內存無法滿足程序的需求,在這種情況下就有了虛擬內存的出現

虛擬內存使得多個進程在同時運行時可以共享物理內存,這裏的共享只是空間上共享,在邏輯上仍然是不能相互訪問的。虛擬地址不但可以讓進程共享物理內存、提高內存利用率,而且還能夠擴展內存的地址空間,如一個虛擬地址可能被映射到一段物理內存、文件或者其他可以尋址的存儲上。一個進程在不活動的情況下,操作系統將這個物理內存中的數據移到一個磁盤文件中(即通常Windows系統上的頁面文件,或者Linux系統上的交換分區),而真正高效的物理內存留給正在活動的程序使用。在這種情況下,在我們重新喚醒一個很長時間沒有使用的程序時,磁盤會吱吱作響,並且會有一個短暫的停頓得到印證,這時操作系統又會把磁盤上的數據重新交互到物理內存中。但是必須要避免這種情況的經常出現,如果操作系統頻繁地交互物理內存的數據和磁盤數據,則效率將會非常低,尤其是在Linux服務器上,我們要關注Linux中swap的分區的活躍度。如果swap分區被頻繁使用,系統將會非常緩慢,很可能意味着物理內存已經嚴重不足或者某些程序沒有及時釋放內存

 

 

內存使用形式-內核空間和用戶空間:

一個計算機通常有一定大小的內存空間,但是程序並不能完全使用這些地址空間,因爲這些地址空間被劃分爲內核空間和用戶空間

程序只能使用用戶空間的內存,這裏所說的使用是指程序能夠申請的內存空間,並不是程序真正訪問的地址空間。

 

爲何需要內存空間和用戶空間的劃分呢?爲了保證操作系統的穩定性,運行在操作系統中的用戶程序不能訪問操作系統所使用的內存空間。這也是從安全性上考慮的,如訪問硬件資源只能由操作系統來發起,用戶程序不允許直接訪問硬件資源。如果用戶程序需要訪問硬件資源,如網絡連接等,可以調用操作系統提供的接口來實現,這個調用接口的過程也就是系統調用。每一次系統調用都會存在兩個內存空間的切換,通常的網絡傳輸也是一次系統調用,通過網絡傳輸的數據先是從內核空間接收到遠程主機的數據,然後再從內核空間複製到用戶空間,供用戶程序使用。這種從內核空間到用戶空間的數據複製很費時,雖然保住了程序運行的安全性和穩定性,但是也犧牲了一部分效率。但是現在已經出現了很多其他技術能夠減少這種從內核空間到用戶空間的數據複製的方式,如Linux系統提供了sendfile文件傳輸方式

 

內核空間主要是指操作系統運行時所使用的用於程序調度、虛擬內存的使用或者連接硬件資源等的程序邏輯。

 

內核空間和用戶空間的大小如何分配?

更多地分配給用戶空間供用戶程序使用,還是首先保住內核有足夠的空間來運行,這要平衡一下。

如果是一臺登錄服務器,很顯然要分配更多的內核空間,因爲每一個登錄用戶操作系統都會初始化一個用戶進程,這個進程大部分都在內核空間裏運行。

在當前32位Linux系統中默認的比例是1:3 (1GB的內核空間,3GB的用戶空間)

 

 

 

Java自己中哪些組件需要使用內存?

Java啓動後也作爲一個進程運行在操作系統中,這個進程有哪些部分需要分配內存空間呢?

1、Java堆:用於存儲Java對象的內存區域,堆的大小JVM啓動時就一次向操作系統申請完成,通過-Xmx和-Xms兩個選項來控制大小,Xmx表示堆的最大大小,Xms表示初始大小。一旦分配完成,堆的大小就將固定,不能在內存不夠時再向操作系統重新申請,同時當內存空閒時也不能將多餘的空間交還給操作系統。

在Java堆中內存空間的管理由JVM來控制,對象創建由Java應用程序控制,但是對象所佔的空間釋放由管理堆內存的垃圾收集器來完成。根據垃圾收集(GC)算法的不同,內存回收的方式和時機也會不同

2、線程:JVM運行實際程序的實體是線程,線程需要內存空間來存儲一些必要的數據。每個線程創建時JVM都會爲它創建一個堆棧,堆桟的大小根據不同的JVM實現而不同,通常在256KB〜756KB之間。線程所佔空間相比堆空間來說比較小。如果線程過多,線程堆棧的總內存使用量可能也非常大。當前有很多應用程序根據CPU的核數來分配創建的線程數,如果運行的應用程序的線程數量比可用於處理它們的處理器數量多,效率通常很低,並且可能導致比較差的性能和更高的內存佔用率

3、類和類加載器:類和加載類的類加載器本身同樣需要存儲空間,在Sun JDK中被存儲在堆中,這個區域叫做永久代(PermGen區)

JVM是按需來加載類的,

JVM如果要加載一個jar包是否把這個jar包中的所有類都加載到內存中?顯然不是。JVM只會加載那些在你的應用程序中明確使用的類到內存中。要查看JVM到底加載了哪些類,可以在啓動參數上加上-verbose:class

 

3個默認類加載器Bootstrap ClassLoader、ExtClassLoader和AppClassLoader都不可能滿足這些條件,因此,任何系統類(如java.lang.String)或通過應用程序類加載器加載的任何應用程序類都不能在運行時釋放

4、NIO:一種基於通道和緩衝區來執行I/O的新方式。就像在Java堆上的內存支持I/O緩衝區一樣,NIO使用java.nio.ByteBuffer.allocateDirect()方法分配內存,這種方式也就是通常所說的NIO direct memory。ByteBuffer.allocateDirect()分配的內存使用的是本機內存而不是Java堆上的內存,每次分配內存時會調用操作系統的Os::malloc()函數。另外一方面直接ByteBuffer產生的數據如果和網絡或者磁盤交互都在操作系統的內核空間中發生,不需要將數據複製到Java內存中,執行這種I/0操作要比一般的從操作系統的內核空間到Java堆上的切換操作快得多,可以避免在Java堆與本機堆之間複製數據。如果你的I/O頻繁地發送很小的數據,這種系統調用的開銷可能會抵消數據在內核空間和用戶空間複製帶來的好處。

直接ByteBuffer對象會自動清理本機緩衝區,這個過程只能作爲Java堆GC的一部分來執行,因此它們不會自動響應施加在本機堆上的壓力。GC僅在Java堆被填滿,以至於無法爲堆分配請求提供服務時發生,或者在Java應用程序中顯示請求時發生。當前在很多NIO框架中都在代碼中顯式地調用System.gc()來釋放NIO持有的內存。這種方式會影響應用程序的性能,因爲會增加GC的次數,一般情況下通過設置-XX:+DisableExplicitGC來控制System.gc()的影響,但是又會導致NIO direct memory內存泄漏問題。

 

5、JNI: Java運行時本身也依賴於JNI代碼來實現類庫功能,如文件操作、網絡I/O操作或者其他系統調用。所以JNI也會增加Java運行時的本機內存佔用。

 

 

 

JVM內存結構:JVM是按照運行時數據的存儲結構來劃分內存結構的,JVM在運行Java程序時,將它們劃分成幾種不同格式的數據,分別存儲在不同的區域,這些數據統一稱爲運行時數據(Runtime Data)。運行時數據包括Java程序本身的數據信息和JVM運行Java程序需要的額外數據信息,如要記錄當前程序指令執行的指針(又稱爲PC指針)等。

在Java虛擬機規範中將Java運行時數據劃分爲6種:PC寄存器數據、Java棧、堆、方法區、本地方法區、運行時常量池

 

JVM內存模型:JVM運行時數據區(JVM Runtime Area)

指JVM在運行期間,其對計算機內存空間的劃分和分配

包括:程序計數器、虛擬機棧、本地方法棧、Java堆、方法區以及方法區中的運行時常量池

方法區和堆是由線程共享的數據區,其他幾個是線程隔離的數據區

 

1.程序計數器PC(Program Counter Register):線程私有

JVM會爲每個線程創建一個程序計數器,不相互影響,在線程創建時被創建,指向下一條指令的地址。

程序計數器是一塊較小的內存,保存當前線程所執行的行號指示器(內存地址)

字節碼解釋器工作時就是通過改變計數器值來選下一條需執行的字節碼的指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

當有多個線程交叉執行時,被中斷線程的程序當前執行到哪條的內存地址必然要保存下來,以便於它被恢復執行時再按照被中斷時的指令地址繼續執行下去。

此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域

1如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址

2如果正在執行的是Native方法. 這個計數器則爲空undefinednative關鍵字說明其修飾的方法是原生態方法,方法對應的實現不在當前文件,而是用其他語言(如C/C++)實現的文件中。Java本身不能對操作系統底層進行訪問和操作,但可通過JNI接口調用其他語言實現對底層的訪問。JNIJava本機接口(Java Native Interface),JNI允許Java代碼使用以其他語言編寫的代碼和代碼庫

 

2.Java虛擬機棧(常說的棧)(VM stack):

線程私有,所以不必關心數據一致性問題,以及同步鎖問題。

虛擬機棧是一個先入後出的棧,每創建一個線程時,JVM會爲這個線程創建一個對應的私有虛擬機棧。

線程對方法的調用就對應着一個棧幀的入棧和出棧的過程。

每調用一個方法就創建一個棧幀(虛擬機棧的生命週期和線程一樣)。

 

棧幀是用來存儲數據和存儲部分過程結果的數據結構,也被用來處理動態鏈接(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。

每個棧幀會含有一些內部變量、操作數棧、方法返回值等信息。方法執行完成後,棧幀被銷燬。

存儲方法運行時所產生的數據,成爲棧幀(保存一個方法的局部變量、操作數棧、常量池指針)。

線程在運行的過程中,只有一個棧幀是處於活躍狀態,這個棧幀在棧頂。

 

描述的Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame,用於儲存局部變量表操作數棧(Java無寄存器,所有參數傳遞使用操作數棧)、動態鏈接方法(指向運行時常量池的引用)方法出口(返回地址)等信息。每個方法從調用直至完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。虛擬機棧的生命週期和線程相同

棧內存就是虛擬機棧,或者說是虛擬機棧中局部變量表的部分

Java虛擬機規範對這個區域規定了兩種異常狀況:

如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。棧溢出,常發生在遞歸中

如果虛擬機擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。棧可以設置大小

 

注:

棧幀(Stack Frame):

棧幀用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址和一些額外的附加信息等信息。每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。

在編譯程序代碼時,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了,並且寫入到方法表的 Code 屬性中,因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

一個線程中的方法調用鏈可能會很長,很多方法都同時處於執行狀態。對於執行引擎來說,在活動線程中,只有位於棧頂的棧幀纔是有效的,稱爲當前棧幀(Current Stack Frame),與這個棧幀相關聯的方法稱爲當前方法(Current Method)。執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。

https://img-blog.csdn.net/20141013142638909

1.局部變量表(Local Variable Table):

1、局部變量表(其內容空間在編譯期就完成了分配):保存方法的參數以及局部變量

存放編譯期可知的各種基本數據類型,引用類型(reference),returnAddress類型(指向了一條字節碼指令的地址)。當進入一個方法時,這個方法需要在幀分配多少內存是固定的,在方法運行期間是不會改變局部變量表的大小。因爲局部變量表只是存儲對象的引用。

局部變量表所需的內存空間在編譯期間完成分配。當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

其中64位長度的long和double類型的數據會佔用兩個局部變量空間,其餘的數據類型只佔用1個。

 

用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的

=====

局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在 Java 程序編譯爲 Class 文件時,就在方法的 Code 屬性的 max_locals 數據項中確定了該方法所需要分配的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot,下稱 Slot)爲最小單位,虛擬機規範中並沒有明確指明一個 Slot 應占用的內存空間大小,只是很有導向性地說到每個 Slot 都應該能存放一個 boolean、byte、char、short、int、float、reference (注:Java 虛擬機規範中沒有明確規定 reference 類型的長度,它的長度與實際使用 32 還是 64 位虛擬機有關,如果是 64 位虛擬機,還與是否開啓某些對象指針壓縮的優化有關,這裏暫且只取 32 位虛擬機的 reference 長度)或 returnAddress 類型的數據,這 8 種數據類型,都可以使用 32 位或更小的物理內存來存放,但這種描述與明確指出 “每個 Slot 佔用 32 位長度的內存空間” 是有一些差別的,它允許 Slot 的長度可以隨着處理器、操作系統或虛擬機的不同而發送變化。只要保證即使在 64 位虛擬機中使用了 64 位的物理內存空間去實現一個 Slot,虛擬機仍要使用對齊和補白的手段讓 Slot 在外觀上看起來與 32 位虛擬機中的一致。

 

Java 虛擬機的數據類型。一個 Slot 可以存放一個 32 位以內的數據類型,Java 中佔用 32 位以內的數據類型有 boolean、byte、char、short、int、float、reference 和 returnAddress 8 種類型。

reference 類型表示對一個對象實例的引用,虛擬機規範既沒有說明他的長度,也沒有明確指出這種引用應有怎樣的結構。但一般,虛擬機實現至少都應當能通過這個引用做到兩點,一是從此引用中直接或間接地查找到對象在 Java 堆中的數據存放的起始地址索引,二是此引用中直接或間接地查找到對象所屬數據類型在方法區中的存儲的類型信息,否則無法實現 Java 語言規範中定義的語法約束約束。

returnAddress 類型目前已經很少見了,它是爲字節碼指令 jsr、jsr_w 和 ret 服務的,指向了一條字節碼指令的地址,很古老的 Java 虛擬機曾經使用這幾條指令來實現異常處理,現在已經由異常表代替。

 

對於 64 位的數據類型,虛擬機會以高位對齊的方式爲其分配兩個連續的 Slot 空間。Java 語言中明確的(reference 類型則可能是 32 位也可能是 64 位)64 位的數據類型只有 long 和 double 兩種。值得一提的是,這裏把 long 和 double 數據類型分割存儲的做法與 “long 和 double 非原子性協定” 中把一次 long 和 double 數據類型讀寫分割爲兩次 32 位讀寫的做法有些類似,讀者閱讀到 Java 內存模型時可以互相對比一下。不過,由於局部變量建立在線程的堆棧上,是線程私有的數據,無論讀寫兩個連續的 Slot 是否爲原子操作,都不會引起數據安全問題。

虛擬機通過索引定位的方式使用局部變量表,索引值的範圍是從 0 開始至局部變量表最大的 Slot 數量。如果訪問的是 32 位數據類型的變量,索引 n 就代表了使用第 n 個 Slot,如果是 64 位數據類型的變量,則說明會同時使用 n 和 n+1 兩個 Slot。對於兩個相鄰的共同存放一個 64 位數據的兩個 Slot,不允許採用任何方式單獨訪問其中的某一個,Java 虛擬機規範中明確要求瞭如果遇到進行這種操作的字節碼序列,虛擬機應該在類加載的校驗階段拋出異常。

在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,如果執行的是實例方法(非 static 的方法),那局部變量表中第 0 位索引的 Slot 默認是用於傳遞方法所屬對象實例的引用,在方法中可以通過關鍵字 “this” 來訪問到這個隱含的參數。其餘參數則按照參數表順序排列,佔用從 1 開始的局部變量 Slot,參數表分配完畢後,再根據方法體內部定義的變量順序和作用域分配其餘的 Slot。

爲了儘可能節省棧幀空間,局部變量中的 Slot 是可以重用的,方法體中定義的變量,其作用域並不一定會覆蓋整個方法體,如果當前字節碼 PC 計數器的值已經超出了某個變量的作用域,那這個變量對應的 Slot 就可以交給其他變量使用。不過,這樣的設計除了節省棧幀空間以外,還會伴隨一些額外的副作用,例如,在某些情況下,Slot 的複用會直接影響到系統的垃圾收集行爲

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

             byte[] placeholder = new byte[64 * 1024 * 1024]; 

             System.gc(); 

         } 

}

在虛擬機運行參數中加上“-verbose:gc” 來看看垃圾收集的過程,發現在 System.gc() 運行後並沒有回收這 64 MB 的內存:

[GC 68516K->66144K(186880K), 0.0014354 secs]

[Full GC 66144K->66008K(186880K), 0.0127933 secs]

沒有回收 placeholder 所佔的內存能說得過去(full Gc回收之後的內存佔用66008k所以說明沒有被回收),因爲在執行 System.gc() 時,變量 placeholder 還處於作用域之內,虛擬機自然不敢回收 placeholder 的內存。那把代碼修改一下,如下:

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

                   {

                            byte[] placeholder = new byte[64 * 1024 * 1024]; 

                   }

             System.gc(); 

         } 

}

結果:

[GC 68516K->66120K(186880K), 0.0011906 secs]

[Full GC 66120K->66008K(186880K), 0.0093979 secs]

 

加入了花括號後,placeholder 的作用域被限制在花括號之內,從代碼邏輯上講,在執行 System.gc()時,placeholder 已經不可能再被訪問了,但執行一下這段程序,會發現運行結果如下,還是有 64MB 的內存沒有被回收(gc之後使用內存爲6608k說明內存未被回收),這又是爲什麼呢?

在解釋爲什麼之前,先對這段代碼進行第二次修改,在調用 System.gc() 之前加入一行 “int a = 0;”,變成代碼清單如下:

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

                   {

                            byte[] placeholder = new byte[64 * 1024 * 1024]; 

                   }

                   int a =0;

             System.gc(); 

         } 

}

運行結果:

[GC 68516K->66176K(186880K), 0.0012137 secs]

[Full GC 66176K->472K(186880K), 0.0095775 secs]

這個修改看起來很莫名其妙,但運行一下程序,卻發現這次內存真的被正確回收了(gc之後使用內存變爲472k明顯小於64m)。

在代碼清單1 ~ 代碼清單 3 中,placeholder 能否被回收的根本原因是:局部變量中的 Slot 是否還存在關於 placeholder 數組對象的引用。第一次修改中,代碼雖然已經離開了 placeholder 的作用域,但在此之後,沒有任何局部變量表的讀寫操作,placeholder 原本佔用的 Slot 還沒有被其他變量所複用,所以作爲 GC Roots 一部分的局部變量表仍然保持着對它的關聯。這種關聯沒有被及時打斷,在絕大部分情況下影響都很輕微。但如果遇到一個方法,其後面的代碼有一些耗時很長的操作,而前面又定義了佔用了大量的內存、實際上已經不會再使用的變量,手動將其設置爲 null 值(用來代替那句 int a=0,把變量對應的局部變量表 Slot 清空)便不見得是一個絕對無意義的操作,這種操作可以作爲一種在極特殊情形(對象佔用內存大、此方法的棧幀長時間不能被回收、方法調用次數達不到 JIT 的編譯條件)下的 “奇技” 來使用。Java 語言的一本著名書籍《Practical Java》中把 “不使用的對象應手動賦值爲 null” 作爲一條推薦的編碼規則。

雖然代碼清單 1 ~ 代碼清單 3 的代碼示例說明了賦 null 值的操作在某些情況下確實是有用的,但筆者的觀點是不應當對賦 null 值的操作又過多的依賴,更沒有必要把它當做一個普遍的編碼規則來推廣。原因有兩點,從編碼角度講,以恰當的變量作用域來控制變量回收時間纔是最優雅的解決方法,如代碼清單 3 那樣的場景並不多見。更關鍵的是,從執行角度來將,使用賦 null 值的操作來優化內存回收是建立在對字節碼執行引擎概念模型的理解之上的,而概念模型與實際執行過程是外部看起來等效,內部看上去則可以完全不同。在虛擬機使用解釋器執行時,通常與概念模型還比較接近,但經過 JIT 編譯器後,纔是虛擬機執行代碼的主要方式, null 值的操作在經過 JIT 編譯優化後就會被消除掉,這時候將變量設置爲 null 就是沒有意義的。字節碼被編譯爲本地代碼後,對 GC Roots 的枚舉也與解釋執行時期有巨大差別,以前面例子來看,代碼清單 2 在經過 JIT 編譯後,System.gc() 執行時就可以正確回收掉內存,無須寫成代碼清單 3 的樣子。

關於局部變量表,還有一點可能會對實際開發產生影響,就是局部變量不像前面介紹的類變量那樣存在 “準備階段”。通過之前的講解,已經知道類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始化;另外一次在初始化階段,賦予程序員定義的初始值。因此,即使在初始化階段程序沒有爲類變量賦值也沒有關係,類變量仍然具有一個確定的初始值。但局部變量就不一樣,如果一個局部變量定義了但沒有賦初始值是不能使用的,不要認爲 Java 中任何情況下都存在諸如整型變量默認爲 0,布爾型變量默認爲 false 等這樣的默認值。如代碼清單所示,

package com.clazz;

public class TestClzz {

         public static void main(String[] args) { 

         int a;

         System.out.println(a);

         } 

}

這段代碼其實並不能運行,還好編譯器能在編譯期間就檢查到並提示這一點,即便編譯能通過或者手動生成字節碼的方式製造出下面代碼的效果,字節碼校驗的時候也會被虛擬機發現而導致類加載失敗

 

2.操作數棧:(Operand Stack)

對於每個方法調用,JVM都會建立一個操作數棧,供計算使用。保存計算過程的中間結果,同時作爲計算過程中變量的臨時存儲空間。後入先出的棧。方法執行中進行算術運算或者是調用其他的方法進行參數傳遞的時候是通過操作數棧進行的。在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分操作數棧與上面的局部變量表重疊在一塊,這樣在方法調用的時候可以共用一部分數據,無需進行額外的參數複製傳遞

 

同局部變量表一樣,操作數棧的最大深度也在編譯的時候寫入到 Code 屬性的 max_stacks 數據項中。操作數棧的每一個元素可以是任意的 Java 數據類型,包括 long 和 double。32 位數據類型所佔的棧容量爲 1,64 位數據類型所佔的棧容量爲 2。在方法執行的任何時候,操作數棧的深度都不會超過在 max_stacks 數據項中設定的最大值。

當一個方法剛剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧 / 入棧操作。如,在做算術運算的時候是通過操作數棧來進行的,又或者再調用其他方法的時候是通過操作數棧來進行參數傳遞的。

舉個例子,整數加法的字節碼指令 iadd 在運行的時候操作數棧中最接近棧頂的兩個元素已經存入了兩個 int 型的數值,當執行這個指令時,會將這兩個 int 值出棧並相加,然後將相加的結果入棧。

操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。以iadd 指令爲例,這個指令用於整型數加法,它執行時,最接近棧頂的兩個元素的數據類型必須爲 int 型,不能出現一個 long 和一個 float 使用 iadd 命令相加的情況。

 

在概念模型中,兩個棧幀作爲虛擬機棧的元素,是完全相互獨立的。但在大多虛擬機的實現裏都會做一些優化處理,令兩個棧幀出現一部分重疊。讓下面棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調用時就可以共用一部分數據,無須進行額外的參數複製傳遞,重疊的過程如圖 8-2 所示。

https://img-blog.csdn.net/20171018143342550?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMzgyNTk1Mzk=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

 

 

3.動態連接:

每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態連接(Dynamic Linking

Class 文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就轉化爲直接引用,這種轉化成爲靜態解析。另外一部分將在每一次運行期間轉化爲直接引用,這部分成爲動態連接

 

根據動態連接可以動態地確定該棧幀屬於哪個方法

 

4.方法返回地址:

當一個方法開始執行以後,有兩種方法可以退出當前方法:

1.正常完成出口(Normal Method Invocatino Completion):當執行引擎遇到返回字節碼指令,會將返回值傳遞給上層的方法調用者,一般,調用者的PC計數器可以作爲返回地址

2.異常完成出口 (Abrupt Method Invocation Completion):當執行引擎遇到異常,且當前方法體內沒有處理,導致方法退出,此時沒有返回值,返回地址要通過異常處理器表來確定

 

無論何種退出方式,方法退出後,都需返回到方法被調用的位置,程序才能繼續執行,方法返回時可能要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的 PC 計數器的值可作爲返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。

方法退出的過程實際上就等同於把當前棧幀出棧,因此退出時可能執行的三個操作:

  1. 恢復上層方法的局部變量表和操作數棧
  2. 把返回值(如果有的話)壓入調用者棧幀的操作數棧中,
  3. 調整 PC 計數器的值以指向方法調用指令後面的一條指令等。

 

 

5.額外的附加信息

虛擬機規範允許具體的虛擬機實現增加一些規範裏沒有描述的信息到棧幀之中,例如與調試相關的信息,這部分信息完全取決於具體的虛擬機實現。在實際開發中,一般會把動態連接、方法返回地址與其他附加信息全部歸爲一類,稱爲棧幀信息。

 

 

3.本地方法棧(Native Method Stack):線程私有,

本地方法棧和虛擬機棧發揮的作用類似,但本地方法棧爲虛擬機使用的 Native 方法服務。

區別:

虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,

本地方法棧則爲虛擬機使用到的Native方法服務。 

本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

 

4. java堆(堆內存)(Heap):線程共享,

Java堆是被所有線程共享的一塊最大的內存區域在虛擬機啓動時創建,唯一目的是存放對象實例幾乎所有的對象實例和數組都在堆上分配內存。 

Java堆是垃圾收集器管理的主要區域。由於不同對象生命週期不同,進行分代處理(適合GC)。Java堆細分爲新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation3個區域【JDK8前】。JDK8後Heap = { Old + NEW = {Eden, from, to} }
Java堆可以處於物理上不連續的內存空間中,邏輯上連續是連續即可。若在堆中沒有完成實例分配,且堆無法再擴展時,會拋出OutOfMemoryError異常

 

詳細分析:並不是所有的對象實例都存儲在堆空間的

https://img-blog.csdn.net/20171014184632761?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaG90cG90cw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

1.新生代

Eden 與 2個Survivor Space(S0S1FromTo)構成,大小通過-Xmn參數指定,Eden Survivor Space 的內存大小比例默認爲8:1【這樣空間利用率可以達到90%】,可通過-XX:SurvivorRatio 參數指定,如新生代爲10M 時,Eden分配8M,S0和S1各分配1M。

1)、Eden伊甸園:大多情況下,對象在Eden中分配,Eden沒有足夠空間時,會觸發一次Minor GC

2)、Survivor倖存者:新生代發生GC(Minor GC)時,存活對象會反覆在S0S1之間移動,同時清空Eden區,當對象從Eden移動到Survivor或在Survivor間移動時,對象的GC年齡自動累加,當GC年齡超過默認閾值15時,該對象將被移動到老年代,可通過參數-XX:MaxTenuringThreshold 對GC年齡閾值設置。

 

2.老年代

空間大小即-Xmx 與-Xmn 兩個參數之差,用於存放經過幾次Minor GC之後依舊存活的對象。當老年代的空間不足時,會觸發Major GC/Full GC,速度一般比Minor GC10倍以上。

 

3.永久代(JDK1.7前)

用於存放靜態類型數據,如 Java Class, Method 等。JDK8前的HotSpot實現中,類的元數據如方法數據、方法信息(字節碼,棧和變量大小)、運行時常量池、已確定的符號引用和虛方法表等被保存在永久代中,32位默認永久代的大小爲64M,64位默認爲85M,可通過參數-XX:MaxPermSize進行設置,一旦類的元數據超過了永久代大小,就會拋出OOM異常。

 

JDK8的HotSpot中,把永久代從Java堆中移除了,並把類的元數據直接保存在本地內存區域(堆外內存),稱爲元空間。

 

逃逸分析和棧上分配優化技術是降低GC回收頻率和提升GC回收效率的有效方法,

 

 

5.方法區(Method Area):線程共享

方法區主要保存的信息是類的元數據。

方法區存儲虛擬機加載的類信息常量、靜態變量、即時編譯器編譯後的代碼等數據。(深入JVM中的描述)

線程共享,用於存儲已被虛擬機加載的信息【JDK6時,String等常量信息置於方法區,JDK7時移動到堆】,通常和永久代(Perm)關聯在一起。一般保存類相關信息,具體不同虛擬機實現

 

 

方法區大小一般在程序啓動後一段時間內就固定了,JVM運行一段時間後,需要加載的類通常都已經加載到JVM中了。

方法區有點特殊,它不像其他Java堆一樣會頻繁地被GC回收器回收,它存儲的信息相對比較穩定,但是它仍然佔用了Java堆(永久代)的空間,所以仍然會被JVM的GC回收器來管理。

由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出。

 

關於方法區: (移除永久代的工作在JDK7就開始了)
JDK7前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時可以設置一個固定值,不可變; 
JDK7
中,存儲在永久代的部分數據就轉移到Java HeapNative memory。但還存在永久代,並沒有完全移除,譬如符號引用(Symbols)轉移到了native memory;字符串常量池(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap 
java8
中,取消永久代,方法存放於元空間(Metaspace),元空間仍與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中。 
Native memory
:本地內存,也稱C-Heap,是供JVM自身進程使用的。當Java Heap空間不足時會觸發GC,但Native memory空間不夠卻不會觸發GC 

 

方法區是JVM的規範。永久代和元空間都是JVM規範的一種實現。只有HotSpot纔有永久代。其他諸如JRockitOracle)、J9IBM)都沒有。

 

兩個屬性:

-XX:MaxPermSize設置上限 
-XX:PermSize設置最小值 例:VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M 

默認情況下,-XX:MaxPermSize爲64M,如果系統產生大量的類,就需要設置一個相對合適的方法區,避免永久區內存溢出。

 

關於運行時常量池:

Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。 
運行時常量池(Runtime Constant Pool)相對於Class文件常量池的一個重要特徵是具備動態性:即除了Class文件中常量池的內容能被放到運行時常量池外,運行期間也可能將新的常量放入池中,比如String類的intern()方法

 

當兩個線程同時需要加載一個類型時,只有一個類會請求 ClassLoader  加載,另一個線程則會等待

 

方法區的垃圾回收:

在 HotSpot虛擬機中,方法區也被稱爲永久區,是一塊獨立於 Java 堆的內存空間。永久區中的對象也可被 GC 回收,只是GC 的對應策略與Java 堆空間略有不同。GC 針對永久區的回收,通常主要從兩個方面分析:

一是 GC 對永久區常量池的回收,

二是永久區對類元數據的回收。

HotSpot 虛擬機對常量池的回收策略是很明確的, 只要常量池中的常量沒有被任何地方引用 , 就可以被回收。

 

String. intern()方法的含義:

如果常量池中已經存在當前String,則返回池中的對象:

如果常量池中不存在當前 String對象,則先將 String 加入常量池,井返回池中的對象引用。

因此,不停地將 String 對象加入常量池會導致永久區飽和,如果 GC 不能回 收永久區的這些常量數據,那麼就會拋出OutofMemoryError 錯誤。

 

 

逃逸分析與棧上分配:

逃逸分析是指分析指針動態範圍的方法,它同編譯器優化原理的指針分析和外形分析相關聯。

計算機軟件方面,逃逸分析指的是計算機語言編譯器語言優化管理中,分析指針動態範圍的方法。

通俗點講,如果一個對象的指針被多個方法或線程引用時,可以稱這個指針發生了逃逸。

 

3中指針逃逸場景:全局變量賦值、方法返回值、實例引用傳遞

public  class  G {

public  static B  b;

public  void  globalVariablePointerEscape() {// 給全局變量賦值,發生逃逸

b=new  B();

  }

public  B  methodPointerEscape() {  // 方法返回值,發生逃逸

return  new  B();

  }

public  void  instancePassPointerEscape() {

methodPointerEscape() .printClassName(this);  //實例引用發生逃逸

}

}

逃逸的分析研究對於 Java 虛擬機的好處:

Java 對象總是在堆中被分配的,對象的創建和回收對系統的開銷是很大的。

JDK6 裏的 Swing 內存和性能消耗的瓶頸就是由於發生逃逸所造成的。棧裏只保存了對象的指針,當對象不再被使用後,需要依靠 GC 來遍歷引用樹井回收內存,如果對象數量較多,會給 GC 帶來較大壓力,也直接或間接地影響了應用的性能。減少臨時對象在堆內分配的數量,無疑是最有效的優化方法。

 

 

一般是在方法體內,聲明瞭一個局部變量,且該變量在方法執行生命週期內未發生逃逸,因爲在方法體內未將引用暴露給外面,按照JVM內存分配機制,首先會在堆內創建變量類的實例,然後將返回的對象指針壓入調用攏,繼續執行。這是JVM優化前的方式。

可以採用逃逸分析原理對 JVM 進行優化,即針對攏的重新分配方式。首先要分析並且找到未逃逸的變量,將變量類的實例化內存直接在棧裏分配(無須進入堆),分配完成後,繼續在調用技內執行,最後線程結束,找空間被回收,局部變量對象也被回收。通過這種優化方式,與優化前的方式的主要區別在於棧空間直接作爲臨時對象的存儲介質,從而減少了臨時對象在堆內的分配數量。

基於逃逸分析的JVM優化原理很簡單,但是在應用過程中還是有諸多因素需要被考慮。如,由於與 Java 的動態性有衝突,所以逃逸分析不能在靜態編譯時進行,必須在JIT裏完成。

因爲你可以在運行時通過動態代理改變一個類的行爲,此時,逃逸分析是無法得知類已經變化

 

public void mymethod() {

V  v=new  V();

//use  v

v=null ;

}

在這個方法中創建的局部對象被賦給了 v, 但是沒有返回,沒有賦給全局變量等操作,因此這個對象是沒有逃逸的,是可以在運行時在棧上進行分配和銷燬的對象。沒有發生逃逸的對象,由於生命週期都在一個方法體內,因此它們可以在運行時在棧上分配井銷燬。

這樣在 TIT 編譯 Java 僞代碼時,如果能分析出這種代碼,那麼非逃逸對象其創建和回收就可以在棧上進行,從而能大大提高 Java 的運行性能。

 

爲什麼要在逃逸分析之前進行內聯分析呢?這是因爲往往有些對象在被調用過程中創建井返回給調用過程,調用過程使用完該對象就被銷燬了。這種情況下如果將這些方法進行內聯,它們就由兩個方法體變成一個方法體了,這種原來通過返回傳遞的對象就變成了方法內的局部對象,就變成了非逃逸對象了,這樣這些對象就可以在同一椅上進行分配了。

 

Java7 開始支持對象的棧分配和逃逸分析機制。這樣的機制除了能將堆分配對象變成棧分配對象外,逃逸分析還有其他兩個優化應用。

·同步消除。線程同步的代價是相當高的,同步的後果是降低併發性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問,如果只被一個線程訪問,那麼對該對象的同步操作就可以轉化成沒有同步保護的操作,這樣就能大大提高併發性和性能。

·矢量替代。逃逸分析方法如果發現對象的內存存儲結構不需要連續進行的話,就可以將對象的部分甚至全部都保存在 CPU 寄存器內,這樣能大大加快訪問速度。

 

Java7 完全支持棧式分配對象,JIT支持逃逸分析優化,此外 Java7 還默認支持 OpenGL 的加速功能。

 

 

 

 

Java虛擬機提供的優化技術,基本思想是一些不可能被其他線程訪問的私有對象,可以打散分配到棧上,不分配到堆上,分配到棧上能在方法調用結束後自動銷燬,不需要垃圾回收器,性能好。

運行參數爲第一個時,會優化,在棧上分配,系統性能較高

運行參數爲第二個時,堆上分配,系統性能不高,因爲gc要不斷回收

棧上分配一般用於小對象【因爲棧很小】,在沒有逃逸的情況下,可以直接分配在棧上,

直接分配在棧上,可以自動回收,減輕GC壓力

大對象或逃逸對象無法再棧上分配

好處是方法結束後,分配的東西隨着棧幀被銷燬(也就是c++的new和delete的思想)

逃逸:如果這個對象在當前線程被用,其他線程也被用的時候,不能分配在棧上

 

 

直接內存和運行時常量池

運行時常量池:屬於方法區。那些字符串什麼的都在方法區的常量池

運行時可以產生常量

Intern可以把對象對應區域變成常量區

普通的常量成爲字節碼常量

運行時產生的常量叫運行時常量

 

直接內存:不是java虛擬機規範的一個區域。但確實真的存在的。

 

 

探究:JVM內存分配(不同版本)

https://img-blog.csdn.net/20171126114357224?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMjU2Mjk0Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

JDK7及前期的JDK版本中,JVM空間可以分成三個大區,新生代、老年代、永久代

新生代劃分爲三個區,Eden區,兩個倖存區。

一個對象被創建以後首先被放到新生代中的Eden內存中,如果存活期超兩個Survivor之後就會被轉移到老年代(Old Generation)中。

 

永久代中存放着對象的方法、變量等元數據信息

如果永久內存不夠,會得到如下錯誤:java.lang.OutOfMemoryError: PermGen
 

JDK8 HotSpot JVM 將移除永久區,用本地內存來存儲類元數據信息並稱之爲:元空間(Metaspace);

所以JDK8中JVM空間分爲兩大區:新生代、老年代。

https://img-blog.csdn.net/20171126114441180?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMjU2Mjk0Mw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

永久代被移除後它的JVM參數:PermSize 和 MaxPermSize 會被忽略並給出警告(如果在啓用時設置了這兩個參數)。
 

關於元空間(Metaspace)

Metaspace 容量:

默認,類元數據只受可用的本地內存限制(容量取決於是32位或是64位操作系統的可用虛擬內存大小)。
JVM參數MaxMetaspaceSize用於限制本地內存分配給類元數據的大小。若沒有指定這個參數,元空間會在運行時根據需要動態調整。
Metaspace 垃圾回收:

對於僵死的類及類加載器的垃圾回收將在元數據使用達到“MaxMetaspaceSize”參數的設定值時進行。
適時地監控和調整元空間對於減小垃圾回收頻率和減少延時很有必要。持續的元空間垃圾回收說明,可能存在類、類加載器導致的內存泄漏或是大小設置不合適。

 

元空間特性:

1.      充分利用了Java語言規範中的好處:類及相關的元數據的生命週期與類加載器的一致

2.      每個加載器有專門的存儲空間

3.      只進行線性分配

4.      不會單獨回收某個類

5.      省掉了GC掃描及壓縮的時間

6.      元空間裏的對象的位置是固定的

7.      如果GC發現某個類加載器不再存活了,會把相關的空間整個回收掉

 

元空間的內存分配模型

1.      絕大多數的類元數據的空間都從本地內存中分配

2.      用來描述類元數據的類也被刪除了

3.      分元數據分配了多個虛擬內存空間

4.      給每個類加載器分配一個內存塊的列表。塊的大小取決於類加載器的類型; sun/反射/代理對應的類加載器的塊會小一些

5.      歸還內存塊,釋放內存塊列表

6.      一旦元空間的數據被清空了,虛擬內存的空間會被回收掉

7.      減少碎片的策略

 

 

永久代:

JDK6和JDK7中,方法區可理解爲永久區(Perm)。JDK8中,永久區被徹底移除,取而代之的是元數據區,

元空間和永久代的聯繫與區別

聯繫:元空間的本質和永久代類似,都是對JVM規範中方法區的實現。(方法區是規範,元空間和永久代是實現)

區別:

1元空間並不在虛擬機中,而是使用本地內存,默認情況下,元空間的大小僅受本地內存的限制。與永久區不同,如果不指定大小,虛擬機會消耗所有可用的內存

可通過參數來指定元空間的大小:

  -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。

  -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

  除了上面兩個指定大小的選項以外,還有兩個與 GC 相關的屬性:

  -XX:MinMetaspaceFreeRatio,在GC之後,最小的Metaspace剩餘空間容量的百分比,減少爲分配空間所導致的垃圾收集

  -XX:MaxMetaspaceFreeRatio,在GC之後,最大的Metaspace剩餘空間容量的百分比,減少爲釋放空間所導致的垃圾收集

2永久區可使用-XX:PermSize和-XX:MaxPermSize指定。默認爲64M。大的永久區可保存更多的類信息,如果系統使用一些動態代理,有可能會在運行時生產大量的類,要設置合理的數值,確保永久區不會內存溢出。

 

永久代溢出問題:

永久代是存在垃圾的,理論上使用的Java類越多,需要佔用的內存也會越多,還有一種情況是可能會重複加載同一個類。通常情況下JVM只會加載一個類到內存一次,但是如果是自己實現的類加載器會出現重複加載的情況,如果PermGen區不能對已經失效的類做卸載,可能會導致PermGen區內存泄漏

PermGen區內存回收問題。通常一個類能夠被卸載,有如下條件需要被滿足:

在Java堆中沒有對錶示該類加載器的java.lang.ClassLoader對象的引用。
Java堆沒有對錶示類加載器加載的類的任何java.lang.Class對象的引用。
在Java堆上該類加載器加載的任何類的所有對象都不再存活(被引用)。

 

垃圾回收對永久代不顯著。但有些應用可能動態生成或調用一些Class,如CGLib 等,就需要設置一個比較大的持久代空間來存放這些運行過程中動態增加的類型。但是需要設置多大才不會OOM並且不會浪費空間呢?

 

爲什麼移除永久代?

1、字符串存在永久代中,容易出現性能問題和內存溢出

2、永久代大小不容易確定,PermSize指定太小容易造成永久代OOM

3、永久代會爲 GC 帶來不必要的複雜度,並且回收效率偏低

4、Oracle 可能會將HotSpot 與 JRockit 合二爲一。(JRockit沒有永久代)

 

對永久代的調優過程非常困難,永久代的大小很難確定,涉及到太多因素,如類的總數、常量池大小和方法數量等,永久代的數據可能會隨着每一次Full GC而發生移動

類的元數據保存在本地內存中,元空間的最大可分配空間就是系統可用內存空間,可避免永久代的內存溢出問題,不過需要監控內存的消耗情況,一旦發生內存泄漏,會佔用大量的本地內存。

 

移除永久代的應用:在JDK7中, 把原本放在永久代的字符串常量池移出, 放在堆中. 爲什麼這樣做呢?

因爲使用永久代來實現方法區不是個好主意, 很容易遇到內存溢出的問題. 通常使用PermSize和MaxPermSize設置永久代的大小, 這個大小就決定了永久代的上限, 但是不是總是知道應該設置爲多大的, 如果使用默認值容易遇到OOM錯誤

 

移除永久代後數據存放的策略:

類的元數據, 字符串池, 類的靜態變量將會從永久代移除, 放入Java heap或者native memory。

建議JVM的實現中將類的元數據放入 native memory, 將字符串池和類的靜態變量放入java堆中. 這樣加載多少類的元數據就不在由MaxPermSize控制, 而由系統的實際可用空間來控制.

這樣做的原因: 減少OOM只是表因, 深層原因是要合併HotSpot和JRockit的代碼, JRockit沒永久代, 但是運行良好, 也不需要開發運維人員設置這麼一個永久代的大小。不用擔心運行性能問題, 在覆蓋到的測試中, 程序啓動和運行速度降低不超過1%, 但是這一點性能損失換來了更大的安全保障。

 

 

JDK7之前的HotSpot,字符串常量池的字符串被存儲在永久代中,可能導致一系列的性能問題和內存溢出錯誤。在JDK8中,字符串常量池中只保存字符串的引用。

 

關於方法區: (移除永久代的工作在JDK7就開始了)
JDK7前,方法區位於永久代(PermGen),永久代和堆相互隔離,永久代的大小在啓動JVM時可以設置一個固定值,不可變; 
JDK7
中,存儲在永久代的部分數據就轉移到Java HeapNative memory。但還存在永久代,並沒有完全移除,譬如符號引用(Symbols)轉移到了native memory;字符串常量池(interned strings)轉移到了Java heap;類的靜態變量(class statics)轉移到了Java heap 
java8
中,取消永久代,方法存放於元空間(Metaspace),元空間仍與堆不相連,但與堆共享物理內存,邏輯上可認爲在堆中。 
Native memory
:本地內存,也稱C-Heap,是供JVM自身進程使用的。當Java Heap空間不足時會觸發GC,但Native memory空間不夠卻不會觸發GC 

 

通過一段程序來比較 JDK 1.6 與 JDK 1.7及 JDK 1.8 的區別,以字符串常量爲例:

public class StringOomMock {
   
static String  base = "string";
    public static void
main(String[] args) {
        List<String> list =
new ArrayList<String>();
        for
(int i=0;i< Integer.MAX_VALUE;i++){   // 不斷產生新的字符串,快速消耗內存
           
String str = base + base;
           
base = str;
           
list.add(str.intern());
       
}
    }
}

分別使用-XX:PermSize=8m -XX:MaxPermSize=8m參數進行運行

JDK8多采用-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m方式運行

JDK1.6:

JDK1.7:

JDK1.8:

-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m 方式:

https://images2015.cnblogs.com/blog/820406/201603/820406-20160327010233933-699106123.png  

這次不再出現永久代溢出,而是出現了元空間的溢出

JDK 6下,會出現“PermGen Space”的內存溢出,

在JDK7和 JDK8 中,會出現堆內存溢出,

並且 JDK 8中 PermSize 和 MaxPermGen 已經無效。

可大致驗證 JDK 7 和 8 將字符串常量由永久代轉移到堆中,並且 JDK 8 中已經不存在永久代的結論。

 

 

Q:永久帶和方法區的區別?Java8後的元數據區又是怎麼回事?

永久代和元數據區都是對方法區的實現。

方法區的內存回收目標主要是針對常量池的回收和對類型的卸載。

當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常

 

HotSpot 虛擬機把 GC 分代收集擴展到方法區,即使用永久代來實現方法區,像 GC 管理 Java 堆一樣管理方法區,從而省去專門爲方法區編寫內存管理代碼,內存回收目標是針對常量池的回收和堆類型的卸載;

 

運行時常量池 :方法區的一部分。class文件中除了有關的版本、字段、方法、接口等描述信息外、還有一項信息是常量池,用於存放編輯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

Java語言並不要求常量一定只有編輯期才能產生,也可能將新的常量放入池中,這種特性被開發人員利用得比較多是便是String類的intern()方法。 

當常量池無法再申請到內存時會拋出OutOfMemoryError異常

 

垃圾回收在方法區的行爲

異常的定義

 

 

 

關於直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,但是Java程序中重要的組成成份。

直接內存跳過了Java堆,使Java程序可直接訪問原生堆空間,一定程度上加快了內存空間的訪問速度。

廣泛用於NIO中。

直接內存使用達到上線時,會觸發垃圾回收,如果不能有效釋放空間,也會引起系統的OOM。

 

相關配置參數:

-XX:MaxDirectMEmorySize:如果不設置默認值爲最大堆空間,即-Xmx。

 

JDK1.4加入了NIO(New Input/Output)類,引入一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。因爲避免了在Java堆和Native堆中來回複製數據,提高了性能。

 

 

 

 

 

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