天天看點

JVM源碼分析之SystemGC完全解讀

jvm的gc一般情況下是jvm本身根據一定的條件觸發的,不過我們還是可以做一些人為的觸發,比如通過jvmti做強制gc,通過system.gc觸發,還可以通過jmap來觸發等,針對每個場景其實我們都可以寫篇文章來做一個介紹,本文重點介紹下system.gc的原理

或許大家已經知道如下相關的知識

system.gc其實是做一次full gc

system.gc會暫停整個程序

system.gc一般情況下我們要禁掉,使用-xx:+disableexplicitgc

system.gc在cms gc下我們通過-xx:+explicitgcinvokesconcurrent來做一次稍微高效點的gc(效果比full gc要好些)

system.gc最常見的場景是rmi/nio下的堆外記憶體配置設定等

如果你已經知道上面這些了其實也說明你對system.gc有過一定的了解,至少踩過一些坑,但是你是否更深層次地了解過它,比如

為什麼cms gc下-xx:+explicitgcinvokesconcurrent這個參數加了之後會比真正的full gc好?

它如何做到暫停整個程序?

堆外記憶體配置設定為什麼有時候要配合system.gc?

如果你上面這些疑惑也都知道,那說明你很懂system.gc了,那麼接下來的文字你可以不用看啦

先貼段代碼吧(java.lang.system)

發現主要調用的是runtime裡的gc方法(java.lang.runtime)

這裡看到gc方法是native的,在java層面隻能到此結束了,代碼隻有這麼多,要了解更多,可以看方法上面的注釋,不過我們需要更深層次地來了解其實作,那還是準備好進入到jvm裡去看看

上面提到了runtime.gc是一個本地方法,那需要先在jvm裡找到對應的實作,這裡稍微提一下jvm裡native方法最常見的也是最簡單的查找,jdk裡一般含有native方法的類,一般都會有一個對應的c檔案,比如上面的java.lang.runtime這個類,會有一個runtime.c的檔案和它對應,native方法的具體實作都在裡面了,如果你有source,可能會猜到和下面的方法對應

其實沒錯的,就是這個方法,jvm要查找到這個native方法其實很簡單的,看方法名可能也猜到規則了,java_pkgname_classname_methodname,其中pkgname裡的"."替換成"_",這樣就能找到了,當然規則不僅僅隻有這麼一個,還有其他的,這裡不細說了,有機會寫篇文章詳細介紹下其中細節

上面的方法裡是調用jvm_gc(),實作如下

看到這裡我們已經解釋其中一個疑惑了,就是<code>disableexplicitgc</code>這個參數是在哪裡生效的,起的什麼作用,如果這個參數設定為true的話,那麼将直接跳過下面的邏輯,我們通過-xx:+ disableexplicitgc就是将這個屬性設定為true,而這個屬性預設情況下是true還是false呢

這裡主要針對cmsgc下來做分析,是以我們上面看到調用了heap的collect方法,我們找到對應的邏輯

collect裡一開頭就有個判斷,如果should_do_concurrent_full_gc傳回true,那會執行collect_mostly_concurrent做并行的回收

其中should_do_concurrent_full_gc中的邏輯是如果使用cms gc,并且是system gc且explicitgcinvokesconcurrent==true,那就做并行full gc,當我們設定-xx:+ explicitgcinvokesconcurrent的時候,就意味着應該做并行full gc了,不過要注意千萬不要設定-xx:+disableexplicitgc,不然走不到這個邏輯裡來了

說到gc,這裡要先提到vmthread,在jvm裡有這麼一個線程不斷輪詢它的隊列,這個隊列裡主要是存一些vm_operation的動作,比如最常見的就是記憶體配置設定失敗要求做gc操作的請求等,在對gc這些操作執行的時候會先将其他業務線程都進入到安全點,也就是這些線程從此不再執行任何位元組碼指令,隻有當出了安全點的時候才讓他們繼續執行原來的指令,是以這其實就是我們說的stop the world(stw),整個程序相當于靜止了

這裡必須提到cms gc,因為這是解釋并行full gc和正常full gc的關鍵所在,cms gc我們分為兩種模式background和foreground,其中background顧名思義是在背景做的,也就是可以不影響正常的業務線程跑,觸發條件比如說old的記憶體占比超過多少的時候就可能觸發一次background式的cms gc,這個過程會經曆cms gc的所有階段,該暫停的暫停,該并行的并行,效率相對來說還比較高,畢竟有和業務線程并行的gc階段;而foreground則不然,它發生的場景比如業務線程請求配置設定記憶體,但是記憶體不夠了,于是可能觸發一次cms gc,這個過程就必須是要等記憶體配置設定到了線程才能繼續往下面走的,是以整個過程必須是stw的,是以cms gc整個過程都是暫停應用的,但是為了提高效率,它并不是每個階段都會走的,隻走其中一些階段,這些省下來的階段主要是并行階段,precleaning、abortablepreclean,resizing這幾個階段都不會經曆,其中sweep階段是同步的,但不管怎麼說如果走了類似foreground的cms gc,那麼整個過程業務線程都是不可用的,效率會影響挺大。cms gc具體的過程後面再寫文章詳細說,其過程确實非常複雜的

正常的full gc其實是整個gc過程包括ygc和cms gc(這裡說的是真正意義上的full gc,還有些場景雖然調用full gc的接口,但是并不會都做,有些時候隻做ygc,有些時候隻做cms gc)都是由vmthread來執行的,是以整個時間是ygc+cms gc的時間之和,其中cms gc是上面提到的foreground式的,是以整個過程會比較長,也是我們要避免的

并行full gc也通樣會做ygc和cms gc,但是效率高就搞在cms gc是走的background的,整個暫停的過程主要是ygc+cms_initmark+cms_remark幾個階段

這裡說的堆外記憶體主要針對java.nio.directbytebuffer,這些對象的建立過程會通過unsafe接口直接通過os::malloc來配置設定記憶體,然後将記憶體的起始位址和大小存到java.nio.directbytebuffer對象裡,這樣就可以直接操作這些記憶體。這些記憶體隻有在directbytebuffer回收掉之後才有機會被回收,是以如果這些對象大部分都移到了old,但是一直沒有觸發cms gc或者full gc,那麼悲劇将會發生,因為你的實體記憶體被他們耗盡了,是以為了避免這種悲劇的發生,通過-xx:maxdirectmemorysize來指定最大的堆外記憶體大小,當使用達到了門檻值的時候将調用system.gc來做一次full gc,以此來回收掉沒有被使用的堆外記憶體,具體堆外記憶體是如何回收的,其原理機制又是怎樣的,還是後面寫篇詳細的文章吧

該文章來自阿裡巴巴技術協會(ata)精選集

個人公衆号:

JVM源碼分析之SystemGC完全解讀