《JVM G1源碼分析和調優》 —3.2 快速分配

      網友投稿 809 2025-03-31

      3.2 快速分配

      TLAB產生的目的就是為了進行內存快速分配。通常來說,JVM堆是所有線程的共享區域。因此,從JVM堆空間分配對象時,必須鎖定整個堆,以便不會被其他線程中斷和影響。為了解決這個問題,TLAB試圖通過為每個線程分配一個緩沖區來避免和減少使用鎖。

      在分配線程對象時,從JVM堆中分配一個固定大小的內存區域并將其作為線程的私有緩沖區,這個緩沖區稱為TLAB。只有在為每個線程分配TLAB緩沖區時才需要鎖定整個JVM堆。由于TLAB是屬于線程的,不同的線程不共享TLAB,當我們嘗試分配一個對象時,優先從當前線程的TLAB中分配對象,不需要鎖,因此達到了快速分配的目的。

      更進一步地講,實際上TLAB是Eden區域中的一塊內存,不同線程的TLAB都位于Eden區,所有的TLAB內存對所有的線程都是可見的,只不過每個線程有一個TLAB的數據結構,用于保存待分配內存區間的起始地址(start)和結束地址(end),在分配的時候只在這個區間做分配,從而達到無鎖分配,快速分配。

      另外值得說明的是,雖然TLAB在分配對象空間的時候是無鎖分配,但是TLAB空間本身在分配的時候還是需要鎖的,G1中使用了CAS來并行分配。

      圖3-2 TLAB在分區中的使用

      在圖3-2中,Tn表示第n個線程,深灰色表示該TLAB塊已經分配完畢,淺灰色表示該TLAB塊還可以分配更多的對象。

      從圖中我們可以看出,線程T1已經使用了兩個TLAB塊,T1、T2和T4的TLAB塊都有待分配的空間。這里并沒有提及Eden和多個分區的概念,實際上一個分區可能有多個TLAB塊,但是一個TLAB是不可能跨分區的。從圖中我們也可以看出,每個線程的TLAB塊并不重疊,所以線程之間對象的分配是可以并行的,且無影響。另外圖中還隱藏了一些細節:

      T1已經使用完兩個TLAB塊,這兩個塊在回收的時候如何處理?

      我們可以想象TLAB的大小是固定的,但是對象的大小并不固定,因此TLAB中可能存在內存碎片的問題,這個該如何解決?請繼續往下閱讀。

      快速TLAB對象分配也有兩步:

      從線程的TLAB分配空間,如果成功則返回。

      不能分配,先嘗試分配一個新的TLAB,再分配對象。

      代碼如下所示:

      hotspot/src/share/vm/gc_interface/collectedHeap.inline.hpp

      HeapWord* CollectedHeap::allocate_from_tlab(KlassHandle klass, Thread*

      thread, size_t size) {

      HeapWord* obj = thread->tlab().allocate(size);

      if (obj != NULL)??? return obj;

      // 省略一些判斷比如是否需要申請一個新的TLAB

      return allocate_from_tlab_slow(klass, thread, size);

      }

      從TLAB已分配的緩沖區空間直接分配對象,也稱為指針碰撞法分配,其方法非常簡單,在TLAB中保存一個top指針用于標記當前對象分配的位置,如果剩余空間(end-top)大于待分配對象的空間(objSize),則直接修改top = top + ObjSize,相關代碼位于thread->tlab().allocate(size)中。對于分配失敗,處理稍微麻煩一些,相關代碼位于allocate_from_tlab_slow()中,在學習這部分代碼之前,先思考一下這樣的內存分配管理該如何設計。

      如果TLAB過小,那么TLAB則不能存儲更多的對象,所以可能需要不斷地重新分配新的TLAB。但是如果TLAB過大,則可能導致內存碎片問題。假設TLAB大小為1M,Eden為200M。如果有40個線程,每個線程分配1個TLAB,TLAB被填滿之后,發生GC。假設TLAB中對象分配符合均勻分布,那么發生GC時,TLAB總的大小為:40×1×0.5 = 20M(Eden的10%左右),這意味著Eden還有很多空間時就發生了GC,這并不是我們想要的。最直觀的想法是增加TLAB的大小或者增加線程的個數,這樣TLAB在分配的時候效率會更高,但是在GC回收的時候則可能花費更長的時間。因此JVM提供了參數TLABSize用于控制TLAB的大小,如果我們設置了這個值,那么JVM就會使用這個值來初始化TLAB的大小。但是這樣設置不夠優雅,其實TLABSize默認值是0,也就是說JVM會推斷這個值多大更合適。采用的參數為TLABWasteTargetPercent,用于設置TLAB可占用的Eden空間的百分比,默認值1%,推斷方式為TLABSize = Eden×2×1%/線程個數(乘以2是因為假設其內存使用服從均勻分布),G1中是通過下面的公式計算的:

      hotspot/src/share/vm/memory/threadLocalAllocBuffer.cpp

      init_sz? = (Universe::heap()->tlab_capacity(myThread()) / HeapWordSize) /

      (nof_threads * target_refills());

      其中,tlab_capacity在G1CollectedHeap中實現,代碼如下所示:

      hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp

      size_t G1CollectedHeap::tlab_capacity(Thread* ignored) const {

      return (_g1_policy->young_list_target_length() - young_list()->survivor_

      length()) * HeapRegion::GrainBytes;

      }

      簡單來說,tlab_capacity就是Eden所有可用的區域。另外要注意的是,這里采用的啟發式推斷也僅僅是一個近似值,實際上線程在使用內存分配對象時并不是無關的(不完全服從均勻分布),另外不同的線程類型對內存的使用也不同,比如一些調度線程、監控線程等幾乎不會分配新的對象。

      在Java對象分配時,我們總希望它位于TLAB中,如果TLAB滿了之后,如何處理呢?前面提到TLAB其實就是Eden的一塊區域,在G1中就是HeapRegion的一塊空閑區域。所以TLAB滿了之后無須做額外的處理,直接保留這一部分空間,重新在Eden/堆分區中分配一塊空間給TLAB,然后再在TLAB分配具體的對象。但這里會有兩個小問題。

      1.如何判斷TLAB滿了?

      按照前面的例子TLAB是1M,當我們使用800K,還是900K,還是950K時被認為滿了?問題的答案是如何尋找最大的可能分配對象和減少內存碎片的平衡。實際上虛擬機內部會維護一個叫做refill_waste的值,當請求對象大于refill_waste時,會選擇在堆中分配,若小于該值,則會廢棄當前TLAB,新建TLAB來分配對象。這個閾值可以使用TLABRefillWasteFraction來調整,它表示TLAB中允許產生這種浪費的比例。默認值為64,即表示使用約為1/64的TLAB空間作為refill_waste,在我們的這個例子中,refill_waste的初始值為16K,即TLAB中還剩(1M - 16k = 1024 - 16 = 1008K)1008K內存時直接分配一個新的,否則盡量使用這個老的TLAB。

      2.如何調整TLAB

      如果要分配的內存大于TLAB剩余的空間則直接在Eden/HeapRegion中分配。那么這個1/64是否合適?會不會太小,比如通常分配的對象大多是20K,最后剩下16K,這樣導致每次都進入Eden/堆分區慢速分配中。所以,JVM還提供了一個參數TLAB

      WasteIncrement(默認值為4個字)用于動態增加這個refill_waste的值。默認情況下,TLAB大小和refill_waste都會在運行時不斷調整,使系統的運行狀態達到最優。在動態調整的過程中,也不能無限制變更,所以JVM提供MinTLABSize(默認值2K)用于控制最小值,對于G1來說,由于大對象都不在新生代分區,所以TLAB也不能分配大對象,HeapRegion/2就會被認定為大對象,所以TLAB肯定不會超過HeapRegionSize

      的一半。

      如果想要禁用自動調整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB,

      并使用-XX:TLABSize手工指定一個TLAB的大小。-XX:+PrintTLAB可以跟蹤TLAB的使用情況。一般不建議手工修改TLAB相關參數,推薦使用虛擬機默認行為。

      繼續來看TLAB中的慢速分配,主要的步驟有:

      TLAB的剩余空間是否太小,如果很小,即說明這個空間通常不滿足對象的分配,所以最好丟棄,丟棄的方法就是填充一個dummy對象,然后申請新的TLAB來分配對象。

      如果不能丟棄,說明TLAB剩余空間并不小,能滿足很多對象的分配,所以不能丟棄這個TLAB,否則內存浪費很多,此時可以把對象分配到堆中,不使用TLAB分配,所以可以直接返回。

      TLAB慢速分配代碼如下所示:

      hotspot/src/share/vm/gc_interface/collectedHeap.cpp

      HeapWord* CollectedHeap::allocate_from_tlab_slow(KlassHandle klass, Thread*

      thread, size_t size) {

      // 判斷TLAB尚未分配的剩余空間是否可以丟掉。如果剩余空間大于閾值則保留,其中閾值為

      // refill waste limit,它由desired size和參數TLABRefillWasteFraction

      // 計算得到

      if (thread->tlab().free() > thread->tlab().refill_waste_limit()) {

      // 不能丟掉,根據TLABWasteIncrement更新refill_waste的閾值

      thread->tlab().record_slow_allocation(size);

      // 返回NULL,說明在Eden/HeapRegion中分配

      return NULL;

      }

      // 說明TLAB剩余空間很小了,所以要重新分配一個TLAB。老的TLAB不用處理,因為它屬于Eden,

      // GC可以正確回收空間

      size_t new_tlab_size = thread->tlab().compute_size(size);

      // 分配之前先清理老的TLAB,其目的就是為了讓堆保持parsable可解析

      thread->tlab().clear_before_allocation();

      if (new_tlab_size == 0)???? return NULL;

      // 分配一個新的TLAB...

      HeapWord* obj = Universe::heap()->allocate_new_tlab(new_tlab_size);

      if (obj == NULL)???? return NULL;

      // 發生一個事件,用于統計分配信息

      AllocTracer::send_allocation_in_new_tlab_event(klass, new_tlab_size *

      HeapWordSize, size * HeapWordSize);

      // 是否把內存空間清零

      if (ZeroTLAB)? Copy::zero_to_words(obj, new_tlab_size);

      // 分配對象,并設置TLAB的start、top、end等信息

      thread->tlab().fill(obj, obj + size, new_tlab_size);

      return obj;

      }

      為什么要對老的TLAB做清理動作?

      TLAB存儲的都是已經分配的對象,為什么要清理以及清理什么?其實這里的清理就是把尚未分配的空間分配一個對象(通常是一個int[]),那么為什么要分配一個垃圾對象?代碼說明是為了棧解析(Heap Parsable),Heap Parsable是什么?為什么需要設置?下面繼續分析。

      內存管理器(GC)在進行某些需要線性掃描堆里對象的操作時,比如,查看Heap

      Region對象、并行標記等,需要知道堆里哪些地方有對象,而哪些地方是空白。對于對象,掃描之后可以直接跳過對象的長度,對于空白的地方只能一個字一個字地掃描,這會非常慢。所以可以把這塊空白的地方也分配一個dummy對象(啞元對象),這樣GC在線性遍歷時就能做到快速遍歷了。這樣的話就能統一處理,示例代碼如下:

      HeapWord* cur = heap_start;

      while (cur < heap_used) {

      object o = (object)cur;

      do_object(o);

      cur = cur + o->size();

      }

      具體我們可以在新生代垃圾回收的時候再來驗證這一點。我們再看一下如何申請一個新的TLAB緩沖區,代碼如下所示:

      hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp

      HeapWord* G1CollectedHeap::allocate_new_tlab(size_t word_size) {

      return attempt_allocation(word_size, &dummy_gc_count_before, &dummy_

      gclocker_retry_count);

      }

      它最終會調用到G1CollectedHeap中分配,其分配主要是在attempt_allocation完成的,步驟也分為兩步:快速無鎖分配和慢速分配。圖3-3為慢速分配流程圖。

      TLAB緩沖區分配代碼如下所示:

      hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.inline.cpp

      inline HeapWord* G1CollectedHeap::attempt_allocation(…) {

      AllocationContext_t context = AllocationContext::current();

      HeapWord* result = _allocator->mutator_alloc_region(context)->attempt_

      allocation(word_size, false /* bot_updates */);

      if (result == NULL) {

      result = attempt_allocation_slow(…);

      }

      if (result != NULL)? dirty_young_block(result, word_size);

      return result;

      }

      《JVM G1源碼分析和調優》 —3.2 快速分配

      圖3-3 申請TLAB分區和對象慢速分配流程圖

      快速無鎖分配:指的是在當前可以分配的堆分區中使用CAS來獲取一塊內存,如果成功則可以作為TLAB的空間。因為使用CAS可以并行分配,當然也有可能不成功。對于不成功則進行慢速分配,代碼如下所示:

      hotspot/src/share/vm/gc_implementation/g1/heapRegion.inline.hpp

      inline HeapWord* G1OffsetTableContigSpace::par_allocate_impl(size_t size,

      HeapWord* const end_value) {

      do {

      HeapWord* obj = top();

      if (pointer_delta(end_value, obj) >= size) {

      HeapWord* new_top = obj + size;

      HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);

      if (result == obj)??? return obj;

      } else {

      return NULL;

      }

      } while (true);

      }

      對于不成功則進行慢速分配,慢速分配需要嘗試對Heap加鎖,擴展新生代區域或垃圾回收等處理后再分配。

      首先嘗試對堆分區進行加鎖分配,成功則返回,在attempt_allocation_locked完成。

      不成功,則判定是否可以對新生代分區進行擴展,如果可以擴展則擴展后再分配TLAB,成功則返回,在attempt_allocation_force完成。

      不成功,判定是否可以進行垃圾回收,如果可以進行垃圾回收后再分配,成功則返回,在do_collection_pause完成。

      不成功,如果嘗試分配次數達到閾值(默認值是2次)則返回失敗。

      如果還可以繼續嘗試,再次判定是否進行快速分配,如果成功則返回。

      不成功重新再嘗試一次,直到成功或者達到閾值失敗。

      所以慢速分配要么成功分配,要么嘗試次數達到閾值后結束并返回NULL。代碼如下:

      hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp

      HeapWord* G1CollectedHeap::attempt_allocation_slow(…) {

      HeapWord* result = NULL;

      for (int try_count = 1; /* we'll return */; try_count += 1) {

      {

      // 加鎖分配

      result = _allocator->mutator_alloc_region(context)->attempt_

      allocation_locked(word_size,??? false /* bot_updates */);

      if (result != NULL)???????? return result;

      if (GC_locker::is_active_and_needs_gc()) {

      if (g1_policy()->can_expand_young_list()) {

      result = _allocator->mutator_alloc_region(context)->attempt_

      allocation_force(word_size, false /* bot_updates */);

      if (result != NULL)???? return result;

      }

      should_try_gc = false;

      } else {

      if (GC_locker::needs_gc()) {

      should_try_gc = false;

      } else {

      gc_count_before = total_collections();

      should_try_gc = true;

      }

      }

      }

      if (should_try_gc) {

      // GCLocker沒有進入臨界區,可以進行垃圾回收

      result = do_collection_pause(word_size, gc_count_before, &succeeded,

      GCCause::_g1_inc_collection_pause);

      if (result != NULL)?????? return result;

      if (succeeded) {

      // 稍后可以進行回收,可以先返回

      MutexLockerEx x(Heap_lock);

      *gc_count_before_ret = total_collections();

      return NULL;

      }

      } else {

      // JNI進入臨界區中,判斷是否達到分配次數閾值

      if (*gclocker_retry_count_ret > GCLockerRetryAllocationCount) {

      MutexLockerEx x(Heap_lock);

      *gc_count_before_ret = total_collections();

      return NULL;

      }

      GC_locker::stall_until_clear();

      (*gclocker_retry_count_ret) += 1;

      }

      // 可能因為其他線程正在分配或者GCLocker正在被競爭使用等,

      // 在進行加鎖分配前再嘗試進行無鎖分配

      result = _allocator->mutator_alloc_region(context)->attempt_

      allocation(word_size, false /* bot_updates */);

      if (result != NULL)????? return result;

      }

      ShouldNotReachHere();

      return NULL;

      }

      這里GCLocker是與JNI相關的。簡單來說Java代碼可以和本地代碼交互,在訪問JNI代碼時,因為JNI代碼可能會進入臨界區,所以此時會阻止GC垃圾回收。這部分知識相對獨立,有關GCLocker的知識可以參看其他文章。

      日志及解讀

      從一個Java的例子出發,代碼如下:

      public class Test {

      private static final LinkedList strings = new LinkedList<>();

      public static void main(String[] args) throws Exception {

      int iteration = 0;

      while (true) {

      for (int i = 0; i < 100; i++) {

      for (int j = 0; j < 10; j++) {

      strings.add(new String("String " + j));

      }

      }

      Thread.sleep(100);

      }

      }

      }

      通過命令設置參數,如下所示:

      -Xmx128M -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

      -XX:+PrintTLAB -XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest

      可以得到:

      garbage-first heap?? total 131072K, used 37569K [0x00000000f8000000,

      0x00000000f8100400, 0x0000000100000000)

      region size 1024K, 24 young (24576K), 0 survivors (0K)

      TLAB: gc thread: 0x0000000059ade800 [id: 16540] desired_size: 491KB slow

      allocs: 8? refill waste: 7864B alloc: 0.99999??? 24576KB refills: 50

      waste? 0.0% gc: 0B slow: 816B fast: 0Bd

      對于多線程的情況,這里還會有每個線程的輸出結果以及一個總結信息。由于篇幅的關系此處都已經省略。下面我們分析日志中TLAB這個信息的每一個字段含義:

      desired_size為期望分配的TLAB的大小,這個值就是我們前面提到如何計算TLABSize的方式。在這個例子中,第一次的時候,不知道會有多少線程,所以初始化為1,desired_size = 24576/50 = 491.5KB這個值是經過取整的。

      slow allocs為發生慢速分配的次數,日志中顯示有8次分配到heap而沒有使用TLAB。

      refill waste為retire一個TLAB的閾值。

      alloc為該線程在堆分區分配的比例。

      refills發生的次數,這里是50,表示從上一次GC到這次GC期間,一共retire過50個TLAB塊,在每一個TLAB塊retire的時候都會做一次refill把尚未使用的內存填充為dummy對象。

      waste由3個部分組成:

      gc:發生GC時還沒有使用的TLAB的空間。

      slow:產生新的TLAB時,舊的TLAB浪費的空間,這里就是新生成50個TLAB,浪費了816個字節。

      fast:指的是在C1中,發生TLAB retire(產生新的TLAB)時,舊的TLAB浪費的空間。

      JVM

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:對話準獨角獸簡道云:低代碼不是萬能的
      下一篇:Word文檔數字上標怎么弄(Word文檔數字上標)
      相關文章
      久久精品国产亚洲av日韩| 亚洲av永久无码制服河南实里| 亚洲欧洲日韩不卡| 亚洲精品无码永久中文字幕| 亚洲精品国产综合久久一线| 亚洲国产成人五月综合网| 亚洲精品成人久久久| 亚洲av麻豆aⅴ无码电影| 亚洲国产精品无码久久青草 | 久久亚洲成a人片| 亚洲AV中文无码字幕色三| 国产亚洲精品一品区99热| 亚洲av综合avav中文| 亚洲国产高清在线| 99亚洲精品高清一二区| 亚洲码在线中文在线观看| 亚洲日产2021三区在线| 亚洲一区二区三区久久| 亚洲自偷自偷在线成人网站传媒 | 亚洲人av高清无码| 亚洲avav天堂av在线网毛片| 国产综合成人亚洲区| 亚洲国产成人久久一区久久| 精品亚洲成α人无码成α在线观看 | 亚洲综合成人网在线观看| 久久久亚洲裙底偷窥综合| 亚洲二区在线视频| 亚洲午夜福利在线视频| 亚洲a∨无码一区二区| 亚洲成a人在线看天堂无码| 亚洲中文字幕视频国产| 好看的亚洲黄色经典| 噜噜噜亚洲色成人网站∨| 亚洲免费黄色网址| 亚洲精品中文字幕| 亚洲国产综合久久天堂| 久久精品国产亚洲沈樵| 78成人精品电影在线播放日韩精品电影一区亚洲 | 亚洲av伊人久久综合密臀性色| 久久精品亚洲精品国产色婷| 亚洲一区二区久久|