在前面的幾篇博文中,我們一起簡單的了解jvm的基本知識,例如jvm對字符串的處理等等,或許大家看完后就把這當成一條准則來記住了,但是一些比較好奇的朋友有沒有想過,這是為什么呢?他的原理是什么呢?下面就讓我們開始一步一步的深入學習。
在這篇博文中呢,我打算主要就講Java內存區域與內存溢出異常吧。下面言歸正傳吧。
1.Java虛擬機運行時數據區
在前面的幾篇博文中,我們只是簡單的把內存區域分為了堆和棧,但其實,這種分法是十分粗糙的,jvm在實際運行的時候,內存區域的划分絕對不是那么簡簡單單的就兩塊,我們一起看下面這個圖就知道了。
從上圖我們知道了,JVM虛擬機運行時數據區主要划分為:方法區、虛擬機棧、本地方法棧、堆、程序計數器。
1.1程序計數器
雖然在上圖中,程序計數器這塊占用的區域畫的很大,但其實,在內存中,它只是較小的一塊內存空間。
1)生命周期:線程私有的,與它所綁定的線程相同。
2)作用:當前線程所執行的字節碼的行號指示器,簡單的可以理解成,程序計數器記錄着下一行要執行的代碼行數,比如我們經常寫的分支、循環、跳轉都是通過改變該計數器的值來完成的。如果一個線程正在執行的是Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器則為空。
3)程序運行時該區域可能發生的異常:此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
1.2Java虛擬機棧
其實這里所說的棧,就是前面我們博文中粗糙分法堆棧中的棧了。精確的講的話,其實是Java虛擬機棧中的局部變量表部分吧;同時這里是專門為虛擬機執行java方法服務的,為什么這么說呢?因為內存區域中還有本地方法棧,本地方法棧是只為虛擬機使用到的Native方法服務的。
1)生命周期:線程私有的,與它所綁定的線程相同。
2)作用:每個方法被執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。其中局部變量表存儲的是以下類型:
①基本數據類型:boolean, byte, char, short, int, float, long, double;
②對象引用:reference類型,它不等於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用文章,也可能是指向一個對象句柄或其他與此對象有關的位置;
③returnAddress類型:指向了一條字節碼指令的地址,簡單的說,就是下一條要執行的代碼的地址;
3)程序運行時該區域可能發生的異常:
①線程請求棧深度 > 虛擬機允許深度,拋出StackOverflowError;eg:
1 package edu.outofmemoryerror.heap; 2 3 public class JavaStackSOF { 4 private int stackLength = 1; 5 6 public void stackLeak() { 7 stackLength++; 8 stackLeak(); 9 } 10 11 /** 12 * -Xss128K 13 * @param args 14 */ 15 public static void main(String[] args) { 16 JavaStackSOF sf = new JavaStackSOF(); 17 sf.stackLeak(); 18 } 19 }
運行結果:
注意在運行的時候注意要改變那些參數;如果是通過控制台命令來執行的,可以直接跟在Java命令之后書寫,如果是通過Eclipse來執行的可以在下面頁面添加上再來執行,如圖:
②該棧允許動態拓展,當無法申請到足夠的空間時,拋出OutOfMemoryError異常;
1.3本地方法棧
本地方法棧跟虛擬機棧相似,他們的區別在前面已經提過了:本地方法棧是只為虛擬機使用到的Native方法服務的,而java虛擬機棧是專門為虛擬機執行java方法服務的。
1)生命周期:線程私有的,與它所綁定的線程相同。
2)作用:為使用到的Native方法服務。
3)程序運行時該區域可能發生的異常:跟虛擬機棧一樣。
1.4Java堆(Java Heap)
對於很多應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊。
1)生命周期:線程共享的,在虛擬機啟動時就創建
2)作用:存放對象實例,幾乎所有的對象實例以及數組都在這里分配內存。
3)程序運行時該區域可能發生的異常:堆中沒有內存完成實例分配或者堆無法再拓展時,將會拋出OutOfMemoryError異常,eg:
1 package edu.outofmemoryerror.heap; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 public class HeapOOM { 7 static class OOMObject { } 8 9 /** 10 * -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError 11 * @param args 12 */ 13 public static void main(String[] args) { 14 List<OOMObject> list = new ArrayList<OOMObject>(); 15 while (true) { 16 list.add(new OOMObject()); 17 } 18 } 19 }
運行結果:
1.5方法區(Method Area)
方法區有一個別名叫做非堆(Non-Heap),對於習慣在HotSpot虛擬機上開發和部署程序的開發者來說,它也叫“永久代”。
1)生命周期:線程共享的,在虛擬機啟動時就創建
2)作用:存儲已被虛擬機加載的類信息、常量、靜態變量,也就是編譯器編譯后的代碼數據。
3)程序運行時該區域可能發生的異常:當方法區無法滿足內存分配需求時,將會拋出OutOfMemoryError異常。比如java.lang.OutOfMemoryError,這個錯誤其實對於經常使用SSh框架進行開發的我們應該很熟悉,它就是由於方法區空間不足導致的內存溢出。
因為該區域中的垃圾收集行為是比較少出現的,所以對象相當於永久不被回收一般,所以在這個區域就叫做“永久代”了。
1.5.1運行時常量池
運行常量池是方法區的一部分。
1)生命周期:線程共享的,在虛擬機啟動時就創建
2)作用:存放Class文件中類的版本、字段、方法、接口等描述信息外,還有一些信息是常量池,用於存放編譯期間生產的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行是常量池中。
3)程序運行時該區域可能發生的異常:運行時是方法區的一部分,自然收到方法區內存的限制,當常量池無法在申請到內存是就會拋出OutOfMemoryError異常
看到這里,不知道大家會不會想起前面一篇博文中我們說的JVM對字符串的處理呢?
沒錯,在那篇博文中講的Java字符串緩沖池,其實就是運行時常量池的一部分,而運行池是在方法區中的,方法區中的垃圾收集行為是幾乎沒有的,相當於不會被回收。這就是前面的JVM對字符串處理的原理了。
1.6直接內存
其實直接內存不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中的一部分,但是這部分也頻繁的使用,也可能會導致OutOfMemoryError異常的出現。
2引用對象的訪問
在之前我們有一篇博文講了關於對象與內存管理的知識,但是那里只是比較簡單的,下面我們更加深入了解一下,在Java語言中,對象訪問是如何進行的?
其實對象訪問並不像前面那篇博文中講的那么簡單只涉及到堆和棧,因為即使是最簡單的訪問,也會涉及到Java虛擬機棧,Java堆,方法區這三個重要的內存區域。之所以只講堆和棧是因為開發人員在大多數情況下關注的最多的是堆和棧兩個區域。
關於基本類型的訪問,在前面那篇博文中我們已經講了,下面,我們先提一下相關的概念:
1)對象類型數據:指的是該對象的對象類型,父類,實現的接口,方法等,其實就是前面說的,方法區存儲的是加載的對象的信息。
2)對象實例數據:指的是該類實例化后的對象的實例變量的值。
接着,我們繼續重點深入講解下關於引用類型對象的兩種訪問方式:
1)使用句柄方式訪問
使用這種對象的訪問方式的優勢是:存儲的是穩定的句柄地址,在對象移動時只會改變句柄中的實例數據指針,而reference本身不用改變。
使用句柄池訪問方式,Java堆中將會划分出一塊內存來作文句柄池,reference中存儲的是對象的句柄地址,而句柄中包含了對象實力數據和類型數據各自的具體地址信息。
其實從上圖中,我們還能看出一個知識點,那就是:64位長度的long和double類型的數據會占用2個局部變量空間
2)使用直接指針訪問方式
使用這種訪問方式的優勢是:訪問速度快。