文章目錄
- 概述
- 運行時數據區域
- 程序計數器
- Java虛擬機棧
- 本地方法棧
- Java堆
- 方法區
- 運行時常量池
- 直接內存
- HotSpot虛擬機對象探秘
- 對象的創建
- 第一步
- 第二步
- 第三步
- 第四步
- 最后一腳
- 對象的內存布局
- 對象頭Header
- 第一部分
- 第二部分
- 實例數據Instance
- 對齊填充Padding
- 對象的訪問定位
- 句柄
- 直接指針
- 對象的訪問定位小結
- 實戰:OutOfMerroyError
- Java堆溢出
- 解決之道
- 虛擬機棧和本地方法棧溢出
- 解決之道
- 方法區和運行時常量池溢出
- 運行時常量池溢出
- 方法區溢出
- 方法區和運行時常量池溢出小結
- 本機直接內存溢出
概述
內存泄露 Memory Leak
內存溢出 Out Of Memory
有JVM存在,無需為每個new操作寫配對的delete/free代碼,不易出現 Out Of Memory 或 Memory Leak。但是,愿望是美好的,該出現的問題還是會出現,了解JVM如何使用內存有助于解決問題。
A memory leak in Java is when objects you aren’t using cannot be garbage collected because you still have a reference to them somewhere.
An OutOfMemoryError is thrown when there is no memory left to allocate new objects. This is usually caused by a memory leak, but can also happen if you’re just trying to hold too much data in memory at once.
——What is the difference between an OutOfMemoryError and a memory leak
運行時數據區域
程序計數器
Program Counter Register PCR 是一塊較小的內存空間,可看作當前線程所執行的字節碼的行號指示器。
在VM概念模型中,字節碼解釋器工作時就是通過改變PCR的值來選取下一條需要執行的字節碼指令。
以下功能需要這個計數器來完成。
- 分支
- 循環
- 跳轉
- 異常處理
- 線程恢復
- …
由于JVM多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一處理器(對于多核處理器來說是一個內核)都只會執行一條線程中的指令。(一時刻一核一指令)
因此,為了線程切換后能恢復到正確的執行位置,每條線程需要一個獨立PCR,各條線程PCR互不影響,獨立存儲,這內存區域被稱為“線程私有”。(線程獨立性)
若線程正執行一個Java方法,這PCR記錄正在執行VM字節碼指令的地址;若正執行是Native方法,這PCR則為空Undefinded。
這內存區域是唯一一個在JVM規范沒有規定任何OutOfMemoryError OOME情況的區域
Java虛擬機棧
VM Stack,線程私有,聲明周期與線程相同。
VM Stack描述的Java方法執行的內存模型:每個方法在執行的同時會創建一個棧幀Stack Frame。
Stack Frame用于
- 存儲局部變量表
- 操作數棧
- 動態鏈接
- 方法出口信息
每一個方法從調用直至執行完成的過程,就對應著一個Stack Frame 在VM Stack中入棧到出棧的過程。
粗糙說法,Java內存區分為
- Heap 堆內存
- Stack 棧內存 這里指 VMStack
Java內存區域劃分實際上遠比這復雜。
局部變量表存放
- 編譯期可知的各種基本數據類型(boolean/byte/char/short/int/float/double)
- 對象引用 reference類型 它不等同于對象本身,可能是
- 一個指向對象起始地址的引用指針
- 一個代表對象的句柄或其他與此對象相關的位置
- returnAddress類型(指向了一條字節碼指令的地址)
64位的double/long類型數據會占用2個局部變量空間Slot,其余基本類型占1個。
局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
JVM規范中,這區域規定2種異常狀況:
- StackOverflowError SOE,當線程請求棧深度大于VM所允許深度時。
- OutOfMemoryError OOME,VM Stack可動態擴展(當前大部分JVM都可動態擴展,只不過JVM規范中允許固定程度的VM Stack),當擴展時無法申請到足夠的內存。
本地方法棧
Native Method Stack NVS 與 VM Stack作用相似,它們區別為
- VM Stack為VM執行Java方法(也就是字節碼)服務。
- NVS則為VM使用到Native方法服務。
VM規范未強制規定
- 對NVS中方法使用的語言
- 使用方式與數據結構并沒有強制規定。
不同VM各有發揮。
Sum HotSpot VM 直接把 VM Stack 和 NVS 合二為一。
會拋SOE和OOME異常。
Java堆
Java Heap ,線程共享
通常,Java堆是JVM所管理內存最大一塊。
VM啟動時創建。
此內存區域的唯一目的就是存放對象實例,幾乎所有對象實例在這里分配內存。
JVM規范:所有對象實例以及數組都要在堆上分配。
但隨著JIT編譯器技術等發展,所有對象在堆上分配不是那么絕對。
Java堆是GC管理主要區域,因此也被稱為GC堆(Garbage Collected Heap)
從內存回收的角度看,由于現在收集器基于分代收集算法,所以Java堆中可細分為:新生代 和 老生代;更細致的 Eden空間、From Survivor 空間、To Survivor空間等。
從內存分配的角度來看,線程共享的Java堆可劃分多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)
無論怎樣劃分,目的始終是 更好回收內存,更快分配內存。
通過-Xmx/-Xms控制擴展大小
若堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OOME
方法區
Method Area 線程共享。
它用于存儲已被VM加載的
- 類型信息
- 常量
- 靜態變量
- 即時編譯器編譯后的代碼等數據
- 。。。
JVM規范 把 方法區描述為堆的一個邏輯部分,但它別名叫做Non-Heap(非堆),目的與Java堆區分開。
大部分HotSpot VM開發者更愿意成方法區稱為永久代Permanent Generation,
二者并不等價,只因HotSpot設計團隊選擇把GC分代收集擴展至方法區,或說使用永久代來實現方法區而已,這樣HotSpot的GC可以像管理Java堆一樣管理這部分內存,能夠省去專門為方法區編寫內存管理代碼工作。
其他虛擬機BEA JRockit/IBM J9等不存在永久代的概念。
原則上,如何實現方法區屬于VM實現細節,不受虛擬機規范結束,但使用永久代來實現方法區,現在看不是好主意,因為易于 內存溢出 問題(永久代有-XX:MaxPermSize的上限)
JDK1.7的HotSpot已把原在永久代的字符串常量池移出.
GC比較少在方法區內出現,但并非數據進入了方法區就如永久代的名字一樣“永久”存在了,這區域的內存回收目標主要是針對常量池的回收 和 對類型的卸載。
當內存不滿足,拋OOME
運行時常量池
Runtime Constant Pool RCP是方法區的一部分
Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池Constant Pool Table,用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的RCP中存放。
JVM對Class文件每一部分的格式都有嚴格規定,每一字節用于存儲那種數據都必須符合規范上的要求才會被VM認可、裝載和執行。
但對于RCP,JVM規范沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要實現區域。
通常,除了保存Class文件中描述的符號引用外,還會翻譯出來的字節引用也存儲在RCP中。
運行時常量池 相對于Class文件常量池 的另外一個重要特征是具備動態性。
Java語言并不要常量一定只有編譯器才能產生,也就是并非預置如Class文件中常量池的內容才能進入方法區RCP,運行期間也可能將新的常量放入池中,如String類的intern()方法。
當內存不滿足,拋OOME
直接內存
Direct Memory 并不是虛擬機運行時數據區的一部分,也就是JVM規范中定義的內存區域。
這部分內存也頻繁使用,而也可能導致OOME.
在JDK1.4中新加入了NIO類,引入一種基于通道Channel與緩沖區Buffer的IO方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制數據。
顯然,Direct Memory分配不受Java 堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括RAM一級SWAP區或者分頁文件)大小 以及 處理器尋址空間的限制。
服務器管理員在配置VM參數,會根據實際內存設置-Xms等參數信息,但經常忽略直接內存,使得各種內存區域總和大于物理內存限制,從而導致動態擴展時出現OOME.
HotSpot虛擬機對象探秘
對于細節問題,必須把討論范圍限定在具體的虛擬機和集中在某一個內存區域上才有意義。
對象的創建
JVM創建一個普通對象(非數組或Class對象)過程大概可分4步。
第一步
VM遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用是否已被加載、解析和初始化過。
若沒有,那必須先執行相應的類加載過程。
第二步
在類加載檢查通過后,接下來VM將為新生對象分配內存。
對象所需內存的大小在類加載完成后完全確定為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。
兩種分配方式:
- 指針碰撞 Bump the Pointer
- 空閑列表Free List
假設Java堆中內存是絕對的完整的,所有用過的內存放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅是那指針向空閑那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞 Bump the Pointer”
若Java堆中的內存并不是規整的,已使用的內存和空閑的內存相互交錯,那就無法簡單地進行指針碰撞,VM就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的激勵,這種分配方式稱為“空閑列表Free List”。
采用GC是否帶有壓縮整理功能 --決定–>Java堆是否規整 --決定–> ? 指針碰撞 : 空閑列表
因此,使用Serial、ParNew等帶Compact過程的收集器時,系統采用的分配算法是指針碰撞
使用CMS這種基于Mark-Sweep算法的手機器時,通常采用空閑列表
除了如何劃分可用空間之外,還有另外一個需要考慮的問題是對象創建在虛擬機中是否非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發情況下也不是線程安全,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。(分配內存時的線程安全問題)
解決上述問題有兩種方案:(好似synchronized和ThreadLocal :)
- 對分配內存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性
- 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩存(Thread Local Allocation Buffer, TLAB)。哪個線程分配內存,就在哪個線程上的TLAB上分配,只有TLAB用完并分配新的TLAB是,才需要同步鎖定。VM是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。
第三步
內存分配完成后,VM需要將分配到的內存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
第四步
VM對對象進行必要的設置,例如:
- 這個對象是哪個類的實例
- 如何才能找到類的元數據信息
- 對象的哈希碼
- 對象的GC分代年齡
- 。。。
這些信息存放在對象的對象頭(Object Header)之中。根據VM當前的運行狀態的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。
最后一腳
在上面工作都完成之后,從VM的視角來看,一個新對象已經誕生,但從Java程序的視角來看,對象創建才剛剛開始(前面那個頂多是個半成品)——<init>方法還沒有執行,所有字段都還為零。所以,一般來說(由字節碼是否跟隨invokespecial指令所決定),執行new指令之后會接著執行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。
對象的內存布局
在HotSpot VM中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、實例數據(Instance)和對齊填充(Padding)
對象頭Header
對象頭包含兩部分信息
第一部分
第一部分用于存儲對象自身的運行時數據,如
- 哈希碼HashCode
- GC分代年齡
- 鎖狀態標志
- 線程持有的鎖
- 偏向線程ID
- 偏向時間戳
- 。。。
這部分數據的長度在32/64位的VM(未開啟壓縮指針)中分別為32/64bit,官方稱它為“Mark Word”
第二部分
另一部分是類型指針,即對象指向它的類元數據的指針,VM通過這個指針來確定這個對象是哪個類的實例。
并不是所有VM實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息并不一定要經過對象本身。
若對象是一個Java數組,那在對象頭還必須有一塊用于記錄數組長度的數據,因為VM可通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數組中卻無法確定數組的大小。
實例數據Instance
對象真正存儲的有效信息,也是程序代碼中所定義的各種類型的字段內容。
無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。
對齊填充Padding
雞肋,僅僅起著占位符的作用。HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍。也就是對象大小必須是8字節的整數倍。當對象實例數據部分沒有對齊時,就需要通過對齊填充來補充。
對象的訪問定位
建立對象是為了使用對象,Java程序需要通過棧上的reference數據來操作堆上的具體對象。
由于reference類型在JVM規范中只規定了一個指向對象的引用,并沒有定義這個引用通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決于虛擬機實現而定的。
目前主流訪問方式有使用句柄和直接指針兩種
句柄
Java堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據 與 對象類型數據各自的具體地址信息。
直接指針
Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址
對象的訪問定位小結
這兩種對象訪問方式各有優勢,使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要修改。
使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多也是一項非常可觀的執行成本。
HotSpot使用直接指針進行對象訪問的,但從整個軟件開發的范圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。
實戰:OutOfMerroyError
- 通過代碼驗證JVM規范中描述的各個運行時區域存儲內容。
- 希望讀者在工作中遇到實際的內存溢出異常,知道什么樣的代碼可能會導致這區域內存溢出,以及出現這異常該如何處理。
例如,VM啟動參數
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
verbose /v?’bos/ adj. 冗長的;啰嗦的
Java堆溢出
Java堆用于存儲對象實例,只要不斷創建對象,并且保證GC Roots到對象之間有達途徑來避免垃圾回收機制清除這些對象,那么再對象數量到達最大堆的容量限制后就會產生內存溢出異常
將堆的最小值-Xms和最大值-Xmx參數設置一致可避免堆自動擴展
-XX:+HeapDumpOnOutOfMemoryError讓VM在出現內存溢出時Dump出當前的內存堆轉儲快照以便事后進行分析
package com.lun.c02;import java.util.ArrayList;
import java.util.List;/*** VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError* @author zzm*/
public class HeapOOM {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list = new ArrayList<OOMObject>();while (true) {list.add(new OOMObject());}}}/*result:[GC (Allocation Failure) [PSYoungGen: 7793K->1001K(9216K)] 7793K->5334K(19456K), 0.1073292 secs] [Times: user=0.09 sys=0.00, real=0.11 secs]
[GC (Allocation Failure) [PSYoungGen: 9193K->1024K(9216K)] 13526K->11194K(19456K), 0.0330039 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 1024K->1023K(9216K)] [ParOldGen: 10170K->10140K(10240K)] 11194K->11163K(19456K), [Metaspace: 2790K->2790K(1056768K)], 0.4100364 secs] [Times: user=0.44 sys=0.00, real=0.41 secs]
[Full GC (Ergonomics) [PSYoungGen: 8551K->8424K(9216K)] [ParOldGen: 10140K->8028K(10240K)] 18692K->16452K(19456K), [Metaspace: 2790K->2790K(1056768K)], 0.3584301 secs] [Times: user=0.39 sys=0.00, real=0.36 secs]
[Full GC (Allocation Failure) [PSYoungGen: 8424K->8423K(9216K)] [ParOldGen: 8028K->8016K(10240K)] 16452K->16440K(19456K), [Metaspace: 2790K->2790K(1056768K)], 0.2883919 secs] [Times: user=0.30 sys=0.02, real=0.29 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid5784.hprof ...
Heap dump file created [28135555 bytes in 0.213 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3210)at java.util.Arrays.copyOf(Arrays.java:3181)at java.util.ArrayList.grow(ArrayList.java:265)at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)at java.util.ArrayList.add(ArrayList.java:462)at com.lun.c02.HeapOOM.main(HeapOOM.java:19)
HeapPSYoungGen total 9216K, used 8703K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)eden space 8192K, 100% used [0x00000000ff600000,0x00000000ffe00000,0x00000000ffe00000)from space 1024K, 49% used [0x00000000fff00000,0x00000000fff7fd40,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 10240K, used 8016K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)object space 10240K, 78% used [0x00000000fec00000,0x00000000ff3d4398,0x00000000ff600000)Metaspace used 2822K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 302K, capacity 386K, committed 512K, reserved 1048576K*/
Java堆內存的java.lang.OutOfMemoryError是實際應用中常見的內存溢出異常情況。
解決之道
先通過內存影響分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄露Memory Leak還是內存溢出Memory Overflow(從字面上理解吧!)
Eclipse Memory Analyzer使用方法
- 使用Help->Eclipse Marketplace安裝
- Open Perspective中選擇Memory Analysis
- Open Heap Dump 打開上例程序運行所產生的堆轉儲快照文件java_pid5784.hprof(在classpath目錄下)
注意!java_pid5784.hprof文件被打開后,系統產生一堆文件
- 若內存泄露,可一步通過工具查看泄露對象到GC Roots的引用鏈。于是就能找到泄露對象是同過怎樣的路徑與GC Roots相關聯并導致GC無法自動回收它們的。掌握了泄露對象的類型信息及GC Roots引用鏈的信息,即可比較準確地定位出泄露代碼的位置。
- 若不存在泄露,換句話說,就是內存中的對象確實都還必須存活著,那就應當檢查VM的堆參數最小值-Xms和最大值-Xmx參數,與機器物理內存對比看是否還可以調大,從代碼檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
虛擬機棧和本地方法棧溢出
Hotspot VM并不區分 VM Stack和 Native Method Stack,因此,-Xoss參數(設置本地方法棧大小)形同虛設,棧容量只由-Xss參數設定。
關于VM Stack和 Native Method Stack,在JVM規范中描述了兩種異常:
- 若線程請求的棧深度大于VM所允許的最大深度,將拋出StackOverflowError異常。
- 若VM在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常
這兩異常有相互重疊地方:當棧空間無法繼續分配時,到底是內存太小,還是棧空間太大,其本質上只是對同一件事情的兩種描述。
在單線程環境操作
package com.lun.c02;/*** VM Args:-Xss128k* @author zzm*/
public class JavaVMStackSOF {private int stackLength = 1;public void stackLeak() {stackLength++;stackLeak();}public static void main(String[] args) throws Throwable {JavaVMStackSOF oom = new JavaVMStackSOF();try {oom.stackLeak();} catch (Throwable e) {System.out.println("stack length:" + oom.stackLength);throw e;}}
}/*
stack length:999
Exception in thread "main" java.lang.StackOverflowErrorat com.lun.c02.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)at com.lun.c02.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)at com.lun.c02.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
...
*/
- 使用 -Xss參數減少棧內存存量。會拋出java.lang.StackOverflowError,異常出現時輸出的堆棧深度相應縮小。
- 定義大量的本地變量,增大此方法幀中本地變量表的長度。會拋出java.lang.StackOverflowError,異常出現時輸出的堆棧深度相應縮小。
上例程序針對第1點
實驗結果表明:單線程環境下,無論棧幀太大還是VM棧容量太小,當內存無法分配時,VM都拋出StackOverflowError異常等。
package com.lun.c02;/*** VM Args:-Xss2M (這時候不妨設大些)* @author zzm*/
public class JavaVMStackOOM {private void dontStop() {while (true) {}}public void stackLeakByThread() {while (true) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {dontStop();}});thread.start();}}public static void main(String[] args) throws Throwable {JavaVMStackOOM oom = new JavaVMStackOOM();oom.stackLeakByThread();}
}
把-Xss從108k設置到1024M 5min內 依然跑不出書上那樣結果加上-Xmx20M -Xms20M 也是一樣跑不出
上例通過不斷創建線程方式產生內存溢出異常。但是這樣產生的內存溢出異常與棧空間是否足夠大并存在任何聯系,或者準確地說,在這種情況下,為每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。(為什么我跑不出這樣的實驗結果呢?)
歸根到底,OS分配每個進程內存是有限制的,譬如32位的Windows限制為2GB。VM提供了參數來參加Java堆 和 方法區的這兩部分內存的最大值。剩余內存為2GB(OS限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區容量),PCR內存消耗很小,可忽略。
若VM進程本身消耗的內存不計算在內,剩下的內存就由VM Stack 和 Native Method Stack“瓜分”了。
每個線程分配到的棧容量越大,可建立的線程數量自然就越少,建立線程時就越容易把剩下內存耗盡。
解決之道
在開發多線程時要注意,出現StackOverflowError會有錯誤堆棧信息可閱讀,相對來說,比較容易找打問題的所在。而且,若使用虛擬機默認參數,棧深度通常達到1000~2000完全沒問題,對于正常的方法調用(包括遞歸),這深度應該完全夠用了。
但是,若建立過多線程導致的內存溢出,在不能減少線程數或者更換64位VM情況下,就只能通過減少最大堆和減少棧容量來換取更多線程。
這種通過“減少內存”的手段來解決內存溢出的方式會比較難以想到。(似非而是)
方法區和運行時常量池溢出
運行時常量池溢出
JDK1.7開始逐步“去永久代”。
String.intern()是一個Native方法,其作用:若字符串常量池中已經包含一個等于此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用。
JDK1.6及以前的版本中由于常量池分配在永久代內,可通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量(-XX:PermSize 和 -XX:MaxPermSize 已被JDK1.8移除,取而代之的是-XX:MetaspaceSize和-XX:MaxMetaspaceSize)
package com.lun.c02;import java.util.ArrayList;
import java.util.List;/*** VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M* JDK1.8 : -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M* @author zzm*/
public class RuntimeConstantPoolOOM {public static void main(String[] args) {// 使用List保持著常量池引用,避免Full GC回收常量池行為List<String> list = new ArrayList<String>();// 10MB的PermSize在integer范圍內足夠產生OOM了int i = 0; while (true) {list.add(String.valueOf(i++).intern());}}
}/*
Error occurred during initialization of VM
OutOfMemoryError: Metaspace
*//* 設置類-Xms20M -Xmx20MException in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceededat java.lang.Long.toString(Long.java:397)at java.lang.String.valueOf(String.java:3113)at com.lun.c02.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:20)
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=1k; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=1k; support was removed in 8.0*/
運行時常量屬于方法區(HotSpot虛擬機中永久代)的一部分。
使用JDK1.7運行這段程序就不會得到相同的結果,while循環將一直進行下去
JDK 1.8 JVM內部結構的元空間(Metaspace)取代永久代(PermGen)
package com.lun.c02;public class RuntimeConstantPoolOOM2 {public static void main(String[] args) {String str1 = new StringBuilder("中國").append("釣魚島").toString();System.out.println(str1.intern() == str1);String str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2.intern() == str2);}
}
運行結果
- JDK1.6 -> 兩個false
- JDK1.7 -> 前true后false
在JDK1.6,intern()會把首先遇到的字符串實例復制到永久代中,返回的也是永久代中這個字符串實例的引用,而由StringBuilder創建的字符串實例在Java堆上,所以必然不是同一個引用,將返回false。
在JDK1.7的intern()實現不會再復制實例,只是在常量池中記錄首次出現的實例引用,因此intern()返回的引用和由StringBuilder創建的那個字符串實例是同一個。
對str2比較返回false是因為“java”這個字符串在執行StringBuilder.toString()之前已經出現過,字符串常量池中已經有它的引用,不符合“首次出現”的原則,而“計算機軟件”這個字符串是首次出現,因此返回true
方法區溢出
方法區用于存放Class的相關信息,如
- 類名
- 訪問修飾符
- 常量池
- 字段描述
- 方法描述
- 。。。
對于這區域測試,基本思路是運行時產生大量的類填滿方法區,直到溢出。
為簡單起見,不使用反射而借助CGLib直接操作字節碼運行時生成了大量的動態類。
首先添加CGLib依賴
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.2.10</version>
</dependency>
package com.lun.c02;import java.lang.reflect.Method;import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;/*** VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M* * JDK1.8 VM Args:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M* * * @author zzm*/
public class JavaMethodAreaOOM {public static void main(String[] args) {while (true) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object obj, Method arg1, Object[] arg2, MethodProxy proxy) throws Throwable {return proxy.invokeSuper(obj, args);}});enhancer.create();}}static class OOMObject {}
}/*JDK1.8 result Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->nullat net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)at com.lun.c02.JavaMethodAreaOOM.main(JavaMethodAreaOOM.java:27)
Caused by: java.lang.reflect.InvocationTargetExceptionat sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspaceat java.lang.ClassLoader.defineClass1(Native Method)at java.lang.ClassLoader.defineClass(ClassLoader.java:763)... 11 more*/
當前的很多主流框架,如Spring、Hibernate,在對此進行增強時,都會使用CGLib這類字節碼技術,增強的類越多,就需越大的方法區來保證動態生成的Class可以加載入內存。
另外,JVM動態語言(如Groovy等)通常都會持續創建類來實現語言的動態性。
一個類要被GC回收掉,判定條件是比較苛刻的。在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。
這類場景除了上面提到的程序使用了CGLib字節碼增強和動態語言之外,常見還有
- 大量JSP或動態產生的引用(JSP第一次運行時需要編譯為Java類)
- 基于OSGi的應用(即使是同一個類文件,被不同的加載器加載也會視為不同的類)
方法區和運行時常量池溢出小結
-XX:PermSize 和 -XX:MaxPermSize 已被JDK1.8移除,查閱資料得知,JDK 1.8 JVM內部結構的元空間(Metaspace)取代永久代(PermGen),所以在JDK1.8 機器上跑不出書本上那樣結果。
若把參數換成-XX:MetaspaceSize和-XX:MaxMetaspaceSize則有類似的結果。
本機直接內存溢出
DirectMemory容量可通過-XX:MaxDirectMemorySize指定,若不指定,則默認與Java堆最大值(-Xmx指定)一樣。
直接通過Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器才會返回實例,也就是設計者希望只有rt.jar中的類才能使用Unsafe的功能)。
DirectByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時并沒有真正想OS申請分配內存,而是通過計算得知內存無法分配,于是直接拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()
package com.lun.c02;import java.lang.reflect.Field;
import sun.misc.Unsafe;/*** VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M* @author zzm*/
public class DirectMemoryOOM {private static final int _1MB = 1024 * 1024;public static void main(String[] args) throws Exception {Field unsafeField = Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafe unsafe = (Unsafe) unsafeField.get(null);while (true) {unsafe.allocateMemory(_1MB);}}
}/*
Exception in thread "main" java.lang.OutOfMemoryErrorat sun.misc.Unsafe.allocateMemory(Native Method)at com.lun.c02.DirectMemoryOOM.main(DirectMemoryOOM.java:18)
*/
由DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常,若發現OOME之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。