Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裏面的人卻想出來。java
對於從事C、C++程序開發的開發人員來講,在內存管理領域,他們便是擁有最高權力的皇帝又是執行最基礎工做的勞動人民——擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。程序員
對於Java程序員來講,不須要在爲每個new操做去寫配對的delete/free,不容易出現內容泄漏和內存溢出錯誤,看起來由JVM管理內存一切都很美好。不過,也正是由於Java程序員把內存控制的權力交給了JVM,一旦出現泄漏和溢出,若是不瞭解JVM是怎樣使用內存的,那排查錯誤將會是一件很是困難的事情。數組
JVM執行Java程序的過程當中,會使用到各類數據區域,這些區域有各自的用途、建立和銷燬時間。根據《Java虛擬機規範(第二版)》(下文稱VM Spec)的規定,JVM包括下列幾個運行時數據區域:服務器
1.程序計數器(Program Counter Register):多線程
每個Java線程都有一個程序計數器來用於保存程序執行到當前方法的哪個指令,對於非Native方法,這個區域記錄的是正在執行的VM原語的地址,若是正在執行的是Natvie方法,這個區域則爲空(undefined)。此內存區域是惟一一個在VM Spec中沒有規定任何OutOfMemoryError狀況的區域。框架
2.Java虛擬機棧(Java Virtual Machine Stacks)ide
與程序計數器同樣,VM棧的生命週期也是與線程相同。VM棧描述的是Java方法調用的內存模型:每一個方法被執行的時候,都會同時建立一個幀(Frame)用於存儲本地變量表、操做棧、動態連接、方法出入口等信息。每個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。在後文中,咱們將着重討論VM棧中本地變量表部分。函數
常常有人把Java內存簡單的區分爲堆內存(Heap)和棧內存(Stack),實際中的區域遠比這種觀點複雜,這樣劃分只是說明與變量定義密切相關的內存區域是這兩塊。其中所指的「堆」後面會專門描述,而所指的「棧」就是VM棧中各個幀的本地變量表部分。本地變量表存放了編譯期可知的各類標量類型(boolean、byte、char、short、int、float、long、double)、對象引用(不是對象自己,僅僅是一個引用指針)、方法返回地址等。其中long和double會佔用2個本地變量空間(32bit),其他佔用1個。本地變量表在進入方法時進行分配,當進入一個方法時,這個方法須要在幀中分配多大的本地變量是一件徹底肯定的事情,在方法運行期間不改變本地變量表的大小。性能
在VM Spec中對這個區域規定了2中異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是VM棧能夠動態擴展(VM Spec中容許固定長度的VM棧),當擴展時沒法申請到足夠內存則拋出OutOfMemoryError異常。測試
3.本地方法棧(Native Method Stacks)
本地方法棧與VM棧所發揮做用是相似的,只不過VM棧爲虛擬機運行VM原語服務,而本地方法棧是爲虛擬機使用到的Native方法服務。它的實現的語言、方式與結構並無強制規定,甚至有的虛擬機(譬如Sun Hotspot虛擬機)直接就把本地方法棧和VM棧合二爲一。和VM棧同樣,這個區域也會拋出StackOverflowError和OutOfMemoryError異常。
4.Java堆(Java Heap)
對於絕大多數應用來講,Java堆是虛擬機管理最大的一塊內存。Java堆是被全部線程共享的,在虛擬機啓動時建立。Java堆的惟一目的就是存放對象實例,絕大部分的對象實例都在這裏分配。這一點在VM Spec中的描述是:全部的實例以及數組都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),可是在逃逸分析和標量替換優化技術出現後,VM Spec的描述就顯得並不那麼準確了。
Java堆內還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程分配緩衝(TLAB)等,不管對Java堆如何劃分,目的都是爲了更好的回收內存,或者更快的分配內存,在本章中咱們僅僅針對內存區域的做用進行討論,Java堆中的上述各個區域的細節,可參見本文第二章《JVM內存管理:深刻垃圾收集器與內存分配策略》。
根據VM Spec的要求,Java堆能夠處於物理上不連續的內存空間,它邏輯上是連續的便可,就像咱們的磁盤空間同樣。實現時能夠選擇實現成固定大小的,也能夠是可擴展的,不過當前全部商業的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。若是在堆中沒法分配內存,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。
5.方法區(Method Area)
叫「方法區」可能認識它的人還不太多,若是叫永久代(Permanent Generation)它的粉絲也許就多了。它還有個別名叫作Non-Heap(非堆),可是VM Spec上則描述方法區爲堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易使人產生誤解,咱們在這裏就不糾結了。
方法區中存放了每一個Class的結構信息,包括常量池、字段描述、方法描述等等。VM Space描述中對這個區域的限制很是寬鬆,除了和Java堆同樣不須要連續的內存,也能夠選擇固定大小或者可擴展外,甚至能夠選擇不實現垃圾收集。相對來講,垃圾收集行爲在這個區域是相對比較少發生的,但並非某些描述那樣永久代不會發生GC(至少對當前主流的商業JVM實現來講是如此),這裏的GC主要是對常量池的回收和對類的卸載,雖然回收的「成績」通常也比較差強人意,尤爲是類卸載,條件至關苛刻。
6.運行時常量池(Runtime Constant Pool)
Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部份內容將在類加載後進入方法區(永久代)存放。可是Java語言並不要求常量必定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。
運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法在申請到內存時會拋出OutOfMemoryError異常。
7.本機直接內存(Direct Memory)
直接內存並非虛擬機運行時數據區的一部分,它根本就是本機內存而不是VM直接管理的區域。可是這部份內存也會致使OutOfMemoryError異常出現,所以咱們放到這裏一塊兒描述。
在JDK1.4中新加入了NIO類,引入一種基於渠道與緩衝區的I/O方式,它能夠經過本機Native函數庫直接分配本機內存,而後經過一個存儲在Java堆裏面的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java對和本機堆中來回複製數據。
顯然本機直接內存的分配不會受到Java堆大小的限制,可是即然是內存那確定仍是要受到本機物理內存(包括SWAP區或者Windows虛擬內存)的限制的,通常服務器管理員配置JVM參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),而致使動態擴展時出現OutOfMemoryError異常。
上述區域中,除了程序計數器,其餘在VM Spec中都描述了產生OutOfMemoryError(下稱OOM)的情形,那咱們就實戰模擬一下,經過幾段簡單的代碼,令對應的區域產生OOM異常以便加深認識,同時初步介紹一些與內存相關的虛擬機參數。下文的代碼都是基於Sun Hotspot虛擬機1.6版的實現,對於不一樣公司的不一樣版本的虛擬機,參數與程序運行結果可能結果會有所差異。
Java堆
Java堆存放的是對象實例,所以只要不斷創建對象,而且保證GC Roots到對象之間有可達路徑便可產生OOM異常。測試中限制Java堆大小爲20M,不可擴展,經過參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現OOM異常的時候Dump出內存映像以便分析。(關於Dump映像文件分析方面的內容,可參見本文第三章《JVM內存管理:深刻JVM內存異常分析與調優》。)
清單1:Java堆OOM測試
/** * 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()); } } } |
運行結果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3404.hprof ... Heap dump file created [22045981 bytes in 0.663 secs] |
VM棧和本地方法棧
Hotspot虛擬機並不區分VM棧和本地方法棧,所以-Xoss參數其實是無效的,棧容量只由-Xss參數設定。關於VM棧和本地方法棧在VM Spec描述了兩種異常:StackOverflowError與OutOfMemoryError,當棧空間沒法繼續分配分配時,究竟是內存過小仍是棧太大其實某種意義上是對同一件事情的兩種描述而已,在筆者的實驗中,對於單線程應用嘗試下面3種方法均沒法讓虛擬機產生OOM,所有嘗試結果都是得到SOF異常。
1.使用-Xss參數削減棧內存容量。結果:拋出SOF異常時的堆棧深度相應縮小。
2.定義大量的本地變量,增大此方法對應幀的長度。結果:拋出SOF異常時的堆棧深度相應縮小。
3.建立幾個定義不少本地變量的複雜對象,打開逃逸分析和標量替換選項,使得JIT編譯器容許對象拆分後在棧中分配。結果:實際效果同第二點。
清單2:VM棧和本地方法棧OOM測試(僅做爲第1點測試程序)
/** * 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:2402 Exception in thread "main" java.lang.StackOverflowError at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20) at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21) at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21) |
若是在多線程環境下,不斷創建線程卻是能夠產生OOM異常,可是基本上這個異常和VM棧空間夠不夠關係沒有直接關係,甚至是給每一個線程的VM棧分配的內存越多反而越容易產生這個OOM異常。
緣由其實很好理解,操做系統分配給每一個進程的內存是有限制的,譬如32位Windows限制爲2G,Java堆和方法區的大小JVM有參數能夠限制最大值,那剩餘的內存爲2G(操做系統限制)-Xmx(最大堆)-MaxPermSize(最大方法區),程序計數器消耗內存很小,能夠忽略掉,那虛擬機進程自己耗費的內存不計算的話,剩下的內存就供每個線程的VM棧和本地方法棧瓜分了,那天然每一個線程中VM棧分配內存越多,就越容易把剩下的內存耗盡。
清單3:建立線程致使OOM異常
/** * VM Args:-Xss2M (這時候不妨設大些) * @author zzm */ public class JavaVMStackOOM {
private void dontStop() { while (true) { } }
public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } }
public static void main(String[] args) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } } |
特別提示一下,若是讀者要運行上面這段代碼,記得要存盤當前工做,上述代碼執行時有很大令操做系統卡死的風險。
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread |
運行時常量池
要在常量池裏添加內容,最簡單的就是使用String.intern()這個Native方法。因爲常量池分配在方法區內,咱們只須要經過-XX:PermSize和-XX:MaxPermSize限制方法區大小便可限制常量池容量。實現代碼以下:
清單4:運行時常量池致使的OOM異常
/** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * @author zzm */ public class RuntimeConstantPoolOOM {
public static void main(String[] args) { // 使用List保持着常量池引用,壓制Full GC回收常量池行爲 List<String> list = new ArrayList<String>(); // 10M的PermSize在integer範圍內足夠產生OOM了 int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } |
運行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18) |
方法區
上文講過,方法區用於存放Class相關信息,因此這個區域的測試咱們藉助CGLib直接操做字節碼動態生成大量的Class,值得注意的是,這裏咱們這個例子中模擬的場景其實常常會在實際應用中出現:當前不少主流框架,如Spring、Hibernate對類進行加強時,都會使用到CGLib這類字節碼技術,當加強的類越多,就須要越大的方法區用於保證動態生成的Class能夠加載入內存。
清單5:藉助CGLib使得方法區出現OOM異常
/** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=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() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } }
static class OOMObject {
} } |
運行結果:
Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more |
本機直接內存
DirectMemory容量可經過-XX:MaxDirectMemorySize指定,不指定的話默認與Java堆(-Xmx指定)同樣,下文代碼越過了DirectByteBuffer,直接經過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器纔會返回實例,也就是基本上只有rt.jar裏面的類的才能使用),由於DirectByteBuffer也會拋OOM異常,但拋出異常時實際上並無真正向操做系統申請分配內存,而是經過計算得知沒法分配既會拋出,真正申請分配的方法是unsafe.allocateMemory()。
/** * 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.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20) |
到此爲止,咱們弄清楚虛擬機裏面的內存是如何劃分的,哪部分區域,什麼樣的代碼、操做可能致使OOM異常。雖然Java有垃圾收集機制,但OOM仍然離咱們並不遙遠,本章內容咱們只是知道各個區域OOM異常出現的緣由,下一章咱們將看看Java垃圾收集機制爲了不OOM異常出現,作出了什麼樣的努力。