2025-02-10

先來看段代碼:
Java代碼 
import java.util.*;  
public class SummaryCase{ 
    public static void main(String[] args) throws Exception{     
        List<Object> caches=new ArrayList<Object>();     
        for(int i=0;i<7;i++){     
            caches.add(new byte[1024*1024*3]);     
        }     
        caches.clear();     
        for(int i=0;i<2;i++){        
        caches.add(new byte[1024*1024*3]);     
        }     
        Thread.sleep(10000);     
    }  

當用-Xms30m -Xmx30m -Xmn10m -XX:+UseParallelGC 執行上面的代碼時會執行幾次Minor GC和幾次Full GC呢?
按照eden空間不足時觸發minor gc的規則,上面代碼執行後的GC應為:M、M、M、M ,但實際上上面代碼執行後GC則為:M、M、M、F、F 。
這裡的原因就在於Parallel Scavenge GC時的悲觀策略,當在eden上分配內存失敗時且對象的大小尚不需要直接在old上分配時,會觸發YGC,代碼片段如下:
Cpp代碼 
void PSScavenge::invoke(){ 
    …  
    bool scavenge_was_done = PSScavenge::invoke_no_policy(); 
    PSGCAdaptivePolicyCounters* counters = heap->gc_policy_counters(); 
    if (UsePerfData) 
        counters->update_full_follows_scavenge(0); 
    if(!scavenge_was_done || policy->should_full_GC(heap->old_gen()->free_in_bytes())){ 
        if(UsePerfData) 
            counters->update_full_follows_scavenge(full_follows_scavenge); 
            < GCCauseSetter gccs(heap, GCCause::_adaptive_size_policy);  
        if (UseParallelOldGC){ 
            PSParallelCompact::invoke_no_policy(false); 
        }else{ 
            PSMarkSweep::invoke_no_policy(false);  
        } 
    } 
    … 

PSScavenge::invoke_no_policy{ 
    …  
    if(!should_attempt_scavenge()){ 
        return false; 
    } 
    … 

bool PSScavenge::should_attempt_scavenge(){ 
    … 
    PSAdaptiveSizePolicy* policy = heap->size_policy(); 
    size_t avg_promoted = (size_t) policy->padded_average_promoted_in_bytes(); 
    size_t promotion_estimate = MIN2(avg_promoted, young_gen->used_in_bytes()); 
    bool result = promotion_estimate < old_gen->free_in_bytes(); 
    … 
    return result; 

在上面should_attempt_scavenge代碼片段中,可以看到會比較之前YGC晉升到Old中的平均大小與當前新生代中已被使用的字節數大小,取更小的值與舊生代目前剩餘空間大小對比,如更大,則返回false,就終止瞭YGC的執行瞭,當返回false時,PSScavenge::invoke就將觸發Full GC瞭。
在PSScavenge:invoke中還有一個條件為:policy->should_full_GC(heap->old_gen()->free_in_bytes(),來看看這段代碼片段:
Cpp代碼 
bool PSAdaptiveSizePolicy::should_full_GC(size_t old_free_in_bytes){ 
    bool result = padded_average_promoted_in_bytes() > (float) old_free_in_bytes; 
    … 
    return result; 

可看到,這段代碼檢查的也是之前YGC時晉升到old的平均大小是否大於瞭舊生代的剩餘空間,如大於,則觸發full gc。
總結上面分析的策略,可以看到采用Parallel GC的情況下,當YGC觸發時,會有兩個檢查:
1、在YGC執行前,min(目前新生代已使用的大小,之前平均晉升到old的大小中的較小值) > 舊生代剩餘空間大小 ? 不執行YGC,直接執行Full GC : 執行YGC;
2、在YGC執行後,平均晉升到old的大小 > 舊生代剩餘空間大小 ? 觸發Full GC : 什麼都不做。
 
按照這樣的說明,再來看看上面代碼的執行過程中eden和old大小的變化狀況:

 

代碼

Eden

Old

YGC

FGC

第一次循環

3

0

0

0

第二次循環

6

0

0

0

第三次循環

3

6

1

0

第四次循環

6

6

1

0

第五次循環

3

12

2

0

第六次循環

6

12

2

0

第七次循環

3

18

3

1

第八次循環

6

18

3

1

第九次循環

3

3

3

2

在第7次循環時,YGC後舊生代剩餘空間為2m,而之前平均晉級到old的對象大小為6m,因此在YGC後會觸發一次FGC。
而第9次循環時,在YGC執行前,此時新生代已使用的大小為6m,之前晉級到old的平均大小為6m,這兩者去最小值為6m,這個值已大於old的剩餘空間,因此就不執行YGC,直接執行FGC瞭。

 

Sun JDK之所以要有悲觀策略,我猜想理由是程序最終是會以一個較為穩態的狀況執行的,此時每次YGC後晉升到old的對象大小應該是差不多的,在YGC時做好檢查,避免等YGC後晉升到Old的對象導致old空間不足,因此還不如幹脆就直接執行FGC,正因為悲觀策略的存在,大傢有些時候可能會看到old空間沒滿但full gc執行的狀況。

 

 作者“lewis@Taobao”
 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *