JEP-351 ZGC: Uncommit Unused Memory(JDK 13)

1. 簡介

將GC後的內存返回給OS這一能力,在雲計算場景中將節省用戶的支出。因此G1和Shenandoah均實現了該能力,在JDK 13中,ZGC也具備了該能力。

由於ZGC的堆被劃分爲一個個的ZPage,返回OS將以ZPage爲單位執行,實現較爲簡單,下文中將詳細說明。

2. 分析

2.1 頁緩存

ZGC中使用ZPageCache緩存ZPage,ZPageCache中維護了三個LRU鏈表分別緩存small medium large頁面。
zPageCache.cpp

class ZPageCache {
private:
  size_t                  _available;
  ZPerNUMA<ZList<ZPage> > _small;
  ZList<ZPage>            _medium;
  ZList<ZPage>            _large;

  ZPage* alloc_small_page();
  ZPage* alloc_medium_page();
  ZPage* alloc_large_page(size_t size);

  ZPage* alloc_oversized_medium_page(size_t size);
  ZPage* alloc_oversized_large_page(size_t size);
  ZPage* alloc_oversized_page(size_t size);

  void free_page_inner(ZPage* page);

}

2.2 頁的分配與回收

ZPageAllocator分配頁時,首先要從ZPageCache中查找是否有空閒的ZPage,如有則直接使用此ZPage,並將其從ZPageCache鏈表中摘除;ZPageAllocator釋放頁時,也是將ZPage加入ZPageCache鏈表。

zPageAllocator.cpp

ZPage* ZPageAllocator::alloc_page_common_inner(uint8_t type, size_t size, bool no_reserve) {
  if (!ensure_available(size, no_reserve)) {
    // Not enough free memory
    return NULL;
  }

  // 首先嚐試從ZPageCache中分配
  ZPage* const page = _cache.alloc_page(type, size);
  if (page != NULL) {
    return page;
  }

  // Try flush pages from the cache
  ensure_uncached_available(size);

  // Create new page
  return create_page(type, size);
}

void ZPageAllocator::free_page(ZPage* page, bool reclaimed) {
  ZLocker<ZLock> locker(&_lock);

  // Update used statistics
  decrease_used(page->size(), reclaimed);

  // Set time when last used
  page->set_last_used();

  // 添加到ZPageCache的LRU鏈表中
  _cache.free_page(page);

  // Try satisfy blocked allocations
  satisfy_alloc_queue();
}

2.3 頁返回給OS

JDK啓動了一個後臺任務,定時的調用ZHeap的uncommit函數,以返回頁給OS

zUncommitter.cpp

void ZUncommitter::run_service() {
  for (;;) {
    // Try uncommit unused memory
    const uint64_t timeout = ZHeap::heap()->uncommit(ZUncommitDelay);

    log_trace(gc, heap)("Uncommit Timeout: " UINT64_FORMAT "s", timeout);

    // 默認情況下idle 5*60秒
    if (!idle(timeout)) {
      return;
    }
  }
}

zPageAllocator.cpp

uint64_t ZPageAllocator::uncommit(uint64_t delay) {
  // Set the default timeout, when no pages are found in the
  // cache or when uncommit is disabled, equal to the delay.
  uint64_t timeout = delay;

  if (!_uncommit) {
    // Disabled
    return timeout;
  }

  size_t capacity_before;
  size_t capacity_after;
  size_t uncommitted;

  {
    SuspendibleThreadSetJoiner joiner;
    ZLocker<ZLock> locker(&_lock);

    // Don't flush more than we will uncommit. Never uncommit
    // the reserve, and never uncommit below min capacity.
    const size_t needed = MIN2(_used + _max_reserve, _current_max_capacity);
    const size_t guarded = MAX2(needed, _min_capacity);
    // 可以uncommit的容量等於總容量減去必須保留的容量
    const size_t uncommittable = _capacity - guarded;
    const size_t uncached_available = _capacity - _used - _cache.available();
    size_t uncommit = MIN2(uncommittable, uncached_available);
    // 將uncommittable與實際uncommit之間的頁flush,flush頁到uncommit
    const size_t flush = uncommittable - uncommit;

    if (flush > 0) {
      // Flush pages to uncommit
      ZPageCacheFlushForUncommitClosure cl(flush, delay);
      uncommit += flush_cache(&cl);
      timeout = cl.timeout();
    }

    // Uncommit
    uncommitted = _physical.uncommit(uncommit);
    _capacity -= uncommitted;

    capacity_after = _capacity;
    capacity_before = capacity_after + uncommitted;
  }

  if (uncommitted > 0) {
    log_info(gc, heap)("Capacity: " SIZE_FORMAT "M(%.0lf%%)->" SIZE_FORMAT "M(%.0lf%%), "
                       "Uncommitted: " SIZE_FORMAT "M",
                       capacity_before / M, percent_of(capacity_before, max_capacity()),
                       capacity_after / M, percent_of(capacity_after, max_capacity()),
                       uncommitted / M);

    // Update statistics
    ZStatInc(ZCounterUncommit, uncommitted);
  }

  return timeout;
}

3. 總結

基於良好的設計,JEP-351 ZGC: Uncommit Unused Memory僅僅做了很小的代碼改造就實現了。

4. 引用

OpenJDK 13 源代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章