《深入理解java虛擬機器》讀書筆記2(java記憶體區域與OOM)
1.java執行時記憶體劃分
》程式計數器
學過彙編的童鞋都知道程式執行時會記錄當前執行的位置,以便確認接下來執行什麼。這裡的程式計數器就是用來儲存當前執行緒所執行位元組碼的行號指示器,也就是地址,位元組碼指示器通過改變程式計數器的值來指定下一條執行的指令,諸如迴圈,跳轉,異常處理,執行緒恢復等都是這樣。
而這樣做必須保證順序執行,否則就亂套了。我們知道順序執行的最小單位是執行緒,所以對於每條執行緒必須擁有獨立的程式計數器,如圖所示這類記憶體稱為執行緒隔離(執行緒私有)的資料區。
java開發中時常會涉及到呼叫native方法,比如安卓手機的核心是linux,開發安卓應用程式比如視訊編解碼,需要用native的c語言方法呼叫。呼叫java方法時,程式計數器記錄的是虛擬機器器位元組碼指令的地址,這時候計數器值為空。
程式計數器的記憶體區域是java虛擬機器規範中唯一一個沒有規定任何出現OOM異常的區域。
》java虛擬機器棧
從上圖看出這又是一個執行緒私有的記憶體區域。它是幹什麼用的呢?虛擬機器棧描述的是java方法執行時的記憶體模型。每個方法執行開始,都會建立一個棧幀,用於儲存區域性變量表,運算元棧(虛擬機器把運算元棧作為它的工作區——大多數指令都要從這裡彈出資料,執行運算,然後把結果壓回運算元棧。),動態連結(動態連線是一個將符號引用解析為直接引用的過程),方法出口等資訊。方法呼叫就是入棧出棧的過程。
區域性變量表存放了編譯器的各種基本資料型別,物件引用(指標,控制代碼或是相關位置)和returnAddress型別(指向位元組碼指令的地址)。區域性變量表所需空間在編譯器完成分配,當進入方法時,在棧幀中分配多大空間是完全確定的,long和double會佔用2個區域性變數空間(Slot),其他一個
當執行緒請求棧深度大於虛擬機器允許的深度,將丟擲我們熟知的StackOverflowError異常,如果虛擬機器棧可以動態擴充套件,當擴充套件到記憶體不夠的情況則會丟擲OOM異常
》本地方法棧
這個是虛擬機器為使用到的Native方法開闢的空間,與虛擬機器棧類似。不同虛擬機器都是自定義這一塊,甚至有的和虛擬機器棧合二為一。會出現StackOverflowError和OOM異常。
》java堆
java堆在虛擬機器啟動時建立,唯一目的是存放物件例項,“幾乎所有”都在這裡分配,可以看到這塊區域是執行緒共享的。java堆是垃圾回收的主要區域,也稱作GC堆。它可以處於物理不連續邏輯連續的記憶體空間,因此可擴充套件,當然也會出現OOM異常。
》方法區
方法區也是執行緒共享的,用於儲存已被載入的類資訊,常量,靜態變數,即時編譯後的程式碼等資料。java虛擬機器規範對這塊區域的限制非常寬鬆,一樣可擴充套件,可以不實現垃圾回收,因為回收概率小。這個區域回收主要是針對常量池和對型別的解除安裝,不過回收效果差,但是卻是必要的。
方法區的執行時常量池。Class檔案中除了有類的版本,欄位,方法,介面描述外,還有一項資訊是常量池,用於存放編譯器生成的各種字面量和符號引用,在類被載入到方法區後存放到執行時常亮池中。一般來說,除了符號引用,翻譯過來的直接引用也儲存在這裡,同時並非只有編譯時,執行時也可以把常量儲存進來。同樣的存在OOM異常
》直接記憶體
並不是這個體系的一部分,在jdk1.4中引入了NIO,可以使用native函式庫直接分配堆外記憶體,避免在java堆和native堆中來回複製,有可能出現OOM異常。
2.物件訪問
拿一個以前常用的例子說,Object obj = new Object(); 這句中的變數Object obj將會被儲存於虛擬機器棧中的區域性變量表中,作為一個reference型別出現,而new Object()顯然是一個物件例項,儲存在java堆中。
java虛擬機器規範規定reference型別是指向物件的引用,沒有定義如何指向。主流的訪問方式有兩種:使用控制代碼和直接指標。1)控制代碼訪問,java堆會開闢一塊記憶體作為控制代碼池,reference此時儲存物件的控制代碼地址,控制代碼則包含物件型別資料Object類和物件例項資料new Object()的具體地址。2)直接指標方式,reference儲存物件例項資料new Object()的具體地址,該例項資料中又包含了物件型別資料地址。各有優勢:採用控制代碼訪問,當物件發生移動時,只要改變控制代碼中的物件例項指標,不需要改變reference;而直接指標的優勢則是一次定位.
3.OOM實驗
》java堆OOM
java堆存放物件例項,所以無限建立例項就會引起OOM
編寫java程式碼
右鍵進行debug配置
配置虛擬機器引數:記憶體20M,當OOM時dump
執行結果
》java棧溢位實驗
java棧儲存棧幀,只要無限呼叫方法就行。
執行結果如下:
實驗結果表明:在單執行緒下,無論是棧幀太大,還是棧容量太小,丟擲的都是StackOverflowError異常。在多線情況下,則可以出現OOM異常。如果在多執行緒發生OOM,在不能減少執行緒和更換64位虛擬機器的情況下可以通過減少最大堆和減少棧容量來換取更多執行緒。
這裡實驗時發現一個問題,jdk1.8這裡棧容量最低要160k,所以換成-Xss160k
》執行時常量池記憶體溢位實驗
前面提到常量池在執行時也能動態新增常量,這裡可以無限新增常量就行了。利用String.intern()這個native方法,該方法用於向常量池中新增沒有的常量。
執行時發現,java 8不支援VM使用這兩個方法區引數了。Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0,查了一下發現機制已經變了,尷尬。
替換成java 7執行,發現沒有報錯,尷尬+1。於是減小方法區大小,10k,提示初始化時不能太小,尷尬+2。最後改為2M,執行得到結果:
》方法區記憶體溢位實驗
前面說了,方法區用於存放已被載入的類資訊,常量,靜態變數,即時編譯後的程式碼等資料。這裡讓它無限載入類就行。可以通過反射,當然也有簡單的辦法。作者使用了一個CGLIB代理,作用跟java動態代理類似。
參考:http://blog.csdn.net/danchu/article/details/70238002
由於我是在javaweb專案裡測試的,spring框架已經集成了CGLIB所以可以直接使用裡面的方法。import org.springframework.cglib.proxy.Enhancer;
執行結果:
方法區的記憶體回收是比較困難的,所以在動態生成大量class的時候,特別要注意類的回收狀態。(ps:這裡的PermGen space只的就是方法區,也就是HotSpot虛擬機器中的永久代,是對JVM規範的一種實現,別的虛擬機器是沒有的。在 JDK 1.8 中, HotSpot 已經沒有 “PermGen space”這個區間了,取而代之是一個叫做 Metaspace(元空間) 的東西。)
》直接記憶體溢位實驗
這個沒有找到Unsafe類無法測試,大寫的尷尬。據說這個類可以操作記憶體空間,作者也用它來動態申請記憶體空間,每次1M,直到超出虛擬機器限定的10M。