[Inside HotSpot] Java分代堆
1. 宇宙初始化
JVM在啟動的時候會初始化各種結構,比如模板直譯器,類載入器,當然也包括這篇文章的主題,Java堆。在hotspot原始碼結構中gc/shared
表示所有GC共同擁有的資訊,gc/g1
,gc/cms
則是不同實現需要用到的特設資訊。
λ tree
├─gc
│ ├─cms
│ ├─epsilon
│ ├─g1
│ ├─parallel
│ ├─serial
│ ├─shared
│ └─z
比如所有的Java堆都繼承自CollectedHeap,這個結構就位於gc/shared
,然後Serial GC需要的特設資訊位於gc/serial
,關於這點我們後面馬上會提到。另外Java堆的型別很多,本文所述Java堆均為分代堆(Generational Heap),它廣泛用於Serial GC,CMS GC:
關於什麼是分代堆應該不用多說,新生代老年代堆模型都是融入每個Javer靈魂的東西。
在討論分代堆之前,我們先從頭說起。Java堆初始化會經過一個呼叫鏈:
JNI_CreateJavaVM(prims/jni.cpp)
->JNI_CreateJavaVM_inner
->Threads::create_vm(runtime/thread.cpp)
->init_globals(runtime/init.cpp)
->universe_init(memory/universe.cpp)
->Universe::initialize_heap()
Universe模組(宇宙模組?hh)會負責高層次的Java堆的建立與初始化:
jint Universe::initialize_heap() {
// 建立Java堆
_collectedHeap = create_heap();
// 初始化Java堆
jint status = _collectedHeap->initialize();
if (status != JNI_OK) {
return status;
}
// 使用的GC,如[38.500s][info][gc] Using G1
log_info(gc)("Using %s", _collectedHeap->name());
ThreadLocalAllocBuffer::set_max_size(Universe::heap()->max_tlab_size());
if (UseCompressedOops) {
if ((uint64_t)Universe::heap()->reserved_region().end() > UnscaledOopHeapMax) {
Universe::set_narrow_oop_shift(LogMinObjAlignmentInBytes);
}
if ((uint64_t)Universe::heap()->reserved_region().end() <= OopEncodingHeapMax) {
Universe::set_narrow_oop_base(0);
}
AOTLoader::set_narrow_oop_shift();
Universe::set_narrow_ptrs_base(Universe::narrow_oop_base());
LogTarget(Info, gc, heap, coops) lt;
if (lt.is_enabled()) {
ResourceMark rm;
LogStream ls(lt);
Universe::print_compressed_oops_mode(&ls);
}
Arguments::PropertyList_add(new SystemProperty("java.vm.compressedOopsMode",
narrow_oop_mode_to_string(narrow_oop_mode()),
false));
}
// TLAB初始化
if (UseTLAB) {
assert(Universe::heap()->supports_tlab_allocation(),
"Should support thread-local allocation buffers");
ThreadLocalAllocBuffer::startup_initialization();
}
return JNI_OK;
}
2. 建立Java堆
在JVM初始化宇宙模組時會呼叫create_heap()建立堆,這個函式會進一步呼叫位於memory/allocation
模組的AllocateHeap,但是這些APIs實際還沒有做分配動作,它們只是包裝底層分配,處理一下分配失敗,真正的記憶體分配是位於底層runtime/os
模組:
說到runtime/os
是底層記憶體分配,那它到底有多底層?開啟原始碼看看,並沒有像OS這個名字一樣使用作業系統的VirtualAlloc,sbrk,而是使用C/C++語言執行時的malloc()/free()
進行分配/釋放的。
3. 初始化Java堆
在第一步中create_heap()建立了堆這個資料結構,但是裡面的成員都是無效的,而第二步就是負責初始化這些成員。初始化分為initialize()和post_initialize()。
// hotspot\share\gc\shared\genCollectedHeap.cpp
jint GenCollectedHeap::initialize() {
char* heap_address;
ReservedSpace heap_rs;
// 獲取堆對齊
size_t heap_alignment = collector_policy()->heap_alignment();
// 給堆分配空間
heap_address = allocate(heap_alignment, &heap_rs);
// 如果分配失敗則關閉虛擬機器
if (!heap_rs.is_reserved()) {
vm_shutdown_during_initialization(
"Could not reserve enough space for object heap");
return JNI_ENOMEM;
}
// 根據剛剛獲得的堆空間來初始化
// CollectedHeap中的_reserved欄位
initialize_reserved_region((HeapWord*)heap_rs.base(), (HeapWord*)(heap_rs.base() + heap_rs.size()));
// 建立並初始化remembered set
_rem_set = create_rem_set(reserved_region());
_rem_set->initialize();
CardTableBarrierSet *bs = new CardTableBarrierSet(_rem_set);
bs->initialize();
BarrierSet::set_barrier_set(bs);
// 根據剛剛獲得的堆來初始化GenCollectedHeap的新生代
ReservedSpace young_rs = heap_rs.first_part(_young_gen_spec->max_size(), false, false);
_young_gen = _young_gen_spec->init(young_rs, rem_set());
heap_rs = heap_rs.last_part(_young_gen_spec->max_size());
// 根據剛剛獲得的堆來初始化GenCollectedHeap的老年代
ReservedSpace old_rs = heap_rs.first_part(_old_gen_spec->max_size(), false, false);
_old_gen = _old_gen_spec->init(old_rs, rem_set());
clear_incremental_collection_failed();
return JNI_OK;
}
initialize()初始化新生代老年代,完成基礎的分代;然後post_initialize()將新生代細分為Eden和Survivor,然後再初始化標記清楚演算法用到的一些資料結構。至此JVM的分代堆就可以為垃圾回收器所用了。
4. JVM分代堆詳細結構
4.1 CollectedHeap
前面我們提到JVM是如何建立一個堆的,這一節將詳細分析這個堆長什麼樣子。JVM有很多垃圾回收器,每個垃圾回收器處理的堆結構都是不一樣的,比如G1GC處理的堆是由Region組成,CMS處理由老年代新生代組成的分代堆。這些不同的堆型別都繼承自gc/share/CollectedHeap
,抽象基類CollectedHeap表示所有堆都擁有的一些屬性:
// hotspot\share\gc\shared\collectedHeap.hpp
class CollectedHeap : public CHeapObj<mtInternal> {
private:
GCHeapLog* _gc_heap_log; // GC日誌
MemRegion _reserved; // 堆記憶體表示
protected:
bool _is_gc_active; // 是否正在GC
unsigned int _total_collections; // Minor GC次數
unsigned int _total_full_collections; // Full GC次數
GCCause::Cause _gc_cause; // 當前引發GC的原因
GCCause::Cause _gc_lastcause; // 上次引發GC的原因
...
};
上面的**_reserved**就表示Java堆這片連續的地址,它包含堆的起始地址和大小,即[start,start+size]
。然而這樣的堆是不能滿足GC需求的,Full GC處理老年代,Minor GC處理新生代,可是這兩個“代”都沒有在CollectedHeap中體現。翻翻上圖繼承模型,GenCollectedHeap才是分代堆。
4.2 GenCollectedHeap
//hotspot\share\gc\shared\genCollectedHeap.hpp
class GenCollectedHeap : public CollectedHeap {
public:
enum GenerationType {
YoungGen,
OldGen
};
protected:
Generation* _young_gen; // 新生代
Generation* _old_gen; // 老年代
...
};
看到GenCollectedHeap裡面的_young_gen
和_old_gen
基本就穩了。它繼承自CollectedHeap,其中CollectedHeap裡面的_reserved表示整個堆,GenCollectedHeap的新生代和老年代進一步劃分_reserved。這個劃分工作發生在堆初始化中。不同GC使用的新生代老年代也是不同的,所以不能一概而論,hotspot為此建立了如下分代模型:
- 分代基類:公有結構,儲存上次GC耗時,該代的記憶體起始地址,GC效能計數
- 預設新生代:一種包含Eden,From survivor, To survivor的分代
- 並行新生代:可並行GC的預設新生代
- 卡表代:包含卡表(CardTable)的分代
- 久任代:可Mark-Compact的卡表代
- 並行標記清楚代:可並行Mark-Sweep的卡表代
每個代都自己的特色,不同GC根據不同需要可以"自由"組合,比如Serial GC就使用的是DefNewGeneration
+ TenuredGeneration
的組合,CMS使用ParNewGeneration
+ ConcurrentMarkSweepGeneration
的組合。
4.3 SerialHeap
最後一步,Serial GC專用的堆繼承自GenCollectedHeap並在其上稍作封裝。這個SerialHeap最終將用於序列垃圾回收器(-XX:+UseSerialGC
)。
// hotspot\share\gc\serial\serialHeap.hpp
class SerialHeap : public GenCollectedHeap {
static SerialHeap* heap();
virtual Name kind() const {
return CollectedHeap::Serial;
}
virtual const char* name() const {
return "Serial";
}
...
};
5. 分代堆中的卡表代
關於GenCollectedHeap的各種代還有很多內容,我們關注DefNewGeneration
+ TenuredGeneration
的組合,它被用於SerialGC。
久任代繼承自卡表代,所謂卡表代是指用卡(Card)劃分整個老年代。我們知道,標記清除需要遍歷整個老年代來找出指向新生代的指標,至於為什麼要做這個遍歷看兩副圖即可明白。假設有堆中已經存在這樣的引用關係:
現在加入分配了新的物件:
其中老年代的物件存在指向新生代的指標,但是GC Root並沒有,如果這時候只從GC Root出發標記物件,就會錯過紅線指向的物件,繼而導致被誤做垃圾而清除,所以必須遍歷老年代找到指向新生代的物件。但是問題是老年代一般都很大,這樣的遍歷是比較慢的。卡表應此而生,它將老年代劃分為512位元組的卡(Card),這些卡組成卡表(Card table),卡表具體來說是一個位元組陣列。如果卡表中某一個位元組不為dirty,則表示對應的512位元組的區域不存在指向新生代的引用,那麼就可以直接跳過該區域,減少了遍歷時間:
在上圖中只有卡1和卡5存在指向新生代的指標,對整個老年代的遍歷就縮小到只對卡1、卡5的遍歷了。