Java 記憶體分配和回收機制
Java的GC機制是自動進行的,和c語言有些區別需要程式設計師自己保證記憶體的使用和回收。
Java的記憶體分配和回收也主要在Java的堆上進行的,Java的堆中儲存了大量的物件例項,所以Java的堆也叫GC堆。
Java在垃圾收集的過程中,主要用到了分代收集演算法,我會先講一下常用垃圾收集演算法。
常用垃圾收集演算法
1. 標記-清除演算法
這種垃圾收集演算法思路非常簡單,主要是首先標記出所有需要回收的物件,然後回收所有需要回收的物件。
但是有一個明顯的缺點,採用這種演算法之後會發現記憶體塊回收之後就不連續了,這就導致了在下一次想分配一個大記憶體塊的時候無法分配。
2. 標記-清除-壓縮
這種垃圾收集演算法主要是對上面的演算法進行了優化,記憶體回收了對記憶體進行了一次優化壓縮。這樣回收後記憶體塊的連續性又比較強了。
但是這種演算法會涉及到不停的記憶體間的拷貝和複製,效能會非常差。
3.標記-清除-複製
這種演算法會將記憶體空間分配成兩塊相同的區域A和B。當記憶體回收的時候,將A中的記憶體塊拷貝到B中,然後一次性清空A。
但是這種演算法會對記憶體要求比較大一些,並且長期複製拷貝效能上也會受影響。
Java分代收集演算法
Java主要採用了分代收集演算法。分代收集演算法主要將物件存活期的長短將記憶體進行劃分。
Java主要將記憶體劃分為兩部分:新生代和老生代
Java的新生代中,物件的存活率低,存活期期會相對會比較短一些,所以可以選用複製演算法來進行記憶體回收。
Java的老生代中,物件的存活率比較高,並且相對存活期比較長一些,可以採用標記-清除-壓縮的演算法來進行記憶體回收。
可以看圖:
通常新生代分為Eden和兩個Survivor,其中可以通過-XX:SurvivorRatio=1來設定(這裡要考慮兩個Survivor,意味著二個S的大小是整個新生代的2/3)
前面已經說了,Java的記憶體分配和記憶體回收主要在Java的堆上進行的。而Java的方法區間和常量池我們一般稱為永久代。永久代可以通過-XX:PermSize=512M -XX:MaxPermSize=512M
Java堆記憶體設定引數:-Xmx20m -Xms20m
Java堆新生代記憶體分配設定:-Xmn10m 新生代分配了10M的記憶體,那麼剩下的10M就是老生代上面分配了。也可以設定:-XX:NewRatio=4
通過設定引數,我們就可以在控制檯中看到Java虛擬機器在執行GC時候的日誌:-XX:+PrintGCDetails
也可以指定日誌的位置:-Xloggc:gc.log
永久代一般是指方法區和常量池,一般情況下永久代在虛擬機器執行時就能確定大小的,但是一些框架可能動態生成一些類資訊就會導致永久代越來越大。
Java記憶體分配策略
使用的ParNew+Serial Old收集器組合
1. 優先在Eden上分配。
Java的物件優先會在新生代的Eden上分配。
我們可以看一個例子:
我設定了這些引數:-XX:+PrintGCDetails -Xms20m -Xmx20m -Xmn10m,堆記憶體分配20M,新生代10M,老生代10M,預設情況下Survivor區為8:1,所以Eden區域為8M
我執行這段程式碼:
[java] view plain copy print?- publicclass JavaTest {
- staticint m = 1024 * 1024;
- publicstaticvoid main(String[] args) {
- //分配2兆
- byte[] a1 = newbyte[2 * m];
- System.out.println("a1 ok");
- //分配2兆
- byte[] a2 = newbyte[2 * m];
- System.out.println("a2 ok");
- }
- }
控制檯日誌: [html] view plain copy print?
- a1 ok
- a2 ok
- Heap
- def new generation total 9216K, used 4603K [0x331d0000, 0x33bd0000, 0x33bd0000)
- eden space 8192K, 56% used [0x331d0000, 0x3364ef50, 0x339d0000)
- from space 1024K, 0% used [0x339d0000, 0x339d0000, 0x33ad0000)
- to space 1024K, 0% used [0x33ad0000, 0x33ad0000, 0x33bd0000)
- tenured generation total 10240K, used 0K [0x33bd0000, 0x345d0000, 0x345d0000)
- the space 10240K, 0% used [0x33bd0000, 0x33bd0000, 0x33bd0200, 0x345d0000)
- compacting perm gen total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)
- the space 12288K, 3% used [0x345d0000, 0x3462f4d0, 0x3462f600, 0x351d0000)
- ro space 10240K, 55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)
- rw space 12288K, 55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)
日誌中非常清晰的可以看到,我們分配了一個4M記憶體大小,直接是分配在了eden space裡面。
2. 大物件直接進入老生代。
引數:-XX:PretenureSizeThreshold(該設定只對Serial和ParNew收集器生效) 可以設定進入老生代的大小限制,我們設定為3M,則大於3M的大物件就直接進入老生代
測試程式碼:
[java] view plain copy print?- publicclass JavaTest {
- staticint m = 1024 * 1024;
- publicstaticvoid main(String[] args) {
- //分配2兆
- byte[] a1 = newbyte[2 * m];
- System.out.println("a1 ok");
- byte[] a3 = newbyte[4 * m];
- System.out.println("a2 ok");
- }
- }
控制檯日誌: [html] view plain copy print?
- a1 ok
- a2 ok
- Heap
- def new generation total 9216K, used 2555K [0x331d0000, 0x33bd0000, 0x33bd0000)
- eden space 8192K, 31% used [0x331d0000, 0x3344ef40, 0x339d0000)
- from space 1024K, 0% used [0x339d0000, 0x339d0000, 0x33ad0000)
- to space 1024K, 0% used [0x33ad0000, 0x33ad0000, 0x33bd0000)
- tenured generation total 10240K, used 4096K [0x33bd0000, 0x345d0000, 0x345d0000)
- the space 10240K, 40% used [0x33bd0000, 0x33fd0010, 0x33fd0200, 0x345d0000)
- compacting perm gen total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)
- the space 12288K, 3% used [0x345d0000, 0x3462f4d0, 0x3462f600, 0x351d0000)
- ro space 10240K, 55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)
- rw space 12288K, 55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)
上面的日誌中,可以清洗看到第一次分配的2M留存在了eden space中,而4M超過了大物件設定的值3M,所以直接進入了老生代tenured generation
3. 長期存活的物件進入老年代
為了演示方便,我們設定-XX:MaxTenuringThreshold=1(預設15),當在新生代中年齡為1的物件進入老年代。
測試程式碼:
[java] view plain copy print?- publicclass JavaTest {
- staticint m = 1024 * 1024;
- publicstaticvoid main(String[] args) {
- //分配2兆
- byte[] a1 = newbyte[1 * m / 4];
- System.out.println("a1 ok");
- byte[] a2 = newbyte[7 * m];
- System.out.println("a2 ok");
- byte[] a3 = newbyte[3 * m]; //GC
- System.out.println("a3 ok");
- }
- }
控制檯日誌: [html] view plain copy print?
- a1 ok
- a2 ok
- [GC [DefNew: 7767K->403K(9216K), 0.0062209 secs] 7767K->7571K(19456K), 0.0062482 secs]
- [Times: user=0.00 sys=0.00, real=0.01 secs]
- a3 ok
- Heap
- def new generation total 9216K, used 3639K [0x331d0000, 0x33bd0000, 0x33bd0000)
- eden space 8192K, 39% used [0x331d0000, 0x334f9040, 0x339d0000)
- from space 1024K, 39% used [0x33ad0000, 0x33b34de8, 0x33bd0000)
- to space 1024K, 0% used [0x339d0000, 0x339d0000, 0x33ad0000)
- tenured generation total 10240K, used 7168K [0x33bd0000, 0x345d0000, 0x345d0000)
- the space 10240K, 70% used [0x33bd0000, 0x342d0010, 0x342d0200, 0x345d0000)
- compacting perm gen total 12288K, used 381K [0x345d0000, 0x351d0000, 0x385d0000)
- the space 12288K, 3% used [0x345d0000, 0x3462f548, 0x3462f600, 0x351d0000)
- ro space 10240K, 55% used [0x385d0000, 0x38b51140, 0x38b51200, 0x38fd0000)
- rw space 12288K, 55% used [0x38fd0000, 0x396744c8, 0x39674600, 0x39bd0000)
我們可以看到在A3處有一次GC,並且a2的7M已經滿足-XX:MaxTenuringThreshold=1的要求,所以a2進入老年代,而空出來的空間a3就進入新生代
4. 動態物件年齡判定
為了使記憶體分配更加靈活,虛擬機器並不要求物件年齡達到MaxTenuringThreshold才晉升老年代
如果Survivor區中相同年齡所有物件大小的總和大於Survivor區空間的一半,年齡大於或等於該年齡的物件在Minor GC時將複製至老年代
5. 空間分配擔保
新生代使用複製演算法,當Minor GC時如果存活物件過多,無法完全放入Survivor區,就會向老年代借用記憶體存放物件,以完成Minor GC。
在觸發Minor GC時,虛擬機器會先檢測之前GC時租借的老年代記憶體的平均大小是否大於老年代的剩餘記憶體,如果大於,則將Minor GC變為一次Full GC,如果小於,則檢視虛擬機器是否允許擔保失敗,如果允許擔保失敗,則只執行一次Minor GC,否則也要將Minor GC變為一次Full GC。
說白了,新生代放不下就會借用老年代的空間來進行GC
Java垃圾收集器:
首先我們可以看一張圖,下面這張圖中列出來新生代和老生代可以用到的垃圾收集器。
1. Serial 收集器 序列
單執行緒的序列收集器。它在垃圾收集的時候會暫停其它所有工作執行緒。直到收集結束。一般在客戶端模式下使用。
2. ParNew收集器 並行
ParNew收集器是Serial的多執行緒版本。一般執行在Server模式下首先的新生代收集器。如果老年代使用CMS收集器,基本也只能和它進行合作。引數:-XX:+UseConcMarkSweepGC,比較適合web服務的收集器。
一般ParNew和CMS組合
3. Parallel Scavenge收集器 並行
它使用複製演算法的收集器,並且是多執行緒的。該收集器主要目的就是達到一個可控制的吞吐量,說白了就是CPU的利用率。於是該收集器比較適合後端運算比較多的服務。
-XX:MaxGCPauseMillis每次年輕代垃圾回收的最長時間(最大暫停時間),收集器儘量保證記憶體回收時間不大於這個值,應該設定一個合理的值。
-XX:GCTimeRatio設定垃圾回收時間佔程式執行時間的百分比
-XX:+UseAdaptiveSizePolicy 設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟.
4.Serial Old收集器 序列
單執行緒序列的老生代收集器。
5. Parallel Old 收集器 並行
使用“標記-整理”的演算法。該收集器比較適合和Parallel Scavenge收集器進行組合。-XX:+UseParallelOldGC
6. CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,目前大部分的B/S系統都使用CMS的收集器。一般CMS是老生代收集器,新生代就和PerNew進行組合。
CMS收集器基於“標記-清除”的演算法。分四個階段:初始標記,併發標記,重新標記,併發清除
CMS收集器的優點:併發收集、低停頓
CMS缺點:
1. CMS收集器對CPU資源非常敏感。在併發階段,雖然不會導致使用者執行緒停頓,但是會佔用CPU資源而導致引用程式變慢,總吞吐量下降。CMS預設啟動的回收執行緒數是:(CPU數量+3) / 4。
2. CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure“,失敗後而導致另一次Full GC的產生。由於CMS併發清理階段使用者執行緒還在執行,伴隨程式的執行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。這一部分垃圾稱為“浮動垃圾”。也是由於在垃圾收集階段使用者執行緒還需要執行,即需要預留足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分記憶體空間提供併發收集時的程式運作使用。在預設設定下,CMS收集器在老年代使用了68%的空間時就會被啟用,也可以通過引數-XX:CMSInitiatingOccupancyFraction的值來提供觸發百分比,以降低記憶體回收次數提高效能。要是CMS執行期間預留的記憶體無法滿足程式其他執行緒需要,就會出現“Concurrent Mode Failure”失敗,這時候虛擬機器將啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說引數-XX:CMSInitiatingOccupancyFraction設定的過高將會很容易導致“Concurrent Mode Failure”失敗,效能反而降低。
3. CMS是基於“標記-清除”演算法實現的收集器,使用“標記-清除”演算法收集後,會產生大量碎片。空間碎片太多時,將會給物件分配帶來很多麻煩,比如說大物件,記憶體空間找不到連續的空間來分配不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了