垃圾回收
一、java堆內存的細分
1、分代收集算法
分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據對象存活的不同生命週期將內存劃分爲不同的域,一般情況下將 GC 堆劃分爲 新生代(Eden 區、Survivor From 區和 Survivor To 區)和老年代 。。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。
2、新生代
新生代是用來存放新生的對象。一般佔據堆的 1/3 空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又分爲 Eden 區、ServivorFrom、ServivorTo 三個區。
2.1、Eden 區
Java 新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
2.2、Survivor From 區
上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。
2.2、Survivor To 區
保留了一次 MinorGC 過程中的倖存者。
3、老年代
老年代主要存放應用程序中生命週期長的內存對象。
老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
4、元數據
在 Java8 中, 永久代已經被移除,被一個稱爲“元數據區”(元空間)的區域所取代 。元空間的本質和永久代類似,元空間與永久代之間最大的區別在於: 元空間並不在虛擬機中,而是使用本地內存 。因此,默認情況下,元空間的大小僅受本地內存限制。 類的元數據放入 native memory, 字符串池和類的靜態變量放入 java 堆中 ,這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。
5、對象分配
- 優先在Eden區分配。當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將Eden區和其中一塊Survivor區內尚存活的對象放入另一塊Survivor區域。如Minor GC時survivor空間不夠,對象提前進入老年代,老年代空間不夠時進行Full GC;
- 大對象直接進入老年代,避免在Eden區和Survivor區之間產生大量的內存複製, 此外大對象容易導致還有不少空閒內存就提前觸發GC以獲取足夠的連續空間.
6、對象晉級
- 年齡閾值: VM爲每個對象定義了一個對象年齡(Age)計數器, 經第一次Minor GC後仍然存活, 被移動到Survivor空間中, 並將年齡設爲1. 以後對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增加到一定程度(-XX:MaxTenuringThreshold, 默認15), 將會晉升到老年代.
- 提前晉升: 動態年齡判定;如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就可以直接進入老年代, 而無須等到晉升年齡.
二、垃圾回收
1、怎麼定位垃圾
在垃圾回收之前,肯定得先確認哪些是垃圾,這裏有兩種方法: 引用計數法 和 可達性分析 ;其中java虛擬機用的是可達性分析。
1.1、引用計數法
在 Java 中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不爲 0,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。
1.2、可達性分析
爲了解決引用計數法的循環引用問題,Java 使用了可達性分析的方法。通過一系列的“GC roots”對象作爲起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。
在Java, 可作爲GC Roots的對象包括:
- 方法區: 類靜態屬性引用的對象;
- 方法區: 常量引用的對象;
- 虛擬機棧(本地變量表)中引用的對象.
- 本地方法棧JNI(Native方法)中引用的對象。
要注意的是,不可達對象不等價於可回收對象,不可達對象變爲可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。
2、怎麼回收垃圾
2.1、標記清除算法(Mark-Sweep)
最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所佔用的空間。如圖:
缺點:
效率問題: 標記和清除過程的效率都不高。
空間問題: 標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會導致在運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集。
2.2、複製算法(copying)
爲了解決 Mark-Sweep 算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存清掉,如圖:
優點:
- 由於是每次都對整個半區進行內存回收,內存分配時不必考慮內存碎片問題。
- 垃圾回收後空間連續,只要移動堆頂指針,按順序分配內存即可,實現簡單,內存效率高;
- 特別適合java朝生夕死的對象特點;
缺點:
- 內存減少爲原來的一半,太浪費了;
- 對象存活率較高的時候就要執行較多的複製操作,效率變低;
- 如果不使用50%的對分策略,老年代需要考慮的空間擔保策略
目前大部分 JVM 的 GC 對於新生代都採取 Copying 算法,因爲新生代中每次垃圾回收都要回收大部分對象,即要複製的操作比較少,但通常並不是按照 1:1 來劃分新生代。一般將新生代劃分爲一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊 Survivor 空間中。
2.3、標記整理算法(Mark-Compact)
結合了以上兩個算法,爲了避免缺陷而提出。標記階段和 Mark-Sweep 算法相同,標記後不是清理對象,而是將存活對象移向內存的一端。然後清除端邊界外的對象。如圖:
優點:
- 不會損失50%的空間;
- 垃圾回收後空間連續,只要移動堆頂指針,按順序分配內存即可;
- 比較適合有大量存活對象的垃圾回收;
缺點:
- 標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,標記/整理算法要低於複製算法。
老年代因爲每次只回收少量對象,因而採用 標記整理算法(Mark-Compact)。
- JAVA 虛擬機提到過的處於方法區的永生代(Permanet Generation),它用來存儲 class 類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。
- 對象的內存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放對象的那一塊),少數情況(對象較大)會直接分配到老生代。
- 當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden Space 和 From Space 區的存活對象會被挪到 To Space,然後將 Eden Space 和 From Space 進行清理。
- 如果 To Space 無法足夠存儲某個對象,則將這個對象存儲到老生代。
- 在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆循環。
- 當對象在 Survivor 區躲過一次 GC 後,其年齡就會+1。默認情況下年齡到達 15 的對象會被移到老生代中。
三、垃圾回收器
由於新生代和老年代的特點不同,所以會用不同的垃圾回收器去回收,而不同的垃圾回收器用的算法不盡相同,且一般會用一個新生代垃圾回收器配合一個老年代垃圾回收器進行垃圾回收。但是G1回收器比較牛逼,他可以直接回收新生代和老年代,算法也和其他回收器不太一樣,會是以後主流的垃圾回收器。不同垃圾回收器可以搭配使用的關係如下:
新生代垃圾回收器:
收集器 | 收集對象和算法 | 收集器類型 | 說明 | 使用場景 |
---|---|---|---|---|
Serial | 新生代,複製算法 | 單線程 | 進行垃圾收集時,必須暫停所有工作線程,直到完成;(stop the world) | 簡單高效;適合內存不大的情況; |
ParNew | 新生代,複製算法 | 並行的多線程收集器 | ParNew垃圾收集器是Serial收集器的多線程版本 | 搭配CMS垃圾回收器的首選 |
Parallel Scavenge吞吐量優先收集器 | 新生代,複製算法 | 並行的多線程收集器 | 類似ParNew,更加關注吞吐量,達到一個可控制的吞吐量; | 本身是Server級別多CPU機器上的默認GC方式,主要適合後臺運算不需要太多交互的任務; |
注:
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+ 垃圾收集時間)
垃圾收集時間= 垃圾回收頻率 * 單次垃圾回收時間
老年代垃圾回收器:
收集器 | 收集對象和算法 | 收集器類型 | 說明 | 使用場景 |
---|---|---|---|---|
Serial Old | 老年代,標記整理算法 | 單線程 | jdk7/8默認的老生代垃圾回收器 | Client模式下虛擬機使用 |
Parallel Old | 老年代,標記整理算法 | 並行的多線程收集器 | Parallel Scavenge收集器的老年代版本,爲了配合Parallel Scavenge的面向吞吐量的特性而開發的對應組合; | 在注重吞吐量以及CPU資源敏感的場合採用 |
CMS | 老年代,標記清除算法 | 並行的多線程收集器 | 儘可能的縮短垃圾收集時用戶線程停止時間;缺點在於:1.內存碎片 2.需要更多cpu資源 3.浮動垃圾問題,需要更大的堆空間 | 重視服務的響應速度、系統停頓時間和用戶體驗的互聯網網站或者B/S系統。互聯網後端目前cms是主流的垃圾回收器; |
G1 | 跨新生代和老年代;標記整理 + 化整爲零 | 並行的多線程收集器 | JDK1.7才正式引入,採用分區回收的思維,基本不犧牲吞吐量的前提下完成低停頓的內存回收;可預測的停頓是其最大的優勢; | 面向服務端應用的垃圾回收器,目標爲取代CMS |
垃圾回收默認配置及互聯網後臺推薦配置
- 在JVM的客戶端模式(Client)下,JVM默認垃圾收集器是串行垃圾收集器(Serial GC + Serial Old,-XX:+USeSerialGC);
- 在JVM服務器模式(Server)下默認垃圾收集器是並行垃圾收集器(Parallel Scavaenge +Serial Old,-XX:+UseParallelGC)
- 而適用於Server模式下
- ParNew + CMS + SerialOld(失敗擔保),-XX:UseConcMarkSweepGC;
- Parallel scavenge + Parallel,-XX:UseParallelOldGC
四、java四種引用類型
1、強引用
在 Java 中最常見的就是強引用,把一個對象賦給一個引用變量,這個引用變量就是一個強引用。當一個對象被強引用變量引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該對象以後永遠都不會被用到 JVM 也不會回收。因此強引用是造成 Java 內存泄漏的主要原因之一。
2、軟引用
軟引用需要用 SoftReference 類來實現,對於只有軟引用的對象來說,當系統內存足夠時它不會被回收,當系統內存空間不足時它會被回收。軟引用通常用在對內存敏感的程序中。
3、弱引用
弱引用需要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的對象來說,只要垃圾回收機制一運行,不管 JVM 的內存空間是否足夠,總會回收該對象佔用的內存。
4、虛引用
虛引用需要 PhantomReference 類來實現,它不能單獨使用,必須和引用隊列聯合使用。虛引用的主要作用是跟蹤對象被垃圾回收的狀態。
常見內存溢出溢出問題
java內存溢出異常主要有兩個:
- OutOfMemeoryError:當堆、棧(多線程情況)、方法區、元數據區、直接內存中數據達到最大容量時產生;
- StackOverFlowError:如果線程請求的棧深度大於虛擬機鎖允許的最大深度,將拋出StackOverFlowError,其本質還是數據達到最大容量;
一、堆溢出
1、產生原因
堆用於存儲實例對象,只要不斷創建對象,並且保證GC Roots到對象之間有引用的可達,避免垃圾收集器回收實例對象,就會在對象數量達到堆最大容量時產生OutOfMemoryError異常。 java.lang.OutOfMemoryError: Java heap space
2、解決方法
使用-XX:+HeapDumpOnOutOfMemoryError可以讓java虛擬機在出現內存溢出時產生當前堆內存快照以便進行異常分析,主要分析那些對象佔用了內存;也可使用jmap將內存快照導出;一般檢查哪些對象佔用空間比較大,由此判斷代碼問題,沒有問題的考慮調整堆參數;
二、棧溢出
1、產生原因
- 如果線程請求的棧深度大於虛擬機鎖允許的最大深度,將拋出StackOverFlowError;
- 如果虛擬機在擴展棧時無法申請到足夠的內存空間,拋出OutOfMemeoryError;
2、解決辦法
- StackOverFlowError 一般是函數調用層級過多導致,比如死遞歸、死循環,避免這種情況的發生;
- OutOfMemeoryError一般是在多線程環境纔會產生,一般用“減少內存的方法”,既減少最大堆和減少棧容量來換取更多的線程支持;
三、方法區或元數據區溢出
1、產生原因
- jdk 1.6以前,運行時常量池還是方法區一部分,當常量池滿了以後(主要是字符串變量),會拋出OOM異常;
- 方法區和元數據區還會用於存放class的相關信息,如:類名、訪問修飾符、常量池、方法、靜態變量等;當工程中類比較多,而方法區或者元數據區太小,在啓動的時候,也容易拋出OOM異常;
2、解決辦法
- jdk 1.7之前,通過-XX:PermSize,-XX:MaxPerSize,調整方法區的大小;
- jdk 1.8以後,通過-XX:MetaspaceSize ,-XX:MaxMetaspaceSize,調整元數據區的大小;
四、本機直接內存溢出
1、產生原因
jdk本身很少操作直接內存,而直接內存(DirectMemory)導致溢出最大的特徵是,Heap Dump文件不會看到明顯異常,而程序中直接或者間接的用到了NIO;
2、解決辦法
直接內存不受java堆大小限制,但受本機總內存的限制,可以通過MaxDirectMemorySize來設置(默認與堆內存最大值一樣)