[Inside HotSpot] Java分代堆

kelthuzadx發表於2019-05-25

[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:

[Inside HotSpot] Java分代堆

關於什麼是分代堆應該不用多說,新生代老年代堆模型都是融入每個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模組:

[Inside HotSpot] Java分代堆

說到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表示所有堆都擁有的一些屬性:

[Inside HotSpot] Java分代堆

// 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為此建立了如下分代模型:

[Inside HotSpot] Java分代堆

  • 分代基類:公有結構,儲存上次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)劃分整個老年代。我們知道,標記清除需要遍歷整個老年代來找出指向新生代的指標,至於為什麼要做這個遍歷看兩副圖即可明白。假設有堆中已經存在這樣的引用關係:

[Inside HotSpot] Java分代堆

現在加入分配了新的物件:

[Inside HotSpot] Java分代堆

其中老年代的物件存在指向新生代的指標,但是GC Root並沒有,如果這時候只從GC Root出發標記物件,就會錯過紅線指向的物件,繼而導致被誤做垃圾而清除,所以必須遍歷老年代找到指向新生代的物件。但是問題是老年代一般都很大,這樣的遍歷是比較慢的。卡表應此而生,它將老年代劃分為512位元組的卡(Card),這些卡組成卡表(Card table),卡表具體來說是一個位元組陣列。如果卡表中某一個位元組不為dirty,則表示對應的512位元組的區域不存在指向新生代的引用,那麼就可以直接跳過該區域,減少了遍歷時間:

[Inside HotSpot] Java分代堆

在上圖中只有卡1和卡5存在指向新生代的指標,對整個老年代的遍歷就縮小到只對卡1、卡5的遍歷了。

相關文章