由于jvm中垃圾收集器的存在,使得java程式員在開發過程中可以不用關心對象建立時的記憶體配置設定以及釋放過程,當記憶體不足時,jvm會自動開啟垃圾收集線程,進行垃圾對象的回收。
那麼垃圾回收線程到底是什麼時候觸發,并如何實作垃圾回收的呢?本文将對openjdk的源碼進行分析,并通過代碼分析java垃圾回收的過程。
vmthread主要負責排程執行虛拟機内部的vm線程操作,如gc操作等,在jvm執行個體建立時進行初始化。
vmthread::create()方法負責該線程的建立。
在create方法裡主要執行兩個事情:
vmthread内部維護了一個vmoperationqueue類型的隊列,用于儲存内部送出的vm線程操作vm_operation,在vmthread建立時會對該隊列進行初始化。
由于vmthread本身就是一個線程,啟動後通過執行loop方法進行輪詢操作,從隊列中按照優先級取出目前需要執行的vm_operation對象并執行。
其中整個現成的輪詢過程分為兩步:
第一步
如果隊列為空,_vm_queue->remove_next()方法則傳回空的_cur_vm_operation,否則根據隊列中的vm_operation優先級進行重新排序,并傳回隊列頭部的vm_operation。如果_cur_vm_operation為空,則執行如下邏輯:
通過執行vmoperationqueue_lock->wait方法等待vm operation。
第二步
如果目前vm_operation需要在安全點執行,如full gc,則執行上述邏輯,否則執行以下邏輯:
通過evaluate_operation執行目前的_cur_vm_operation,最終調用vm_operation對象的evaluate方法。
子類通過重寫vm_operation類的doit方法實作具體的邏輯。
在java的記憶體配置設定機制中,當新生代不足以配置設定對象所需的記憶體時,會觸發一次ygc,具體實作如下:
上面這段代碼的意思是建立一個vm_gencollectforallocation類型的vm_operation,通過執行vmthread::execute方法儲存到vmthread的隊列中,其中execute的核心實作如下:
ygc的vm_operation加入到隊列後,通過執行vmoperationqueue_lock的notify方法喚醒vmthread線程,等待被執行,其中vm_gencollectforallocation的doit方法實作:
通過vmthread排程執行gc操作,最終調用對應的doit方法:
1、利用svcgcmarker通知minor gc操作的開始;
2、設定觸發gc的原因為gccause::_allocation_failure,即記憶體配置設定失敗;
3、其中gencollectedheap的satisfy_failed_allocation方法會調用gc政策的satisfy_failed_allocation方法,處理記憶體配置設定失敗的情況;
satisfy_failed_allocation
如果其它線程觸發了gc操作,則通過擴充記憶體代的容量進行配置設定,最後不管有沒有配置設定成功都傳回,等待其它線程的gc操作結束;
如果增量式gcincremental collection可行,則通過do_collection方法執行一次minor gc,即回收新生代的垃圾。
如果增量式gc不可行,則通過do_collection方法執行一次full gc。
gc結束之後,再次從記憶體堆的各個記憶體代中依次配置設定指定大小的記憶體塊,如果配置設定成功則傳回,否則繼續。
如果gc結束後還是配置設定失敗,說明gc失敗了,則再次嘗試通過允許擴充記憶體代容量的方式來試圖配置設定指定大小的記憶體塊。
如果執行到這一步,說明gc之後還是記憶體不足,則通過do_collection方法最後再進行一次徹底的gc,回收所有的記憶體代,對堆記憶體進行壓縮,且清除軟引用。
經過一次徹底的gc之後,最後一次嘗試依次從各記憶體代配置設定指定大小的記憶體塊。
注:從上述分析中可以發現,gc操作的入口都位于gencollectedheap::do_collection方法中,不同的參數執行不同類型的gc。
執行gc操作必須滿足四個條件:
1、在一個同步安全點,vmthread在調用gc操作時會通過safepointsynchronize::begin/end方法實作進出安全區域,調用begin方法時會強制所有線程到達一個安全點;
2、目前線程是vm線程或并發的gc線程;
3、目前線程已經獲得記憶體堆的全局鎖;
4、記憶體堆目前_is_gc_active參數為false,即還未開始gc;
如果目前有其它線程觸發了gc,則終止目前的gc線程,否則繼續。
根據參數do_clear_all_soft_refs和gc政策判斷本次gc是否需要清除軟引用;記錄目前永久代的使用量perm_prev_used;如果啟動參數中設定了-xx:+printheapatgc,則列印gc發生時記憶體堆的資訊。
1、設定參數_is_gc_active為真,表示目前線程正式開始gc操作;
2、判斷目前是否要進行一次full gc,并确定觸發full gc的原因,如通過調用system.gc()觸發;
3、如果設定了printgc和printgcdatestamps,則在輸出日志中添加時間戳;
4、如果設定了printgcdetails,則列印本次gc的詳細cpu耗時,如 user_time、system_time和real_time;
5、gc_prologue方法在gc開始前做一些前置處理,如設定每個記憶體代的_soft_end字段;
6、更新發生gc的次數_total_collections,如果目前gc是full gc,則還需更新發生full gc的次數_total_full_collections;
擷取目前記憶體堆的使用量gch_prev_used;初始化開始回收的記憶體代序号starting_level,預設為0,即從最年輕的記憶體代開始;如果目前gc是full gc,則從最老的記憶體代開始向前搜尋,找到第一個可收集所有新生代的記憶體代,稍後從該記憶體代開始回收;
從序号為starting_level的記憶體代開始回收;如果目前記憶體代不需要進行回收,則處理下一個記憶體代,否則對目前記憶體進行回收;如果目前記憶體代所有記憶體代中最老的,則将本次的gc過程更新為full gc,更新full gc的次數,并執行full gc的前置處理。
1、如果設定了參數heapdumpbeforefullgc,則對記憶體堆進行dump;
2、如果設定了參數printclasshistogrambeforefullgc,則列印在進行fgc之前的對象;
1、統計各個記憶體代進行gc時的資料;
2、如果開啟了zapunusedheaparea,則在回收每個記憶體代時都要對記憶體代的記憶體上限位址top進行更新;
到這一步才開始真正的gc操作:設定目前記憶體代的_saved_mark值,即設定這些記憶體區域塊的上限位址;通過每個記憶體代管理器的collect方法對垃圾對象的進行回收,垃圾收集算法的具體細節會在後文進行分析;
1、如果目前是fgc,則調用post_full_gc_dump方法通知gc已經完成,可以進行後續操作,如果設定了參數heapdumpafterfullgc,則在gc後可以對堆記憶體進行dump;如果設定了參數printclasshistogramafterfullgc,則在gc後可以列印存活的對象;
2、如果設定了參數printgcdetails,則在gc後可以列印記憶體堆的變化情況;如果目前還是fgc,則還可以列印永久代的記憶體變化情況。
gc完成後,調整記憶體堆中各記憶體代的大小;如果是fgc,則還需要調整永久代大小;擷取fullgccount_lock鎖,對_full_collections_completed進行更新,并通過鎖機制通知本次fgc已經完成;
列印記憶體堆的gc總次數和fgc次數;exitaftergcnum預設是0,如果設定exitaftergcnum大于0,且gc的總次數超過exitaftergcnum,則終止整個jvm程序。到此java jvm垃圾回收程序就終止gc程序。