溫州網(wǎng)站建設(shè)咨詢谷歌app官方下載
寫在前面:
版本信息:
jdk版本:jdk8u40
垃圾回收器:ParallelScavenge new/old
最近在群里看到有一位老哥拿著異常信息到處問,而發(fā)生的就是java.lang.OutOfMemoryError: GC overhead limit exceeded異常,恰好看到經(jīng)常有人詢問關(guān)于這個異常的問題,如何發(fā)生的,要如何解決呢?所以促使我寫下這篇文章,此文章分為3大塊,出現(xiàn)的原因,如何解決,源碼論證。
異常出現(xiàn)的原因
full gc回收時間大于98%(這里是一個算法,可以忽略,只需要明白最近一直花大量時間在GC),并且回收后可用空間小于總空間的2%。 這樣的情況下,達(dá)到5次就會拋出java.lang.OutOfMemoryError: GC overhead limit exceeded異常。
總而言之:我們首先要明白,GC的過程是需要STW(STOP-THE-WORD) 也即業(yè)務(wù)線程是需要停止工作,而GC過程中消耗大量時間回收空間,而回收后的可使用空間僅僅只有總空間的2%,往往下次new對象的時候又去GC了,周而復(fù)始,給用戶的體驗(yàn)是當(dāng)前系統(tǒng)已經(jīng)完全卡死了~
所以在種種因素下JVM認(rèn)為你已經(jīng)沒必要去GC了,GC也是毫無意義的事情了,完全卡死的情況下,還不如我給你拋出java.lang.OutOfMemoryError: GC overhead limit exceeded異常,開發(fā)者好好去排查一下問題~
如何解決
僅供參考,還是需要分析自身系統(tǒng)環(huán)境做出不同的策略。
- 堆空間是否設(shè)置的太少?可以在啟動時添加-Xms -Xmx參數(shù)設(shè)置堆大小
- 分析是否存在內(nèi)存泄露?
- 如果項(xiàng)目龐大,是否需要提升硬件?
- 啟動時添加-XX:+HeapDump0n0ut0fMemoryError? ?-XX:HeapDumpPath= "路徑" 參數(shù)下次發(fā)生OOM時便可分析
- 分析項(xiàng)目中經(jīng)常使用的大對象,是否可以優(yōu)化一下空間?
- 是否可以把項(xiàng)目中非重要的緩存數(shù)據(jù)設(shè)置成軟、弱引用對象
- 以上的分析可以使用阿里開源的 "Arthas" 工具
- 還有很多筆者暫時沒有考慮到的.......
源碼論證
由于大部分的公司還是停留在Java8,并且垃圾回收器也是默認(rèn)的ParallelScavenge new/old,所以直接給出ParallelScavenge new/old垃圾回收器的部分核心源碼
src/share/vm/gc_interface/collectedHeap.inline.cpp 文件中,?common_mem_allocate_noinit方法,此方法嘗試開辟對象,如果開辟不成功,就會根據(jù)當(dāng)前不同OOM異常種類Dump和拋出對應(yīng)的異常
HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {bool gc_overhead_limit_was_exceeded = false;// 嘗試在堆空間開辟對象result = Universe::heap()->mem_allocate(size,&gc_overhead_limit_was_exceeded);// 根據(jù)gc_overhead_limit_was_exceeded參數(shù)區(qū)分是那種OOM異常。if (!gc_overhead_limit_was_exceeded) {// -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support// 英文注釋特別明顯了,如果設(shè)置了dump參數(shù),就導(dǎo)出report_java_out_of_memory("Java heap space");// 拋出OOM:Java heap spaceTHROW_OOP_0(Universe::out_of_memory_error_java_heap());} else { // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support// 英文注釋特別明顯了,如果設(shè)置了dump參數(shù),就導(dǎo)出report_java_out_of_memory("GC overhead limit exceeded");// 拋出OOM:GC overhead limit exceededTHROW_OOP_0(Universe::out_of_memory_error_gc_overhead_limit());}
}
而我們關(guān)心的是GC overhead limit exceeded異常,而這里是根據(jù)gc_overhead_limit_was_exceeded變量來做區(qū)分,而gc_overhead_limit_was_exceeded變量傳入mem_allocate方法,所以我們接著看mem_allocate方法src/share/vm/gc_implementation/parallelScavenge/parallelScavengeHeap.cpp 文件中mem_allocate方法,此方法也是開辟對象的過程。
HeapWord* ParallelScavengeHeap::mem_allocate(size_t size,bool* gc_overhead_limit_was_exceeded) {// 嘗試在young_gen空間創(chuàng)建對象HeapWord* result = young_gen()->allocate(size);// 在年輕代沒有創(chuàng)建出對象。while (result == NULL) {{// 因?yàn)榇嬖阪i的原因,所以下面又在年輕代嘗試了一次。MutexLocker ml(Heap_lock);gc_count = Universe::heap()->total_collections();result = young_gen()->allocate(size);if (result != NULL) {return result;}// 年輕代開辟不了,老年代嘗試一下。result = mem_allocate_old_gen(size);if (result != NULL) {return result;}// 超過允許嘗試的次數(shù),直接返回if (gclocker_stalled_count > GCLockerRetryAllocationCount) {return NULL;}}// 需要觸發(fā)GC,回收空間后再嘗試開辟對象if (result == NULL) {// 觸發(fā)GC回收空間VM_ParallelGCFailedAllocation op(size, gc_count);VMThread::execute(&op);if (op.prologue_succeeded()) {// 是否達(dá)到次數(shù),是否清空軟引用了const bool limit_exceeded = size_policy()->gc_overhead_limit_exceeded();const bool softrefs_clear = collector_policy()->all_soft_refs_clear();if (limit_exceeded && softrefs_clear) {// 設(shè)置為true,代表拋出GC overhead limit exceeded異常*gc_overhead_limit_was_exceeded = true; size_policy()->set_gc_overhead_limit_exceeded(false);return NULL;}return op.result();}}}return result;
}
我們看到后續(xù)GC回收后,判斷l(xiāng)imit_exceeded 和?softrefs_clear,如果都為true就把gc_overhead_limit_was_exceeded設(shè)置為true。
而softrefs_clear變量是清空軟引用,我們知道,在JVM中,內(nèi)存實(shí)在不足的時候會清空軟引用
而我們看到limit_exceeded變量的設(shè)置即可??春螘r把他設(shè)置為true即可。
src/share/vm/gc_implementation/share/adaptiveSizePolicy.cpp 文件中check_gc_overhead_limit方法
void AdaptiveSizePolicy::check_gc_overhead_limit(size_t young_live,size_t eden_live,size_t max_old_gen_size,size_t max_eden_size,bool is_full_gc,GCCause::Cause gc_cause,CollectorPolicy* collector_policy) {// 當(dāng)前eden空閑大小const size_t free_in_eden = max_eden_size > live_in_eden ?max_eden_size - live_in_eden : 0;// 當(dāng)前老年代空閑大小const size_t free_in_old_gen = (size_t)(max_old_gen_size - avg_old_live()->average());// 當(dāng)前堆空間空閑大小const size_t total_free_limit = free_in_old_gen + free_in_eden;// 堆空間的總大小const size_t total_mem = max_old_gen_size + max_eden_size;// GCHeapFreeLimit是2// 所以這里是算出比例,2%const double mem_free_limit = total_mem * (GCHeapFreeLimit/100.0);const double mem_free_old_limit = max_old_gen_size * (GCHeapFreeLimit/100.0);const double mem_free_eden_limit = max_eden_size * (GCHeapFreeLimit/100.0);// GCTimeLimit是98// 所以這里是算出比例,98%const double gc_cost_limit = GCTimeLimit/100.0;// 如果是fullgcif (is_full_gc) {// 如果GC時長超過98%// 并且回收后可用空間小于總空間的2%if (gc_cost() > gc_cost_limit &&free_in_old_gen < (size_t) mem_free_old_limit &&free_in_eden < (size_t) mem_free_eden_limit) {inc_gc_overhead_limit_count(); // 自增一次// 如果開啟了次數(shù)限制if (UseGCOverheadLimit) {// 如果次數(shù)大于等于5次。if (gc_overhead_limit_count() >=AdaptiveSizePolicyGCTimeLimitThreshold){// 設(shè)置為true,到時候就會拋出GC overhead limit exceeded異常// 并且清空次數(shù)set_gc_overhead_limit_exceeded(true); reset_gc_overhead_limit_count();} }}}// 如果設(shè)置了UseGCOverheadLimit的情況下, 不會響應(yīng)此異常if (UseGCOverheadLimit && PrintGCDetails && Verbose) {if (gc_overhead_limit_exceeded()) {reset_gc_overhead_limit_count();} }
}
這里算出了堆總空間的百分之2,gc回收時間的百分之98。然后算出了GC后空閑空間的占比,GC的回收時間。最后通過比較,如果GC回收時間大于98%,并且回收后可用空間小于總空間的2% 情況下計(jì)數(shù)器+1,如果計(jì)數(shù)器達(dá)到5次就通過set_gc_overhead_limit_exceeded方法設(shè)置為true,最終拋出java.lang.OutOfMemoryError: GC overhead limit exceeded異常。
所以看了源碼后,解決GC overhead limit exceeded異常,還可以通過設(shè)置
-XX:-UseGCOverheadLimit 或者 -XX:AdaptiveSizePolicyGCTimeLimitThreshold = "設(shè)置很大的數(shù)值" 都能讓JVM不拋出 GC overhead limit exceeded異常。但是沒任何意義,因?yàn)闀恢庇|發(fā)GC,一直STW暫停,用戶一直是卡死的狀態(tài)~