1. 程式人生 > 程式設計 >Java metaspace原始碼解析

Java metaspace原始碼解析

本文基於openjdk11及hotspot

從Java8開始,JVM中的永久代被替換為了metaspace,本文將根據JVM原始碼對metaspace的初始化、分配記憶體、釋放記憶體三個主要過程進行解析。

1. 資料結構

在metaspace中有如下一些概念,metaspace、classLoaderMetaspace、virtualSpace、metachunk、chunkManager、spaceManager、metablock。首先來看看各個資料結構中的內容,

1.1 metaspace

// hotspot/share/memory/metaspace.hpp
class Metaspace
:
public AllStatic { static metaspace::VirtualSpaceList* _space_list; static metaspace::VirtualSpaceList* _class_space_list; static metaspace::ChunkManager* _chunk_manager_metadata; static metaspace::ChunkManager* _chunk_manager_class; } 複製程式碼

Metaspace是一個只包含靜態屬性和靜態方法的類,看上去更像是一個工具類。在裡面重要包含了VirtualSpaceList和ChunkManager,不難看出VirtualSpaceList及ChunkManager是全域性共享的。

space_list和class_space_list的區別

這兩者分別對應了一片記憶體區域,從名稱中可以看出class_space_list是用來儲存java中class的資料的。但事實上,不完全正確,只有當壓縮指標生效的時候,class_space_list才會存在,否則class資料也同樣會儲存在space_list中。也就是說其實JVM的metaspace區域其實分為兩塊——Class區域和NonClass區域。

同理,chunk_manager_metadata對應了NonClass,chunk_manager_class對應了Class。

1.2 classLoaderMetaspace

在Java中,每個ClassLoader例項(包括bootstrapClassLoader)都會在metaspace中擁有一塊獨立的區域,叫做classLoaderMetaspace。classLoaderMetaspace的資料結構如下:

class ClassLoaderMetaspace : public CHeapObj<mtClass> {
  metaspace::SpaceManager* _vsm;
  metaspace::SpaceManager* _class_vsm;
}
複製程式碼

每個ClassLoaderMetaspace例項都會有一個spaceManager(可能還有一個classSpaceManager),用來處理ClassLoaderMetaspace的記憶體分配。

classLoaderMetaspace型別

classLoaderMetaspace有些多種型別,分別對應了不同的ClassLoader

名稱 對應ClassLoader
StandardMetaspace 普通ClassLoader
BootMetaspace BootstrapClassLoader
AnonymousMetaspace 匿名ClassLoader
ReflectionMetaspace 反射ClassLoader

不同型別的metaspace之間區別不大,主要在於他們建立的chunk大小的區別。

1.3 virtualSpace

virtualSpace組成了為metaspace分配的空間,以連結串列形式共享給ClassLoaderMetaspace使用。資料結構如下:

class VirtualSpace {
  // Reserved area
  char* _low_boundary;
  char* _high_boundary;

  // Committed area
  char* _low;
  char* _high;
  
  // MPSS Support
  char* _lower_high;
  char* _middle_high;
  char* _upper_high;

  char* _lower_high_boundary;
  char* _middle_high_boundary;
  char* _upper_high_boundary;
}
複製程式碼

在virtualSpace中劃分為了上中下三個區域,如下圖所示

-----------------  upper_high_boundary / high_boundary
| unused |      |
|--------|  上  |- upper_high
|  used  |      |
-----------------  middle_high_boundary
| unused |      |
|--------|  中  |- middle_high
|  used  |      |
-----------------  lower_high_boundary
| unused |      |
|--------|  下  |- lower_high
|  used  |      |
-----------------  low_boundary
複製程式碼

這三塊區域的區別,本文不予細究。

1.4 metachunk

metachunk是ClassLoaderMetaspace從VirtualSpace區域分配出來的記憶體,每個ClassLoaderMetaspace都會通過spaceManager持有一個metachunk列表,表明它所有持有的metaspace記憶體,同樣的該classLoader的所有記憶體申請也全部是在chunk中進行。

在JVM中chunk從小到大分為了四種型別,以及其對應的chunk大小如下表,

chunk型別 Class(單位:字) NonClass(單位:字)
specialized 128 128
small 256 512
medium 4K 8K
humongous 無固定大小 無固定大小

1.5 chunkManager

chunkManager用來那些已經釋放了的chunk,用以重複使用,資料結構如下:

class ChunkManager : public CHeapObj<mtInternal> {
  ChunkList _free_chunks[NumberOfFreeLists];
  ChunkTreeDictionary _humongous_dictionary;
}
複製程式碼

其中free_chunks[]用來儲存special、small、medium三種型別的chunk,而humongous_dictionary用來儲存humongous型別的chunk。前面三種是固定大小,因此直接使用陣列儲存,而humongous是無固定大小的,因此使用排序二叉樹的形式儲存。

1.6 spaceManager

每個classLoaderMetaspace都對應一個NonClassSpaceManager和一個ClassSpaceManager,SpaceManager中儲存了當前classLoaderMetaspace所使用的chunk的資訊以及釋放後用於重新使用的metablock列表。同時classLoaderMetaspace的記憶體分配最終也是由spaceManager來處理的。主要資料結構如下:

class SpaceManager : public CHeapObj<mtClass> {
  Metachunk* _chunk_list;
  Metachunk* _current_chunk;
  BlockFreelist* _block_freelists;
}
複製程式碼

1.7 metablock

metablock則是由metachunk中分配出來用於最終使用的記憶體。在spaceManager的BlockFreeList中儲存了那些釋放後可再次使用的block。

1.8 總圖

image-20190809164533218

2. 初始化過程

JVM metaspace初始化分為了metaspace和classLoaderMetaspace的初始化。我們依次來看這兩者的初始化,

2.1 metaspace初始化

metaspace的初始化分為三步,先是Arguments::apply_ergo()時呼叫Metaspace::ergo_initialize(),接著在universe_init()時呼叫Metaspace::global_initialize(),最後呼叫Metaspace::post_initialize()。這三步都是在JVM初始化的過程中執行。我們依次來看這三步初始化過程,

ergo_initialize

void Metaspace::ergo_initialize() {
  if (DumpSharedSpaces) {
    FLAG_SET_ERGO(bool,UseLargePagesInMetaspace,false);
  }

  size_t page_size = os::vm_page_size();
  if (UseLargePages && UseLargePagesInMetaspace) {
    page_size = os::large_page_size();
  }

  _commit_alignment  = page_size;
  _reserve_alignment = MAX2(page_size,(size_t)os::vm_allocation_granularity());
  
  MaxMetaspaceSize = align_down_bounded(MaxMetaspaceSize,_reserve_alignment);

  if (MetaspaceSize > MaxMetaspaceSize) {
    MetaspaceSize = MaxMetaspaceSize;
  }

  MetaspaceSize = align_down_bounded(MetaspaceSize,_commit_alignment);

  assert(MetaspaceSize <= MaxMetaspaceSize,"MetaspaceSize should be limited by MaxMetaspaceSize");

  MinMetaspaceExpansion = align_down_bounded(MinMetaspaceExpansion,_commit_alignment);
  MaxMetaspaceExpansion = align_down_bounded(MaxMetaspaceExpansion,_commit_alignment);

  CompressedClassSpaceSize = align_down_bounded(CompressedClassSpaceSize,_reserve_alignment);

  size_t min_metaspace_sz =
      VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize;
  if (UseCompressedClassPointers) {
    if ((min_metaspace_sz + CompressedClassSpaceSize) >  MaxMetaspaceSize) {
      if (min_metaspace_sz >= MaxMetaspaceSize) {
        vm_exit_during_initialization("MaxMetaspaceSize is too small.");
      } else {
        FLAG_SET_ERGO(size_t,CompressedClassSpaceSize,MaxMetaspaceSize - min_metaspace_sz);
      }
    }
  } else if (min_metaspace_sz >= MaxMetaspaceSize) {
    FLAG_SET_ERGO(size_t,InitialBootClassLoaderMetaspaceSize,min_metaspace_sz);
  }

  set_compressed_class_space_size(CompressedClassSpaceSize);
}
複製程式碼

在ergo初始化過程中主要是進行一些全域性變數的設定,例如MaxMetaspaceSize、MinMetaspaceExpansion、MaxMetaspaceExpansion和CompressedClassSpaceSize。其中比較重要的就是MaxMetaspaceSize和CompressedClassSpaceSize,預設情況下CompressedClassSpaceSize的大小為1G(相見globals.hpp)。

global_initialize

全域性初始化主要是用來初始化VirtualSpaceList和ChunkManager。其中ClassVirtualSpaceList的首節點大小直接分配為CompressedClassSpaceSize(不考慮開啟UseSharedSpaces模式的情況下)。而NonClassVirtualSpaceList的首節點大小則分配為4M*8/2(64位機器)或 2200K/4*2(32位機器)。原始碼中有很多關於對齊計算的原始碼,較為囉嗦,此處就不展示了。

post_initialize

void Metaspace::post_initialize() {
  MetaspaceGC::post_initialize();
}
複製程式碼

post初始化主要是用於MetaspaceGC的初始化,本文不關注Metaspace的GC,因此此部分也不進行探討。

2.2 classLoaderMetaspace初始化

classLoaderMetaspace的初始化與metaspace的初始化不同,metaspace是在JVM啟動的時候就已經初始化了,而classLoaderMetaspace的初始化則是當其對應的classLoader需要使用metaspace的時候才會進行初始化,程式碼如下:

ClassLoaderMetaspace* ClassLoaderData::metaspace_non_null() {
  ClassLoaderMetaspace* metaspace = OrderAccess::load_acquire(&_metaspace);
  if (metaspace == NULL) {
    MutexLockerEx ml(_metaspace_lock,Mutex::_no_safepoint_check_flag);
    // Check if _metaspace got allocated while we were waiting for this lock.
    if ((metaspace = _metaspace) == NULL) {
      if (this == the_null_class_loader_data()) {
        assert (class_loader() == NULL,"Must be");
        metaspace = new ClassLoaderMetaspace(_metaspace_lock,Metaspace::BootMetaspaceType);
      } else if (is_anonymous()) {
        metaspace = new ClassLoaderMetaspace(_metaspace_lock,Metaspace::AnonymousMetaspaceType);
      } else if (class_loader()->is_a(SystemDictionary::reflect_DelegatingClassLoader_klass())) {
        metaspace = new ClassLoaderMetaspace(_metaspace_lock,Metaspace::ReflectionMetaspaceType);
      } else {
        metaspace = new ClassLoaderMetaspace(_metaspace_lock,Metaspace::StandardMetaspaceType);
      }
      OrderAccess::release_store(&_metaspace,metaspace);
    }
  }
  return metaspace;
}
複製程式碼

在這段程式碼中我們可以看到四種ClassLoaderMetaspace型別分別與四種ClassLoader一一對應。


接下來是classLoaderMetaspace的初始化過程,

void ClassLoaderMetaspace::initialize(Mutex* lock,Metaspace::MetaspaceType type) {
  Metaspace::verify_global_initialization();

  DEBUG_ONLY(Atomic::inc(&g_internal_statistics.num_metaspace_births));

  _vsm = new SpaceManager(Metaspace::NonClassType,type,lock)
  if (Metaspace::using_class_space()) {
    _class_vsm = new SpaceManager(Metaspace::ClassType,lock);
  }

  MutexLockerEx cl(MetaspaceExpand_lock,Mutex::_no_safepoint_check_flag);

  initialize_first_chunk(type,Metaspace::NonClassType);
  if (Metaspace::using_class_space()) {
    initialize_first_chunk(type,Metaspace::ClassType);
  }
}
複製程式碼

在這段程式碼中我們可以看到初始化過程主要包含兩個步驟,

  1. 建立NonClassSpaceManger(_vsm)和ClassSpaceManager(_class_vsm)
  2. 初始化第一個NonClassChunk和第一個ClassChunk

我們接下來重點關注一下第一個Chunk的初始化過程(簡單期間,我們只關注NonClass型別的初始化,其實兩者基本一樣)。

// 程式碼已經過簡單整理
void ClassLoaderMetaspace::initialize_first_chunk(Metaspace::MetaspaceType type,Metaspace::MetadataType mdtype) {
  size_t chunk_word_size = get_space_manager(mdtype)->get_initial_chunk_size(type);
  
  Metachunk* chunk = Metaspace::get_chunk_manager(mdtype)->chunk_freelist_allocate(chunk_word_size);

  if (chunk == NULL) {
    chunk = Metaspace::get_space_list(mdtype)->get_new_chunk(chunk_word_size,get_space_manager(mdtype)->medium_chunk_bunch());
  }
  
  if (chunk != NULL) {
    get_space_manager(mdtype)->add_chunk(chunk,true);
  }
}
複製程式碼

總體看來,初始化第一個chunk分為了三步:

  1. 從全域性chunk_freelist中嘗試分配一個chunk
  2. 從全域性virtualSpaceList中建立一個新的chunk
  3. 將新的chunk新增到spaceManager中管理

不過在探究這三步之前,我們先來看看第一句程式碼,計算chunk大小,我們先來看看chunk大小如何計算,

enum ChunkSizes {    // in words.
  ClassSpecializedChunk = 128,SpecializedChunk = 128,ClassSmallChunk = 256,SmallChunk = 512,ClassMediumChunk = 4 * K,MediumChunk = 8 * K
};

size_t SpaceManager::adjust_initial_chunk_size(size_t requested,bool is_class_space) {
  size_t chunk_sizes[] = {
      specialized_chunk_size(is_class_space),small_chunk_size(is_class_space),medium_chunk_size(is_class_space)
  };
  for (size_t i = 0; i < ARRAY_SIZE(chunk_sizes); i++) {
    if (requested <= chunk_sizes[i]) {
      return chunk_sizes[i];
    }
  }
  return requested;
}

size_t SpaceManager::get_initial_chunk_size(Metaspace::MetaspaceType type) const {
  size_t requested;

  if (is_class()) {
    switch (type) {
      case Metaspace::BootMetaspaceType:       requested = Metaspace::first_class_chunk_word_size(); break;
      case Metaspace::AnonymousMetaspaceType:  requested = ClassSpecializedChunk; break;
      case Metaspace::ReflectionMetaspaceType: requested = ClassSpecializedChunk; break;
      default:                                 requested = ClassSmallChunk; break;
    }
  } else {
    switch (type) {
      case Metaspace::BootMetaspaceType:       requested = Metaspace::first_chunk_word_size(); break;
      case Metaspace::AnonymousMetaspaceType:  requested = SpecializedChunk; break;
      case Metaspace::ReflectionMetaspaceType: requested = SpecializedChunk; break;
      default:                                 requested = SmallChunk; break;
    }
  }

  const size_t adjusted = adjust_initial_chunk_size(requested);

  assert(adjusted != 0,"Incorrect initial chunk size. Requested: "
         SIZE_FORMAT " adjusted: " SIZE_FORMAT,requested,adjusted);

  return adjusted;
}
複製程式碼

在這裡我們可以看到不同型別的classLoaderMetaspace之間的區別,它們的初始chunk大小是不一樣的。同時,對於Class類和NonClass型別的Chunk,它們的specialized、small、medium三檔的大小值也是完全不同的。


接下來,我們重點仍然放回第一個chunk的初始化過程,此處重點關注前兩步,先是第一步——從全域性chunk_freelist中嘗試分配一個chunk。ChunkManager::chunk_freelist_allocate(size_t word_size)中主要呼叫了ChunkManager::free_chunks_get方法,我們來看看具體原始碼,

// 去除了校驗程式碼&日誌程式碼
Metachunk* ChunkManager::free_chunks_get(size_t word_size) {
  slow_locked_verify();

  Metachunk* chunk = NULL;
  bool we_did_split_a_chunk = false;

  if (list_index(word_size) != HumongousIndex) {

    ChunkList* free_list = find_free_chunks_list(word_size);

    chunk = free_list->head();

    if (chunk == NULL) {
      ChunkIndex target_chunk_index = get_chunk_type_by_size(word_size,is_class());
      Metachunk* larger_chunk = NULL;
      ChunkIndex larger_chunk_index = next_chunk_index(target_chunk_index);
      while (larger_chunk == NULL && larger_chunk_index < NumberOfFreeLists) {
        larger_chunk = free_chunks(larger_chunk_index)->head();
        if (larger_chunk == NULL) {
          larger_chunk_index = next_chunk_index(larger_chunk_index);
        }
      }

      if (larger_chunk != NULL) {
        chunk = split_chunk(word_size,larger_chunk);
        we_did_split_a_chunk = true;
      }
    }

    if (chunk == NULL) {
      return NULL;
    }

    free_list->remove_chunk(chunk)
  } else {
    chunk = humongous_dictionary()->get_chunk(word_size);

    if (chunk == NULL) {
      return NULL;
    }
  }
  chunk->set_next(NULL);
  chunk->set_prev(NULL);
  return chunk;
}
複製程式碼

簡單講解一下這段程式碼,記憶體分配分為了兩種情況

  • specialized、small、medium三種大小的chunk
  • humongous型別的chunk

其中specialized、small、medium三種型別的freeChunk分別對應了三個列表,而humongou型別的freeChunk由於其大小不固定,則使用排序二叉樹來儲存。

非humongou型別的chunk在分配過程中如果失敗,會嘗試將更大的chunk進行拆分。


接下來看從全域性virtualSpaceList中建立一個新的chunk的過程,

Metachunk* VirtualSpaceList::get_new_chunk(size_t chunk_word_size,size_t suggested_commit_granularity) {
  
  Metachunk* next = current_virtual_space()->get_chunk_vs(chunk_word_size);
  if (next != NULL) {
    return next;
  }

  const size_t size_for_padding = largest_possible_padding_size_for_chunk(chunk_word_size,this->is_class());

  size_t min_word_size       = align_up(chunk_word_size + size_for_padding,Metaspace::commit_alignment_words());
  size_t preferred_word_size = align_up(suggested_commit_granularity,Metaspace::commit_alignment_words());
  if (min_word_size >= preferred_word_size) {
    preferred_word_size = min_word_size;
  }

  bool expanded = expand_by(min_word_size,preferred_word_size);
  if (expanded) {
    next = current_virtual_space()->get_chunk_vs(chunk_word_size);
  }

   return next;
}
複製程式碼

整段程式碼可以整理為三步:

  1. 嘗試從當前virtualSpace分配chunk
  2. 擴充套件virtualSpace
  3. 再次嘗試從當前virtualSpace分配chunk

比較讓人感到好奇的是第二步,擴充套件virtualSpace,

bool VirtualSpaceList::expand_by(size_t min_words,size_t preferred_words) {

  if (!MetaspaceGC::can_expand(min_words,this->is_class())) {
    return false;
  }

  size_t allowed_expansion_words = MetaspaceGC::allowed_expansion();
  if (allowed_expansion_words < min_words) {
    return false;
  }

  size_t max_expansion_words = MIN2(preferred_words,allowed_expansion_words);

  bool vs_expanded = expand_node_by(current_virtual_space(),min_words,max_expansion_words);
  if (vs_expanded) {
     return true;
  }
  
  retire_current_virtual_space();

  size_t grow_vs_words = MAX2((size_t)VirtualSpaceSize,preferred_words);
  grow_vs_words = align_up(grow_vs_words,Metaspace::reserve_alignment_words());

  if (create_new_virtual_space(grow_vs_words)) {
    if (current_virtual_space()->is_pre_committed()) {
      return true;
    }
    return expand_node_by(current_virtual_space(),max_expansion_words);
  }

  return false;
}
複製程式碼

這一步主要包含幾個核心步驟:

  • 嘗試擴充套件當前virtualSpace
  • 當前virtualSpace不滿足要求,則將當前virtualSpace退休。這一步會將當前virtualSpace的剩餘空間回收到chunk_freelist
  • 建立一個新的virtualSpace

總結

整個classLoaderMetaspace的初始化過程可以總結為如下步驟:

  1. 建立SpaceManager
  2. 初始化ClassLoaderMetaspace第一個Chunk
    1. 從全域性chunk_freelist中嘗試分配一個chunk
    2. 從全域性virtualSpaceList中建立一個新的chunk
      1. 從當前virtualSpace嘗試分配
      2. 回收當前virtualSpace剩餘空間,新建一個virtualSpace並嘗試分配
    3. 初始化完成,將chunk新增到spaceManager中管理

3. 分配記憶體

對於metaspace而言,除了初始化之外,還有兩個最重要的功能——分配記憶體和釋放記憶體。我們先來看分配記憶體,

static bool is_class_space_allocation(MetadataType mdType) {
  return mdType == ClassType && using_class_space();
}

MetaWord* ClassLoaderMetaspace::allocate(size_t word_size,Metaspace::MetadataType mdtype) {
  if (Metaspace::is_class_space_allocation(mdtype)) {
    return  class_vsm()->allocate(word_size);
  } else {
    return  vsm()->allocate(word_size);
  }
}
複製程式碼

這段程式碼中,我們可以看到,只有元資料型別為Class型別以及使用壓縮指標的時候才會使用Class空間,否則都是使用NonClass空間。

接下來,我們繼續探究vsm()->allocate(word_size)方法,

MetaWord* SpaceManager::allocate(size_t word_size) {
  MutexLockerEx cl(lock(),Mutex::_no_safepoint_check_flag);
  size_t raw_word_size = get_allocation_word_size(word_size);
  BlockFreelist* fl =  block_freelists();
  MetaWord* p = NULL;

  if (fl != NULL && fl->total_size() > allocation_from_dictionary_limit) {
    p = fl->get_block(raw_word_size);
  }
  if (p == NULL) {
    p = allocate_work(raw_word_size);
  }

  return p;
}
複製程式碼

在這一步與之前metaspace初始化chunk有些異曲同工之處,此處也是先嚐試從block_freelists中進行分配,分配失敗再嘗試從chunk中進行分配,邏輯幾乎與上文的chunk初始化一摸一樣。

block_freelists同樣也是分為了小的和大的,資料結構如下:

class BlockFreelist : public CHeapObj<mtClass> {
  BlockTreeDictionary* const _dictionary;
  SmallBlocks* _small_blocks;
}

class SmallBlocks : public CHeapObj<mtClass> {
  FreeList<Metablock> _small_lists[_small_block_max_size - _small_block_min_size];
}
複製程式碼

在small_block_max_size到small_block_min_size範圍內的block通過連結串列來儲存,更大的block則使用排序二叉樹來實現。

至於chunk分配記憶體也如出一轍,先嚐試從當前chunk分配,分配失敗再新建chunk進行分配。

4. 釋放記憶體

釋放記憶體的程式碼則比較簡單,即直接將需要釋放的記憶體放回block_freelist中重新使用。

void SpaceManager::deallocate(MetaWord* p,size_t word_size) {
  size_t raw_word_size = get_allocation_word_size(word_size);
  if (block_freelists() == NULL) {
    _block_freelists = new BlockFreelist();
  }
  block_freelists()->return_block(p,raw_word_size);
}

void BlockFreelist::return_block(MetaWord* p,size_t word_size) {
  Metablock* free_chunk = ::new (p) Metablock(word_size);
  if (word_size < SmallBlocks::small_block_max_size()) {
    small_blocks()->return_block(free_chunk,word_size);
  } else {
  	dictionary()->return_chunk(free_chunk);
	}
}
複製程式碼

至此,metaspace部分的初始化,記憶體分配,記憶體釋放便已結束。