0. Serial垃圾回收器Full GC
Serial垃圾回收器的Full GC使用標記-壓縮(Mark-Compact)進行垃圾回收,該演算法基於Donald E. Knuth提出的Lisp2演算法,它會把所有存活物件滑動到空間的一端,所以也叫sliding compact。Full GC始於gc/serial/tenuredGeneration
的TenuredGeneration::collect,它會在GC前後記錄一些日誌,真正的標記壓縮演算法發生在GenMarkSweep::invoke_at_safepoint,我們可以使用-Xlog:gc*
得到該演算法的流程:
Heap address: 0x00000000f9c00000, size: 100 MB, Compressed Oops mode: 32-bit
GC(0) Pause Young (Allocation Failure)
GC(1) Pause Full (Allocation Failure)
GC(1) Phase 1: Mark live objects
GC(1) Phase 1: Mark live objects 1.136ms
GC(1) Phase 2: Compute new object addresses
GC(1) Phase 2: Compute new object addresses 0.170ms
GC(1) Phase 3: Adjust pointers
GC(1) Phase 3: Adjust pointers 0.435ms
GC(1) Phase 4: Move objects
GC(1) Phase 4: Move objects 0.208ms
GC(1) Pause Full (Allocation Failure) 40M->0M(96M) 2.102ms
GC(0) DefNew: 2621K->0K(36864K)
GC(0) Tenured: 40960K->795K(61440K)
GC(0) Metaspace: 562K->562K(1056768K)
GC(0) Pause Young (Allocation Failure) 42M->0M(96M) 3.711ms
GC(0) User=0.00s Sys=0.00s Real=0.00s
標記-壓縮分為四個階段(如果是fastdebug版jvm,可以使用-Xlog:gc*=trace
得到更為詳細的日誌,不過可能詳細過頭了...),這篇文章將圍繞四個階段展開。
1. 階段1:標記存活物件
第一階段對應GC日誌的
GC(1) Phase 1: Mark live objects
JVM在process_string_table_roots()
和process_roots()
中會遍歷所有型別的GC Root,然後使用XX::oops_do(root_closure)
從該GC Root出發標記所有存活物件。XX
表示GC Root型別,root_closure
表示標記存活物件的方法(閉包)。GC模組有很多閉包(closure),它們代表的是一段程式碼、一種行為。root_closure就是一個MarkSweep::FollowRootClosure
閉包。這個閉包很強大,給它一個物件,就能標記這個物件,迭代標記物件的成員,以及物件所在的棧的所有物件及其成員:
// hotspot\share\gc\serial\markSweep.cpp
void MarkSweep::FollowRootClosure::do_oop(oop* p) { follow_root(p); }
template <class T> inline void MarkSweep::follow_root(T* p) {
// 如果引用指向的物件不為空且未標記
T heap_oop = RawAccess<>::oop_load(p);
if (!CompressedOops::is_null(heap_oop)) {
oop obj = CompressedOops::decode_not_null(heap_oop);
if (!obj->mark_raw()->is_marked()) {
mark_object(obj); // 標記物件
follow_object(obj); // 標記物件的成員
}
}
follow_stack(); // 標記引用所在棧
}
// 如果物件是陣列物件則標記陣列,否則標記物件的成員
inline void MarkSweep::follow_object(oop obj) {
if (obj->is_objArray()) {
MarkSweep::follow_array((objArrayOop)obj);
} else {
obj->oop_iterate(&mark_and_push_closure);
}
}
// 標記引用所在的整個棧
void MarkSweep::follow_stack() {
do {
// 如果待標記棧不為空則逐個標記
while (!_marking_stack.is_empty()) {
oop obj = _marking_stack.pop();
follow_object(obj);
}
// 如果物件陣列棧不為空則逐個標記
if (!_objarray_stack.is_empty()) {
ObjArrayTask task = _objarray_stack.pop();
follow_array_chunk(objArrayOop(task.obj()), task.index());
}
} while (!_marking_stack.is_empty() || !_objarray_stack.is_empty());
}
// 標記陣列的型別的Class和陣列成員,比如String[] p = new String[2]
// 對p標記會同時標記java.lang.Class,p[1],p[2]
inline void MarkSweep::follow_array(objArrayOop array) {
MarkSweep::follow_klass(array->klass());
if (array->length() > 0) {
MarkSweep::push_objarray(array, 0);
}
}
既然走到這裡了不如看看JVM是如何標記物件的:
inline void MarkSweep::mark_object(oop obj) {
// 獲取物件的mark word
markOop mark = obj->mark_raw();
// 設定gc標記
obj->set_mark_raw(markOopDesc::prototype()->set_marked());
// 垃圾回收器視情況保留物件的gc標誌
if (mark->must_be_preserved(obj)) {
preserve_mark(obj, mark);
}
}
物件的mark work有32bits或者64bits,取決於CPU架構和UseCompressedOops:
// hotspot\share\oops\markOop.hpp
32 位mark lword:
hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
size:32 ------------------------------------------>| (CMS free block)
PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
最後的lock2位有不同含義:
[ptr | 00] locked ptr指向棧上真正的物件頭
[header | 0 | 01] unlocked 普通物件頭
[ptr | 10] monitor 膨脹鎖
[ptr | 11] marked GC標記
原來垃圾回收的標記就是對每個物件mark word最後兩位置為11,可是如果最後兩位用於其他用途怎麼辦?比如這個物件的最後兩位表示膨脹鎖,那GC就不能對它進行標記了,所以垃圾回收器還需要視情況在額外區域保留物件的mark word(PreservedMark)。回到之前的話題,GC Root有很多,有的是我們耳熟能詳的,有的則是略微少見:
- 所有已載入的類(
ClassLoaderDataGraph::roots_cld_do
) - 所有Java執行緒當前棧幀的引用和虛擬機器內部執行緒(
Threads::possibly_parallel_oops_do
) - JVM內部使用的引用(
Universe::oopds_do
和SystemDictionary::oops_do
) - JNI handles(
JNIHandles::oops_do
) - 所有synchronized鎖住的物件引用(
ObjectSynchronizer::oops_do
) - java.lang.management物件(
Management::oops_do
) - JVMTI匯出(
JvmtiExport::oops_do
) - AOT程式碼的堆(
AOTLoader::oops_do
) - code cache(
CodeCache::blobs_do
) - String常量池(
StringTable::oops_do
)
它們都包含可進行標記的引用,會視情況進行單執行緒標記或者併發標記,JVM會使用CAS(Atomic::cmpxchg)自旋鎖等待標記任務。如果任務全部完成,即標記執行緒和完成計數相等,就結束阻塞。
當物件標記完成後jvm還會使用ref_processor()->process_discovered_references()
對弱引用,軟引用,虛引用,final引用根據他們的Java語義做特殊處理,不過與演算法本身沒有太大關係,有興趣的請自行了解。
2. 階段2:計算物件新地址
就算物件新地址的思想是:從地址空間開始掃描,如果cur_obj指標指向已經GC標記過的物件,則將該物件的新地址設定為compact_top,然後compact_top推進,cur_obj推進,直至cur_obj到達地址空間結束。
計算新地址虛擬碼如下:
// 掃描堆空間
while(cur_obj<space_end){
if(cur_obj->is_gc_marked()){
// 如果cur_Obj當前指向已標記過的物件,就計算新的地址
int object_size += cur_obj->size();
cur_obj->new_address = compact_top;
compact_top = cur_obj;
cur_obj += object_size;
}else{
// 否則快速跳過未標記的連續空間
while(cur_obj<space_end &&!cur_obj->is_gc_marked()){
cur_obj += cur_obj->size();
}
}
}
有了上面的認識,對應到HotSpot實現也比較簡單了。計算物件新地址的程式碼位於CompactibleSpace::scan_and_forward:
// hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_forward(SpaceType* space, CompactPoint* cp) {
space->set_compaction_top(space->bottom());
if (cp->space == NULL) {
cp->space = cp->gen->first_compaction_space();
cp->threshold = cp->space->initialize_threshold();
cp->space->set_compaction_top(cp->space->bottom());
}
// compact_top為物件新地址的起始
HeapWord* compact_top = cp->space->compaction_top();
DeadSpacer dead_spacer(space);
//最後一個標記物件
HeapWord* end_of_live = space->bottom();
// 第一個未標記物件
HeapWord* first_dead = NULL;
const intx interval = PrefetchScanIntervalInBytes;
// 掃描指標
HeapWord* cur_obj = space->bottom();
// 掃描終點
HeapWord* scan_limit = space->scan_limit();
// 掃描老年代
while (cur_obj < scan_limit) {
// 如果cur_obj指向已標記物件
if (space->scanned_block_is_obj(cur_obj) && oop(cur_obj)->is_gc_marked()) {
Prefetch::write(cur_obj, interval);
size_t size = space->scanned_block_size(cur_obj);
// 給cur_obj指向的物件設定新地址,並前移compact_top
compact_top = cp->space->forward(oop(cur_obj), size, cp, compact_top);
// cur_obj指標前移
cur_obj += size;
// 修改最後存活物件指標地址
end_of_live = cur_obj;
} else {
// 如果cur_obj指向未標記物件,則獲取這片(可能連續包含未標記物件的)空間的大小
HeapWord* end = cur_obj;
do {
Prefetch::write(end, interval);
end += space->scanned_block_size(end);
} while (end < scan_limit && (!space->scanned_block_is_obj(end) || !oop(end)->is_gc_marked()));
// 如果需要減少物件移動頻率
if (cur_obj == compact_top && dead_spacer.insert_deadspace(cur_obj, end)) {
oop obj = oop(cur_obj);
compact_top = cp->space->forward(obj, obj->size(), cp, compact_top);
end_of_live = end;
} else {
// 否則跳過未存活物件
*(HeapWord**)cur_obj = end;
// 如果first_dead為空則將這片空間設定為第一個未存活物件
if (first_dead == NULL) {
first_dead = cur_obj;
}
}
// cur_obj指標快速前移
cur_obj = end;
}
}
if (first_dead != NULL) {
space->_first_dead = first_dead;
} else {
space->_first_dead = end_of_live;
}
cp->space->set_compaction_top(compact_top);
}
如果物件需要移動,cp->space->forward()
會將新地址放入物件的mark word裡面。計算物件新地址裡面有個小技巧,比如當掃描到一片未存活物件的時候,它把第一個未存活物件設定為該片區域的結尾,這樣下一次掃描到第一個物件可以直接跳到區域尾,節約時間。
3. 階段3:調整物件指標
第二階段設定了所有物件的新地址,但是沒有改變物件的相對地址和GC Root。比如GC Root指向物件A,B,C,這時候A、B、C都有新地址A',B',C',GC Root應該相應調整為指向A',B',C':
第三階段就是幹這件事的。還記得第一階段GC Root的標記行為嗎?
JVM在
process_string_table_roots()
和process_roots()
中會遍歷所有型別的GC Root,然後使用XX::oops_do(root_closure)
從該GC Root出發標記所有存活物件。XX
表示GC Root型別,root_closure
表示標記存活物件的方法(閉包)。
第三階段和第一階段一樣,只是第一階段傳遞的root_closure表示標記存活物件的閉包(FollowRootClosure
),第三階段傳遞的root_closure表示調整物件指標的閉包AdjustPointerClosure
:
// hotspot\share\gc\serial\markSweep.inline.hpp
inline void AdjustPointerClosure::do_oop(oop* p) { do_oop_work(p); }
template <typename T>
void AdjustPointerClosure::do_oop_work(T* p) { MarkSweep::adjust_pointer(p); }
template <class T> inline void MarkSweep::adjust_pointer(T* p) {
T heap_oop = RawAccess<>::oop_load(p);
if (!CompressedOops::is_null(heap_oop)) {
// 從地址p處得到物件
oop obj = CompressedOops::decode_not_null(heap_oop);
// 從物件mark word中得到新物件地址
oop new_obj = oop(obj->mark_raw()->decode_pointer());
if (new_obj != NULL) {
// 將地址p處設定為新物件地址
RawAccess<IS_NOT_NULL>::oop_store(p, new_obj);
}
}
}
AdjustPointerClosure
閉包會遍歷所有GC Root然後調整物件指標,注意,這裡和第一階段有個重要不同是第一階段傳遞的FollowRootClosure
閉包會從GC Root出發標記所有可達物件,但是AdjustPointerClosure
閉包只會標記GC Root出發直接可達的物件,
從物件出發尋找可達其他物件這一步是使用的另一個閉包GenAdjustPointersClosure
,它會呼叫CompactibleSpace::scan_and_adjust_pointers遍歷整個堆空間然後調整存活物件的指標:
//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_adjust_pointers(SpaceType* space) {
// 掃描指標
HeapWord* cur_obj = space->bottom();
// 最後一個標記物件
HeapWord* const end_of_live = space->_end_of_live;
// 第一個未標記物件
HeapWord* const first_dead = space->_first_dead;
const intx interval = PrefetchScanIntervalInBytes;
// 掃描老年代
while (cur_obj < end_of_live) {
Prefetch::write(cur_obj, interval);
// 如果掃描指標指向的物件是存活物件
if (cur_obj < first_dead || oop(cur_obj)->is_gc_marked()) {
// 調整該物件指標,調整方法和AdjustPointerClosure所用一樣
size_t size = MarkSweep::adjust_pointers(oop(cur_obj));
size = space->adjust_obj_size(size);
// 指標前移
cur_obj += size;
} else {
// 否則掃描指標指向未存活物件,設定掃描指標為下一個存活物件,加速前移
cur_obj = *(HeapWord**)cur_obj;
}
}
}
4. 階段4:移動物件
第四階段傳遞GenCompactClosure
閉包,該閉包負責物件的移動:
移動的程式碼位於CompactibleSpace::scan_and_compact:
//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_compact(SpaceType* space) {
verify_up_to_first_dead(space);
// 老年代起始位置
HeapWord* const bottom = space->bottom();
// 最後一個標記物件
HeapWord* const end_of_live = space->_end_of_live;
// 如果該區域所有物件都存活,或者沒有任何物件,或者沒有任何存活物件
// 就不需要進行移動
if (space->_first_dead == end_of_live && (bottom == end_of_live || !oop(bottom)->is_gc_marked())) {
clear_empty_region(space);
return;
}
const intx scan_interval = PrefetchScanIntervalInBytes;
const intx copy_interval = PrefetchCopyIntervalInBytes;
// 設定掃描指標cur_obj為空間底部
HeapWord* cur_obj = bottom;
// 跳到第一個存活的物件
if (space->_first_dead > cur_obj && !oop(cur_obj)->is_gc_marked()) {
cur_obj = *(HeapWord**)(space->_first_dead);
}
// 從空間開始到最後一個存活物件為截止進行掃描
while (cur_obj < end_of_live) {
// 如果cur_obj執行的物件未標記
if (!oop(cur_obj)->is_gc_marked()) {
// 掃描指標快速移動至下一個存活的物件(死物件的第一個word
// 存放了下一個存活物件的地址,這樣就可以快速移動)
cur_obj = *(HeapWord**)cur_obj;
} else {
Prefetch::read(cur_obj, scan_interval);
size_t size = space->obj_size(cur_obj);
// 獲取物件將要被移動到的新地址
HeapWord* compaction_top = (HeapWord*)oop(cur_obj)->forwardee();
Prefetch::write(compaction_top, copy_interval);
// 移動物件,並初始化物件的mark word
Copy::aligned_conjoint_words(cur_obj, compaction_top, size);
oop(compaction_top)->init_mark_raw();
// 掃描指標前移
cur_obj += size;
}
}
clear_empty_region(space);
}