1.1 基礎知識
1.1.1 一些基本概念
JDK(Java Development Kit):Java語言、Java虛擬機、Java API類庫
JRE(Java Runtime Environment):Java虛擬機、Java API類庫
JIT(Just In Time):Java虛擬機內置JIT編譯器,將字節碼編譯成本機機器代碼。
OpenJDK:OpenJDK是基於Oracle JDK基礎上的JDK的開源版本,但因爲歷史緣由缺乏了部分(不過重要)的代碼。Sun JDK > SCSL > JRL > OpenJDK
JCP組織(Java Community Process):由Java開發者以及被受權者組成,負責維護和發展技術規範、參考實現(RI)、技術兼容包。html
1.1.2 編譯JDK
參見《深刻理解Java虛擬機》1.6節
走進JVM之一 本身編譯openjdk源碼java
1.2 Java內存模型
1.2.1 運行時數據區域
根據Java虛擬機規範(Java SE7)的規定,JVM的內存包括如下幾個運運行時數據區域:程序員
程序計數器
- 程序計數器(Program Counter Register)是一塊較小的內存空間,他能夠看做是當前線程所執行的字節碼的行號指示器。
- 在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
- 程序計數器是線程私有的,每條線程都有一個獨立的獨立的程序計數器,各條線程之間計數器互不影響。
- 若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的Native方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。
Java虛擬機棧
- Java虛擬機棧是線程私有的,他的生命週期與線程相同。
- 虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame),用於包含局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用到執行完成這個過程,就對應這一個棧幀在虛擬機棧中的入棧到出棧的過程。
- 局部變量表存放了編譯期可知的各類基本數據類型和對象引用(
reference
類型,他不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此相關的位置)和returnAddress
類型(指向了一條字節碼指令的地址) 。
- 其中64位長度的long和double類型會佔用2個局部變量空間,其他的數據類型只會佔用1個局部變量空間。局部變量表所需的內存空間在編譯期間完成內存分配。當進入一個方法時,這個方法須要在幀中分配多大的內存空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
- 在Java虛擬機規範中,對這個區域規定了兩種異常狀態:若是線程請求的棧的深度大於虛擬機容許的深度,將拋出
StackOverFlowError
異常(棧溢出);若是虛擬機棧能夠動態擴展(如今大部分Java虛擬機均可以動態擴展,只不過Java虛擬機規範中也容許固定長度的java虛擬機棧),若是擴展時沒法申請到足夠的內存空間,就會拋出OutOfMemoryError
異常(沒有足夠的內存)。
本地方法棧
- 本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,他們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的本地Native方法服務。
- 在虛擬機規範中對本地方法棧中的使用方法、語言、數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。甚至有的虛擬機(例如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
- 本地方法棧也會拋出StackOverFlowError和OutOfmMemoryError異常。
Java堆
- Java堆(Java Heap)是Java虛擬機管理內存中的最大一塊。
- Java堆是全部線程共享的一塊內存管理區域。
- 此內存區域惟一目的就是存放對象的實例,幾乎全部對象實例都在堆中分配內存。這一點在Java虛擬機規範中的描述是:全部對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展與逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在堆上也不是變的那麼「絕對」了。
- Java堆是垃圾回收器管理的主要區域,所以不少時候也被稱爲GC堆(Garbage Collected Heap)。
- 從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此Java堆中還能夠細分爲:新生代和年老代。再在細緻一點的劃分能夠分爲:Eden空間、From Survivor空間、To Survivor空間等。
- 從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。
- 不過不管如何劃分,都與存放內容無關,不管哪一個區域存放的都是對象實例。進一步劃分的目的是爲了更好的回收內存,或者更快的分配內存。
- Java堆能夠處在物理上不連續的內存空間,只要邏輯上是連續的便可。在實現上既能夠實現成固定大小,也能夠是可擴展的大小,不過當前主流的虛擬機都是按照可擴展來實現的(經過-Xmx和-Xms控制)。
- 若是在堆中沒有內存實例完成分配,而且堆也沒法在擴展時將會拋出OutOfMemoryError異常。
方法區
- 方法區是各個線程共享的內存區域
- 方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
- 雖然Java虛擬機規範把方法區描述爲堆的一部分,可是他還有個別名叫作Non-heap(非堆),目的應該是與Java堆區分開來。
- 根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError 異常。
- Java虛擬機規範對方法區的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。相對而言,垃圾收集在這個區域是比較少出現的,但並不是數據進入了方法區就如永久代的名字同樣永久存在了。這區域的內存回收目標重要是針對常量池的回收和類型的卸載,通常來講這個內存區域的回收‘成績’比較難以使人滿意。尤爲是類型的卸載條件很是苛刻,可是這部分的回收確實是必要的。在Sun公司的bug列表中,曾出現過的若干個嚴重的bug就是因爲低版本的HotSpot虛擬機對此區域未完成回收致使的內存溢出。
注意:方法區與永久代
- 對於習慣在HotSpot虛擬機上開發、部署程序的開發者來講,不少人都更願意把方法區稱爲「永久代」(Permanent Generation),本質上二者並不等價。
- 僅僅是由於HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器能夠像管理Java堆同樣管理這部份內存,可以省去專門爲方法區編寫內存管理代碼的工做。
- 對於其餘虛擬機(如 BEA JRockit、IBM J9 等)來講是不存在永久代的概念的。
- 對於HotSpot虛擬機,根據官方發佈的路線圖信息,如今也有放棄永久代並逐步改成採用Native Memory來實現方法區的規劃了,在目前已經發布的JDK1.7的HotSpot中,已經把本來放在永久代的字符串常量池移出。
運行時常量池
(見1.2.2)算法
直接內存
- 直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。可是這部份內存也被頻繁地使用,並且也可能致使 OutOfMemoryError 異常出現。
- 在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。
- 顯然,本機直接內存的分配不會受到 Java 堆大小的限制。可是,既然是內存,確定仍是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置
-Xms
等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現 OutOfMemoryError 異常。
1.2.2 常量池
Class常量池
- Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用。
- 字面量(Literal):文本字符串(如
String str = "SpiderLucas"
中SpiderLucas
就是字面量)、八種基本類型的值(如int i = 0
中0
就是字面量)、被聲明爲final
的常量等;
- 符號引用(Symbolic References):類和方法的全限定名、字段的名稱和描述符、方法的名稱和描述符。
- 每一個class文件都有一個class常量池。
字符串常量池
- 參考資料來源:Java中的常量池、完全弄懂字符串常量池等相關問題、Java中String字符串常量池
- 字符串常量池中的字符串只存在一份。
- 字符串常量池(String Constant Pool)是存儲Java String的一塊內存區域,在JDK 6以前是放置於方法區的,在JDK 7以後放置於堆中。
- 在HotSpot中實現的字符串常量池功能的是一個StringTable類,它是一個Hash表,默認值大小長度是1009;這個StringTable在每一個HotSpot VM的實例只有一份,被全部的類共享。字符串常量由一個一個字符組成,放在了StringTable上。
- StringTable的長度:在JDK 6中,StringTable的長度是固定的,所以若是放入String Pool中的String很是多,就會形成hash衝突,致使鏈表過長,當調用String#intern()時會須要到鏈表上一個一個找,從而致使性能大幅度降低;在JDK 7中,StringTable的長度能夠經過參數指定:
-XX:StringTableSize=1024
。
- 字符串常量池中存放的內容:在JDK 6及以前版本中,String Pool裏放的都是字符串常量;JDK 7中,因爲String#intern()發生了改變,所以String Pool中也能夠存放放於堆內的字符串對象的引用。
intern() 函數
- 在JDK 6中,
intern()
的處理是先判斷字符串常量是否在字符串常量池中,若是存在直接返回該常量;若是沒有找到,則將該字符串常量加入到字符串常量區,也就是在字符串常量區創建該常量。
- 在JDK 7中,
intern()
的處理是先判斷字符串常量是否在字符串常量池中,若是存在直接返回該常量,若是沒有找到,說明該字符串常量在堆中,則處理是把堆區該對象的引用加入到字符串常量池中,之後別人拿到的是該字符串常量的引用,實際存在堆中。
字符串常量池案例
String s1 = new String("Spider"); // s1 -> 堆
// 該行代碼建立了幾個對象
// 兩個對象(不考慮對象內部的對象):首先建立了一個字符串常量池的對象,而後建立了堆裏的對象
s1.intern(); // 字符串常量池中存在"Spider",直接返回該常量
String s2 = "Spider"; // s2 -> 字符串字符串常量池
System.out.println(s1 == s2); // false
String s3 = new String("Str") + new String("ing"); // s3 -> 堆
// 該行代碼建立了幾個對象?
// 反編譯後的代碼:String s3 = (new StringBuilder()).append(new String("Str")).append(new String("ing")).toString();
// 六個對象(不考慮對象內部的對象):兩個字符串常量池的對象"Str"和"ing",兩個堆的對象"Str"和"ing",一個StringBuilder,一個toString方法建立的new String對象
s3.intern(); // 字符串常量池中沒有,在JDK 7中之後會把堆中該對象的引用放在字符串常量池中(JDK 6中建立一個jdk1.6中會在字符串常量池中創建該常量)
String s4 = "String"; // s4 -> 堆(JDK 6:s4 -> 字符串字符串常量池)
System.out.println(s3 == s4); // true(JDK6 false)
String s5 = "AAA";
String s6 = "BBB";
String s7 = "AAABBB"; // s7 -> 字符串常量池
String s8 = s5 + s6; // s8 -> 堆(緣由就是如上字符串+的重載)
String s9 = "AAA" + "BBB"; // JVM會對此代碼進行優化,直接建立字符串常量池
System.out.println(s7 == s8); // false
System.out.println(s7 == s9); // true(都指向字符串常量池)
方法區與運行時常量池
- 運行時常量池(Runtime Constant Pool)是方法區的一部分。
- Class常量池的內容將在類加載後進入方法區的運行時常量池中存放。
- Java虛擬機對Class文件每一部分(天然也包括常量池)的格式都有嚴格規定,每個字節用於存儲哪一種數據都必須符合規範的要求才會被虛擬機承認、裝載和執行,但對於運行時常量池,Java 虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。
- 運行時常量池相對於Class文件常量池的另外一個重要特徵是具有動態性,Java 語言並不要求常量必定只有編譯期才能產生,也就是並不是預置如Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是 String類的intern()方法。
- 既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。
1.3 HotSpot中的對象
1.3.1 對象的建立
new一個對象的所有流程
- 從常量池中查找該類的符號引用,而且檢查該符號引用表明的類是否被加載、解析、初始化。若是類已經被加載,則跳轉至3;不然跳轉至2。
- 執行類的加載過程。
- 爲新對象分配內存空間:因爲對象所須要內存大小在類加載完成時能夠肯定,因此能夠直接從Java堆中劃分一塊肯定大小的內存。
- 把分配的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB則該操做能夠提早至TLAB中,這是爲了保證對象的字段都被初始爲默認值。
- 執行init方法,按照程序員的意願進行初始化。
對象分配內存空間詳解
- 指針碰撞:若是堆內存是規整,已經分配和爲分配的內存有一個指針做爲分界點,那麼只須要將指針向空閒內存移動便可。
- 空閒列表:若是內存是不規整的,虛擬機須要維護一個列表,記錄那些內存塊是可用的。在分配的時候從足夠大的空間劃分給對象,並更新該列表。
- Java堆是否規整取決於GC是否會壓縮整理,Serial、ParNew等帶Compact過程的收集器,分配算法是指針碰撞;是用CMS這種基於Mark-Sweep算法的收集器時,分配算法是空閒列表。
分配內存的併發問題
不管是指針碰撞仍是空閒列表,都有可能由於併發而產生問題,解決方法有兩種:數組
- 對分配內存空間的動做進行同步處理——實際上JVM採用CAS(Compare And Swap)配上失敗重試的方式保證更新操做的原子性。
- 把內存分配的動做按照線程劃分在不一樣的空間,每一個線程在Java堆中預先分配一小塊內存,成爲本地緩衝內存(Tread Local Allocation Buffer,TLAB)。哪一個線程須要分配內存,就在哪一個線程的TLAB上分配,只有TLAB用完了,才須要同步鎖定。能夠經過
-XX:+/-UseTLAB
參數來設定。
CAS原理
一個CAS方法包含三個參數CAS(V,E,N)。V表示要更新的變量,E表示預期的值,N表示新值。只有當V的值等於E時,纔會將V的值修改成N。若是V的值不等於E,說明已經被其餘線程修改了,當前線程能夠放棄此操做,也能夠再次嘗試次操做直至修改爲功。基於這樣的算法,CAS操做即便沒有鎖,也能夠發現其餘線程對當前線程的干擾(臨界區值的修改),並進行恰當的處理。
1.3.2 對象的內存佈局
在HotSpot虛擬機中,對象在內存中的存儲佈局能夠分爲3部分:對象頭(Object Header)、實例數據(Instance Data)、對齊填充(Padding)。服務器
對象頭第一部分
- 對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等,這部分數據的長度在32位和64位的虛擬機(暫不考慮開啓壓縮指針的場景)中分別爲32個和64個Bits,官方稱它爲「Mark Word」。
- 對象須要存儲的運行時數據不少,其實已經超出了3二、64位所能記錄的限度,可是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,原理是它會根據對象的狀態複用本身的存儲空間。
- 例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌位,1Bit固定爲0,在其餘狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容以下表所示。
對象頭第二部分
- 對象頭的第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。
- 並非全部的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息不必定要通過對象自己。
- 若是對象是一個數組:對象頭中還須要一塊用於記錄數組長度的數據。
實例數據
- 接下來實例數據部分是對象真正存儲的有效信息,也既是咱們在程序代碼裏面所定義的各類類型的字段內容。不管是從父類繼承下來的,仍是在子類中定義的都須要記錄下來。
- 這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中能夠看出,相同寬度的字段老是被分配到一塊兒。在知足這個前提條件的狀況下,在父類中定義的變量會出如今子類以前。
- 若是 CompactFields參數值爲true(默認爲true),那子類之中較窄的變量也可能會插入到父類變量的空隙之中。
對齊填充
第三部分對齊填充並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用。因爲HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。對象頭正好是8字節的倍數(1倍或者2倍),所以當對象實例數據部分沒有對齊的話,就須要經過對齊填充來補全。數據結構
1.3.3 對象的訪問定位
創建對象是爲了使用對象,咱們的Java程序須要經過棧上的reference數據來操做堆上的具體對象。因爲reference類型在Java虛擬機規範裏面只規定了是一個指向對象的引用,並無定義這個引用應該經過什麼種方式去定位、訪問到堆中的對象的具體位置,對象訪問方式也是取決於虛擬機實現而定的。多線程
對象的兩種訪問定位方式
主流的訪問方式有使用句柄和直接指針兩種。併發
- 句柄訪問:Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據的具體各自的地址信息,以下圖所示。
- 直接指針:Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,以下圖所示。
兩種方式比較
- 句柄訪問的優點:reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要被修改。
- 直接指針的優點:最大的好處就是速度更快,它節省了一次指針定位的時間開銷,因爲對象訪問的在Java中很是頻繁,所以這類開銷積小成多也是一項很是可觀的執行成本。
- HotSpot虛擬機:它是使用第二種方式進行對象訪問。
- 但在整個軟件開發的範圍來看,各類語言、框架中使用句柄來訪問的狀況也十分常見。
1.4 OOM異常分類
1.4.1 堆溢出
Java堆用於存儲對象實例,只要不斷建立對象,而且保證GC Roots到對象之間有可達路徑來避免GC,那麼在對象數量到達最大堆容量限制以後便會產生堆溢出。app
/**
* VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* 1.將堆的最小值-Xms與最大值-Xmx參數設置爲同樣能夠避免堆自動擴展
* 2.經過參數-XX:+HeapDumpOnOutOfMemoryError可讓虛擬機出現內存異常時Dump當前堆內存堆轉儲快照
* 3.快照位置默認爲user.dir
*/
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
// 保留引用,防止GC
List<OOMObject> list = new ArrayList<>();
for (;;) {
list.add(new OOMObject());
}
}
}
// 運行結果
// java.lang.OutOfMemoryError: Java heap space
// Dumping heap to java_pid72861.hprof ...
// Heap dump file created [27888072 bytes in 0.086 secs]
堆轉儲快照
如下是JProfiler對轉儲快照的分析
內存泄漏與內存溢出
- 重點:確認內存中的對象是不是必要的,也就是分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)
- 內存泄漏:是指程序在申請內存後,沒法釋放已申請的內存空間,一次內存泄漏彷佛不會有大的影響,但內存泄漏堆積後的後果就是內存溢出。
- 內存溢出:是指程序在申請內存時,沒有足夠的內存空間供其使用。內存泄漏的堆積最終會致使內存溢出。
內存泄漏的分類(按發生方式來分類)
- 常發性內存泄漏:發生內存泄漏的代碼會被屢次執行到,每次被執行的時候都會致使一塊內存泄漏。
- 偶發性內存泄漏:發生內存泄漏的代碼只有在某些特定環境或操做過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。因此測試環境和測試方法對檢測內存泄漏相當重要。
- 一次性內存泄漏:發生內存泄漏的代碼只會被執行一次,或者因爲算法上的缺陷,致使總會有一塊僅且一塊內存發生泄漏。好比,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,因此內存泄漏只會發生一次。
- 隱式內存泄漏:程序在運行過程當中不停的分配內存,可是直到結束的時候才釋放內存。嚴格的說這裏並無發生內存泄漏,由於最終程序釋放了全部申請的內存。可是對於一個服務器程序,須要運行幾天,幾周甚至幾個月,不及時釋放內存也可能致使最終耗盡系統的全部內存。
處理方式
- 若是是內存泄漏:須要找到泄漏對象的類型信息,和對象與GC Roots的引用鏈的信息,分析GC沒法自動回收它們的緣由。
- 若是不存在內存泄漏,即內存中的對象的確必須存活:那就應當檢查JVM的參數可否調大;從代碼上檢查是否某些對象生命週期過長、持有狀態時間過長,嘗試減小程序運行期的內存消耗。
1.4.2 棧溢出
在HotSpot虛擬機中並不區分虛擬機棧和本地方法棧,對於HotSpot來講,雖然-Xoss參數(設置本地方法棧大小)存在,但其實是無效的。棧容量只由-Xss參數設定。關於虛擬機棧和本地方法棧,在Java虛擬機規範中描述了兩種異常:
- 若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError異常。
- 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
這裏把異常分紅兩種狀況,看似更加嚴謹,但卻存在着一些互相重疊的地方:當棧空間沒法繼續分配時,究竟是內存過小,仍是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。
StackOverflowError
/**
* VM args: -Xss256k
* 1. 設置-Xss參數減少棧內存
* 2. 死遞歸增大此方法棧中本地變量表的長度
*/
public class SOF {
int stackLength = 1;
void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
SOF sof = new SOF();
try {
sof.stackLeak();
} catch (Throwable e) {
System.out.println("Stack Length:" + sof.stackLength);
throw e;
}
}
}
// Stack Length:2006
// Exception in thread "main" java.lang.StackOverflowError
// at s1.SOF.stackLeak(SOF.java:13)
// at s1.SOF.stackLeak(SOF.java:13)
多線程致使棧OOM異常
/**
* VM Args: -Xss20M
* 經過不斷建立線程的方式產生OOM
*/
public class StackOOM {
private void dontStop() {
for (;;) {
}
}
private void stackLeakByThread() {
for (;;) {
Thread thread = new Thread(this::dontStop);
thread.start();
}
}
public static void main(String[] args) {
new StackOOM().stackLeakByThread();
}
}
經過不斷建立線程的方式產生OOM異常,可是這樣產生的內存溢出異常與棧空間是否足夠大並不存在任何聯繫。或者準確地說,在這種狀況下,爲每一個線程的棧分配的內存越大,反而越容易產生內存溢出異常。
緣由:操做系統分配給每一個進程的內存是有限制的,假設操做系統的內存爲2GB,剩餘的內存爲2GB(操做系統限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區容量),程序計數器消耗內存很小,能夠忽略掉。若是虛擬機進程自己耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法棧「瓜分」了。因此每一個線程分配到的棧容量越大,能夠創建的線程數量天然就越少,創建線程時就越容易把剩下的內存耗盡。
解決方法:若是是創建過多線程致使的內存溢出,在不能減小線程數或者更換64位虛擬機的狀況下,就只能經過「減小內存」的手段來解決內存溢出——減小最大堆和減小棧容量來換取更多的線程。
1.4.3 方法區和運行時常量池溢出
因爲運行時常量池是方法區的一部分,所以這兩個區域的溢出測試就放在一塊兒進行。方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。對於這些區域的測試,基本的思路是運行時產生大量的類去填滿方法區,直到溢出。
/**
* (JDK 8)VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10m
* (JDK 7以前)VM Args: -XX:PermSize=10M -XX:MaxPermSize=10m
*/
public class MethodAreaOOM {
static class OOMClass {}
public static void main(final String[] args) {
for (;;) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMClass.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
}
// Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
// at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
// at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
// at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
// at com.ankeetc.commons.Main.main(Main.java:28)
方法區溢出場景
方法區溢出也是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,斷定條件是比較苛刻的。在常常動態生成大量Class的應用中,須要特別注意類的回收情況。這類場景主要有:
- 使用了CGLib字節碼加強,當前的不少主流框架,如Spring、Hibernate,在對類進行加強時,都會使用到CGLib這類字節碼技術,加強的類越多,就須要越大的方法區來保證動態生成的Class能夠加載入內存。
- 大量JSP或動態產生JSP文件的應用(JSP第一次運行時須要編譯爲Java類)
- 基於OSGi的應用(即便是同一個類文件,被不一樣的加載器加載也會視爲不一樣的類)等
- JVM上的動態語言(例如Groovy等)一般都會持續建立類來實現語言的動態性
1.4.4 本機直接內存溢出
下面代碼越過了DirectByteBuffer
類,直接經過反射獲取Unsafe
實例進行內存分配(Unsafe
類的getUnsafe()
方法限制了只有引導類加載器纔會返回實例,也就是設計者但願只有rt.jar中的類才能使用Unsafe
的功能)。由於,雖然使用DirectByteBuffer
分配內存也會拋出內存溢出異常,但它拋出異常時並無真正向操做系統申請分配內存,而是經過計算得知內存沒法分配,因而手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()
。
/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* DirectMemory容量可經過-XX:MaxDirectMemorySize指定
* 若是不指定,則默認與Java堆最大值(-Xmx指定)同樣。
*/
public class Main {
private static final long _1024MB = 1024 * 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(_1024MB);
}
}
}
// Exception in thread "main" java.lang.OutOfMemoryError
// at sun.misc.Unsafe.allocateMemory(Native Method)
// at com.ankeetc.commons.Main.main(Main.java:25)
DirectMemory特徵
- 由
DirectMemory
致使的內存溢出,一個明顯的特徵是在Heap Dump
文件中不會看見明顯的異常。
- 若是發現
OOM
以後Dump
文件很小,而程序中又直接或間接使用了NIO,那就能夠考慮檢查一下是否是這方面的緣由。
1.5 不一樣版本的JDK
參考資料
關於永久代和方法區
- 在 HotSpot VM 中 「PermGen Space」 其實指的就是方法區
- 「PermGen Space」 和方法區有本質的區別。前者是 JVM 規範的一種實現(HotSpot),後者是 JVM 的規範。
- 只有 HotSpot 纔有 「PermGen Space」,而對於其餘類型的虛擬機,如 JRockit、J9並無 「PermGen Space」。
不一樣版本JDK總結
- JDK 7以後將字符串常量池由永久代轉移到堆中
- JDK 8中, HotSpot 已經沒有 「PermGen space」這個區間了,取而代之是一個叫作 Metaspace(元空間) 的東西。
- 元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。
-
-XX:MetaspaceSize
:初始空間大小,達到該值就會觸發垃圾收集進行類型卸載。同時GC會對該值進行調整——若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-
-XX:MaxMetaspaceSize
:最大空間,默認是沒有限制的。
-
-XX:MinMetaspaceFreeRatio
:在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-
-XX:MaxMetaspaceFreeRatio
:在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集
-
PermSize
、MaxPermSize
參數已移除