天天看點

深入了解JVM虛拟機筆記——第2章 JAVA記憶體區域與記憶體溢出異常一、JVM運作時資料區域二、對象通路

一、JVM運作時資料區域

JVM在執行JAVA程式的時候會将它管理的記憶體劃分為幾個資料區域,如下圖所示:

深入了解JVM虛拟機筆記——第2章 JAVA記憶體區域與記憶體溢出異常一、JVM運作時資料區域二、對象通路

1、程式計數器

程式計數器是一塊較小的記憶體空間,其作用是作為目前線程所執行位元組碼的行号訓示器。位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支、循環、跳轉、異常處理、線程恢複等功能都是依靠這個計數器來實作的。

Java虛拟機實作多線程是通過線程輪流切換并配置設定處理器執行時間的方式實作的。一個處理器(多核中的一個核)在一個時刻是能執行一個線程的一條指令,是以為了線程切換後能回到原來的位置,每個線程應該有單獨的程式計數器。是以程式計數器是線程私有的記憶體區域。

如果正在執行的是native方法,則程式計數器為空。native方法是使用非java語言實作的方法,它的作用是擴充jvm,有了它,java可以做任何層次的應用,但是使用了native方法的程式可移植性會下降。

程式計數器區域不會與OutOfMemoryError。也是jvm記憶體區域中唯一不會發生該錯誤的區域。

2、Java虛拟機棧

線程私有。它描述的是java方法執行時的記憶體模型:每個方法被執行的時候會建立一個棧幀(Stack Frame)(方法運作時期的基礎資料結構,即java虛拟機棧的棧元素),用來存儲方法的局部變量表、操作棧、動态連結、方法出入口等。每一個方法被調用到執行完成的過程,對應着一個棧幀在java虛拟機棧中入棧到出棧的過程。

我們常說的棧記憶體和堆記憶體中,棧記憶體就是指java虛拟機棧的局部變量表。布局變量表的大小是在編譯期間配置設定的,其空間大小在進入一個方法是就可以确定,運作過程中不會改變。

Java虛拟機棧可能出現兩種異常:

(1)線程請求的棧深度超出虛拟機運作的最大深度,抛出StackOverflowError。

(2)擴充虛拟機棧時無法申請到足夠的記憶體,抛出OutOfMemoryError

這兩種異常實際是對一件事情額不同描述。一般在單線程環境下抛出StackOverflowError異常,多線程環境下抛出OutOfMemoryError異常。需要注意的是:多線程環境下,每個線程配置設定到的棧容量越大,可以建立的線程數就越少。是以,在多線程環境下發生記憶體溢出時,在不能減少線程數或者更換64位虛拟機的情況下,隻能通過減少最大堆和減少棧容量來換取更多的線程。

java虛拟機棧容量由-Xss參數設定。

3、本地方法棧

與java虛拟機棧功能相似,為native方法服務,java虛拟機棧是為java方法(位元組碼)服務。有的虛拟機将這兩個區域合二為一。本地方法棧也是線程私有的。本地方法棧大小是由-Xoss參數設定的,這個參數實際上無效,棧容量隻由-Xss參數設定。

4、Java堆

Java堆是線程共享的記憶體區域,在虛拟機啟動的時候創,用來存放對象執行個體。幾乎所有的對象都在堆上配置設定,JIT編譯器的發展和逃逸技術的發展産生了棧上配置設定、标量替換等優化技術。堆是垃圾回收管理的主要區域,也稱“GC堆”(Garbage Collection Heap)。堆在實體上可以不連續,但是邏輯上必須連續。堆的大小可以固定,也可以動态擴充(通過-Xms和-Xmx設定,兩個值設定為相同時堆大小固定)。

當堆出現OutOfMemoryError異常的時候,可以通過記憶體映像分析工具(如Eclipse Memory Analyzer)對dump出來的對存儲快照進行分析。分析時,首先厘清楚是出現了記憶體洩漏還是記憶體溢出。如果是記憶體洩漏,進一步通過工具檢視洩漏對象的GC Root引用鍊。如果不存在洩漏,就應該檢查堆參數(-Xms和-Xmx),對比實體機記憶體看其是否可以調大。還要從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長,嘗試減少程式運作時的記憶體消耗。

5、方法區

方法區也是線程共享的,用來存儲已經被虛拟機加載的類的資訊,常量、靜态變量、及時編譯器編譯後的代碼等資料。垃圾回收在這個區域進行比較少,主要是常量池的回收和類型的解除安裝。我們可以通過-XX:PermSize和-XX:MaxPermSize設定方法區的大小。在經常動态生成大量class的應用中,容易産生方法區記憶體溢出,要特别注意類的回收狀況。

為了把堆中的垃圾分代回收機制擴充到方法區中,常常将方法區稱為“永久代”。

6、運作時常量池

運作時常量池是方法區的一部分,用來存放編譯期間生成的各種字面量和符号引用(類加載後存入)。允許期間也可能将新的常量放入常量池,比如String類的intern()方法。我們可以通過-XX:PermSize和-XX:MaxPermSize設定方法區的大小,進而間接設定常量池的大小。

7、直接記憶體

直接記憶體不是虛拟機運作時資料區域的一部分。但是虛拟機運作的時候,直接記憶體也會被頻繁地使用。jdk1.4之後加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式,它可以使用native函數庫直接配置設定到堆外記憶體,然後通過一個存儲在java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在java堆和native堆中來回複制資料。

在進行虛拟機記憶體設定(-Xmx)時,直接記憶體容易被伺服器管理者忽略,使得各個區域記憶體的總和大于實體記憶體的限制(包括實體級别和作業系統級别的限制),進而導緻動态擴充記憶體是出現OutOfMemoryError。

直接記憶體容量可以通過 -XX:MaxDirectMemorySize指定,預設情況下跟java堆的最大值一樣。DirectByteBuffer配置設定記憶體時抛出的記憶體溢出異常是通過計算得知的,并沒有真正向作業系統申請。

二、對象通路

在java中,最簡單的對象通路都涉及Java棧、Java堆、方法區三個記憶體區域。比如

Object obj=new Object();

這條語句出現在方法體中時,“Object obj”将反映到java棧的本地變量表中,作為一個引用類型的資料出現。“new Object()”會反映到Java堆中,形成一塊存儲了Object類型所有執行個體資料的結構化記憶體,這塊記憶體的長度是不固定的。另外,java堆中還要儲存能查找到此對象類型資料(如對象類型、父類、實作接口、方法等)的位址資訊,而這些類型資訊本身是存儲在方法區中。

通過引用類型通路對象的方式有兩種:使用句柄和直接指針。

使用句柄通路:在java堆中配置設定出一塊記憶體作為句柄池,引用中存儲的是對象的句柄位址,而句柄中包含對象執行個體資料和類型資料的具體位址。如下圖所示:

深入了解JVM虛拟機筆記——第2章 JAVA記憶體區域與記憶體溢出異常一、JVM運作時資料區域二、對象通路

使用句柄通路的好處:在對象被移動(在垃圾回收時很常見)時無需改變引用變量的值,隻需要改變句柄中指向對象資料的位址資訊。

直接指針通路:引用中直接存儲的是對象位址,需要額外考慮對象的類型資訊的放置。使用直接位址通路的好處是節省了一次指針定位的開銷,速度更快,在頻繁通路對象時優勢明顯。

參考部落格:

https://www.cnblogs.com/fengbs/p/7029013.html

https://www.cnblogs.com/parryyang/p/5726077.html