1. 運行時記憶體劃分
1.1. 程式計數器 位元組碼行號指標,用於讀取下一條需要執行的位元組碼指令。 對Java方法記錄虛擬機器位元組碼指令地址;對Native方法記錄值為空白。 線程私人,各線程互不影響。
1.2. 虛擬機器棧 Java方法執行過程所建立,每調用一個方法就會建立一個棧幀並將之入棧,方法結束後會將棧幀出棧。 棧幀存放局部變數表(編譯期分配,包括基礎資料型別 (Elementary Data Type)、對象引用),運算元棧,動態連結,方法出口。 線程私人,各線程互不影響。 可以拋出兩個異常:StackOverFlowError(請求的棧深度過大)和OutOfMemoryError(動態擴充無法申請到足夠記憶體)。
1.3. 本地方法棧 Native方法執行過程所建立,與虛擬機器棧類似。 Sun HotSpot虛擬機器將虛擬機器棧和本地方法棧合并。
1.4. Java堆 記憶體中最大的一塊,用於存放對象執行個體和數組,垃圾收集器主要管理的地區。 虛擬機器啟動時即建立,所有線程共用,但會有線程私人的分配緩衝區。 可以拋出異常OutOfMemoryError(動態擴充無法申請到足夠記憶體)。
堆⎧⎩⎨⎪⎪⎪⎪新生代老年代1/3⎧⎩⎨⎪⎪EdenFrom SurvivorTo Survivor8/101/101/102/3 堆\left\{\begin{array}{ll}新生代&1/3\left\{\begin{array}{ll}Eden&8/10\\From\ Survivor&1/10\\To\ Survivor&1/10\\\end{array}\right.\\老年代&2/3\end{array}\right.
1.5. 方法區 存放類資訊,運行時常量池,靜態變數,即時編譯代碼等,Sun HotSpot虛擬機器用永久代實現方法區。 記憶體回收較少出現,目標僅有常量池和類型卸載。 Class檔案常量池所存放編譯期的字面量(String變數,final常量)和符號引用(類名,成員變數名,方法名),在類載入後進入運行時常量池。 所有線程共用。 可以拋出異常OutOfMemoryError(動態擴充無法申請到足夠記憶體)。
另外:直接記憶體
並不是運行時記憶體的一部分,是NIO類利用Native方法分配的堆外記憶體,在堆上有DirectByteBuffer對象引用了直接記憶體,可以避免Java堆和Native堆來回複製。
不受Java堆大小的限制,但是會可以拋出異常OutOfMemoryError。 2. 記憶體溢出異常 2.1. Java堆溢出 對象執行個體到達最大堆容量限制,產生java.lang.OutOfMemoryError: Java heap space異常。 解決方案策略:如果是記憶體流失,分析泄漏對象到GC Roots引用路徑,找到垃圾收集器不能自動回收的原因;如果是記憶體溢出,說明虛擬機器記憶體參數分配過小,或者代碼中有些對象生命週期過長。 JVM相關參數:-Xms,-Xmx 2.2. 虛擬機器棧和本地方法棧溢出 一個線程請求的棧深度過大,產生java.lang.StackOverflowError異常。 建立過多線程,為線程分配的棧會導致記憶體不足,產生java.lang.OutOfMemoryError: unable to create new native thread異常。 JVM相關參數:-Xss 2.3. 方法區和運行時常量池溢出 運行時常量池是方法區的一部分,由永久代實現。如果編譯時間的字面量過多或產生大量Class類(反射)會導致運行時常量池產生java.lang.OutOfMemoryError: PermGen space異常。 String.intern()方法可以在程式運行時操作運行時常量池,即當運行時常量池中已經存放了等於該String對象的字串,則返回運行時常量池中的引用,否則要先將該String對象的字串添加入運行時常量池,再返回該String對象的字串的引用。如果不斷建立不同的字串並調用String.intern()方法就會溢出。 JVM相關參數:-XX:PermSize,-XX:MaxPermSize 2.4. 直接記憶體溢出 利用反射擷取Unsafe的執行個體,並通過Unsafe.allocateMemory()大量分配記憶體,產生java.lang.OutOfMemoryError異常。 JVM相關參數:-XX: MaxDirectMemorySize(預設和-Xmx一樣) 3. 垃圾收集 3.1. 需要回收的記憶體
程式計數器、虛擬機器棧、本地方法棧隨線程而生滅,不需要回收。Java堆和方法區在運行時才知道需要分配哪些記憶體,需要回收。 3.1.1. 引用計數法(JVM不使用)
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的需要回收。缺點是無法解決循環參考問題。 3.1.2. 可達性分析法(JVM使用)
從根(GC Roots)對象作為起始點,開始向下搜尋,搜尋所走過的路徑稱為“引用鏈”,當一個對象到GC Roots沒有任何引用鏈相連,則證明此對象是停用需要回收。
可以用作GC Roots的對象有 虛擬機器棧(棧幀中的本地變數表)中引用的對象 方法區中的靜態對象 方法區中的常量對象(常量池中) 本地方法棧中JNI(Native方法)引用的對象 3.1.3. 引用分類 強引用:普遍存在,記憶體回收行程絕不會回收,即使程式異常終止 軟引用(SoftReference類):記憶體足夠時不會回收;記憶體不足而在溢出之前,就會對這些對象進行二次回收,如果記憶體依舊不足才拋出異常。可以實現記憶體敏感的快取 弱引用(WeakReference類):只能生存到下一次記憶體回收之前,屆時不管記憶體足夠與否,都會回收 虛引用(PhantomReference類):和沒有任何引用一樣不能擷取對象執行個體,在任何時候都可能被回收,僅跟蹤對象被回收的活動 3.1.4. 方法區回收
主要針對廢棄常量和無用的類,但是不使用了不一定就要回收,因為方法區(永久代)回收效率遠低於Java堆。 3.2. 垃圾收集的時機和方式 3.2.1. 標記-清除演算法 先標記(通過根節點,標記所有從根節點開始的不可達對象),後清除(統一回收被標記的對象)。 缺點:標記和清除需要遍曆效率不高,標記清除後會產生大量不連續的片段。 3.2.2. 複製演算法(新生代) 將原有的記憶體空間分為相等的兩塊,每次只使用其中一塊,當記憶體不足時,將其中存活對象複製到未使用的記憶體塊中,然後清除正在使用的記憶體塊中的所有對象。適用於對象存活率低的情況,對應於新生代的Minor GC。 優點:無記憶體片段,按順序分配記憶體實現簡單高效。 缺點:空間浪費。 改進:將記憶體分為三塊,一塊大的Eden(8/10)和兩塊小的Survivor(1/10),每次使用Eden和其中一塊Survivor,且優先分配在Eden。回收時將Eden和Survivor中存活對象複製到另外一塊Survivor,最後清理使用的Eden和Survivor。當Survivor不夠用時,需要依賴於老年代進行分配擔保,使大對象直接進入老年代。 3.2.3. 標記-整理演算法(老年代) 先標記(通過根節點,標記所有從根節點開始的可達對象),後整理(所有的可達對象移動到記憶體的一端並清理邊界外記憶體)。適用於對象存活率高的情況,對應於老年代的Full GC。 優點:無記憶體片段。 缺點:標記移動效率不高。
三種方法的比較
效率:複製演算法 > 標記-整理演算法 > 標記-清除演算法。
記憶體整齊度:複製演算法 = 標記-整理演算法 > 標記-清除演算法。
記憶體利用率:標記-整理演算法 = 標記-清除演算法 > 複製演算法。 4. 垃圾收集器 4.1. Serial收集器 單線程的複製演算法收集器,且進行垃圾收集時必須暫停其他所有的背景工作執行緒(Stop-The-World)。 應用:client模式下的預設新生代收集器。 優點:沒有線程互動的開銷,有最高的單線程收集效率。 4.2. ParNew收集器 多線程的複製演算法收集器,其餘和Serial收集器類似。 應用:server模式下的首選新生代收集器。 優點:單線程上收集效率不高,但是可以利用多CPU資源。 4.3. Parallel Scavenge收集器 多線程的複製演算法收集器,和ParNew收集器類似,但是可以控制CPU輸送量(使用者代碼時間/總時間),提高CPU利用率。 應用:適合後台運算不需要太多互動的新生代收集器。 優點:可以提高輸送量,但是可能會增加停頓時間。因為輸送量提高,GC的頻率會減少,每次GC的停頓就會隨之增長。可以自適應動態調整輸送量和停頓時間。 4.4. Serial Old收集器 單線程的標記-整理演算法收集器,可以和各種新生代搭配使用的老年代收集器。 應用:client模式下的預設老年代收集器,server模式下作為CMS收集器的備選方案。 4.5. Parallel Old收集器 多線程的標記-整理演算法收集器。 應用:可以和Parallel Scavenge收集器搭配的老年代收集器。 優點:可以利用多CPU資源。 4.6. CMS收集器 多線程的標記-清理演算法收集器。包括4個步驟:初始標記,並發標記,重新標記,並發清除。並發標記和並發清除操作可以和使用者線程一起工作。 應用:可以擷取最短回收時間的老年代收集器。 缺點:1. 並發階段佔用了線程使輸送量降低;2. 無法處理浮動垃圾(使用者程式新產生的垃圾)會導致收集失敗,從而利用Saerial Old收集器再次收集;3. 標記-清理產生記憶體片段,整理是不能並發的。 4.7. G1收集器 可以獨立管理新生代和老年代的收集器。 優點:1. 並發收集減少停頓;2. 分代收集;3. 融合了標記-整理和複製演算法,不會有記憶體片段;4. 可預測的停頓時間。 5. JVM參數
| 參數 |
含義 |
預設值 |
| -Xms |
初始堆大小 |
實體記憶體的1/64 |
| -Xmx |
最大堆大小 |
實體記憶體的1/4 |
| -Xmn |
新生代大小 |
堆的3/8 |
| -XX:NewSize |
設定新生代大小 |
|
| -XX:MaxNewSize |
新生代最大值 |
|
| -XX:PermSize |
設定永久代初始值 |
實體記憶體的1/64 |
| -XX:MaxPermSize |
設定永久代最大值 |
實體記憶體的1/4 |
| -Xss |
每個線程棧大小 |
小應用128k夠用 大應用建議256k |
| -XX:NewRatio |
老年代與新生代的比值(除去永久代) |
|
| -XX:SurvivorRatio |
Eden與一個Survivor的比值 |
|