TLAB整理

TLAB整理

  • HotSpot VM在JAVA堆中對象創(chuàng)建,布局,訪問全過程(僅限于普通java對象,不包括數(shù)組和Class對象等)

    • 對象創(chuàng)建

      • vm遇到new指令時 檢查指令的參數(shù)是否能在常量池中定位到一個類的符號引用 并檢查這個符號引用代表的類是否已經(jīng)加載,解析,初始化過
      • 如果沒有則先執(zhí)行類加載;在類加載檢查通過后,為新對象分配內(nèi)存,對象所需內(nèi)存大小在類加載完后完全確定;
      • 堆是規(guī)整的 (用過的內(nèi)存在一遍,沒有用過的在另一邊,中間一個指針)
        • 分配內(nèi)存直接挪動指針忘空閑的那邊 - 指針碰撞 bump the pointer
      • 堆不規(guī)整
        • 已使用內(nèi)存和空閑內(nèi)存相互交錯,維護(hù)一個列表記錄哪些內(nèi)存是可用的,空閑列表 free-list
      • 垃圾回收器是否帶有壓縮整理功能
        • Serial, ParNew等帶有Compact過程的收集器時 采用分配算法是 指針碰撞
        • CMS這種基于標(biāo)記-清除的通常采用 空閑列表 算法
      • 并發(fā)問題
        • 即便上面指針碰撞也會出現(xiàn)并發(fā)問題 因?yàn)閯?chuàng)建對象分配空間太頻繁了
        • solution
          • 1.分配空間的動作進(jìn)行同步處理(實(shí)際上VM采用cas+失敗重試的方式保證原子性)
          • 2,預(yù)先給線程分配一塊空間TLAB,后面分配空間先在TLAB上分配,TLAB不夠了再從堆上分配
            • -XX:+UseTLAB
      • 內(nèi)存分配完后,vm將分配到的內(nèi)存空間都初始化為0(不包括對象頭) TLAB的情況下清0過程可以提前至TLAB分配時
      • vm對對象進(jìn)行必要的設(shè)置:設(shè)置對象頭信息等
      • 執(zhí)行<init>方法按程序員的意愿初始化對象
      • bytecodeinterpreter.cpp源碼
    • 對象在內(nèi)存中的布局

      • 對象頭

        • Mark Word(對象自身運(yùn)行時數(shù)據(jù): hashcode, gc年齡, 鎖狀態(tài)標(biāo)識, 線程持有的鎖, 偏向線程ID, 偏向時間戳等)
          • mark word在32,64bits JVM中分別是32bit, 64bit(為開啟指針壓縮)
          • 對象自身運(yùn)行時需要存儲的信息很多,所以mark word被設(shè)計(jì)成一個非固定的數(shù)據(jù)結(jié)構(gòu),根據(jù)對象狀態(tài)復(fù)用存儲空間
        • 類型指針(哪個類的實(shí)例) 但不是所有的查找對象的元數(shù)據(jù)信息都有經(jīng)過對象本身
        • 記錄數(shù)組長度
          • 如果對象是一個JAVA數(shù)組
        • 通過普通java對象的元數(shù)據(jù)信息可以確定java對象的大小,但是從數(shù)組的元數(shù)據(jù)無法確定數(shù)組的大小(數(shù)組長度存儲在對象頭中)
        • markOop.cpp
          • mark word
      • 實(shí)例數(shù)據(jù)

        • 存儲順序
          • 分配策略參數(shù)
            • longs/doubles, ints, shorts/chars, bytes/booleans, oops(ordinary object pointer) (默認(rèn)分配策略)
            • 相同字寬的字段總是分配到一起, 父類定義的變量在子類之前
          • 字段在java源代碼中定義順序
      • 對齊填充

        • 對象起始地址必須是8字節(jié)的整數(shù)倍 也就是對象的大小必須是8字節(jié)的整數(shù)倍 而對象頭部分正好是8字節(jié)的倍數(shù)
    • 對象訪問

      • Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象(只規(guī)定了reference一個指向?qū)ο蟮囊?
        • 句柄
          • reference(java棧本地變量表)->句柄池(java heap)->兩個指針分別指向:實(shí)例池,方法區(qū)
          • 對象被移動,reference本身不需要修改;reference存儲的是穩(wěn)定的句柄地址
        • 直接指針
          • reference->堆中對象->方法區(qū)中的對象類型
          • 速度更快 節(jié)省一次指針定位時間 對象的訪問太頻繁
      • HotSopt VM 使用第二種,直接指針定位方式
  • TLAB細(xì)節(jié)

    • 什么是TLAB
      • TLAB全稱ThreadLocalAllocBuffer,是線程的一塊私有內(nèi)存,如果設(shè)置了虛擬機(jī)參數(shù) -XX:+UseTLAB,在線程初始化時,同時也會申請一塊指定大小的內(nèi)存,只給當(dāng)前線程使用,這樣每個線程都單獨(dú)擁有一個Buffer,如果需要分配內(nèi)存,就在自己的Buffer上分配,這樣就不存在競爭的情況,可以大大提升分配效率,當(dāng)Buffer容量不夠的時候,再重新從Eden區(qū)域申請一塊繼續(xù)使用,這個申請動作還是需要原子操作的(CAS+重試)
      • TLAB的目的是在為新對象分配內(nèi)存空間時,讓每個Java應(yīng)用線程能在使用自己專屬的分配指針來分配空間,均攤對GC堆(eden區(qū))里共享的分配指針做更新而帶來的同步開銷
      • TLAB只是讓每個線程有私有的分配指針,但底下存對象的內(nèi)存空間還是給所有線程訪問的,只是其它線程無法在這個區(qū)域分配而已。當(dāng)一個TLAB用滿(分配指針top撞上分配極限end了),就新申請一個TLAB,而在老TLAB里的對象還留在原地什么都不用管——它們無法感知自己是否是曾經(jīng)從TLAB分配出來的,而只關(guān)心自己是在eden里分配的。
    • TLAB實(shí)現(xiàn)
      • 源碼 openjdk/hotspot/src/share/vm/memory/threadLocalAllocBuffer.hpp
      • TLAB簡單來說本質(zhì)上就是三個指針:start,top 和 end,每個線程都會從Eden分配一大塊空間,例如說100KB,作為自己的TLAB,其中 start 和 end 是占位用的,標(biāo)識出 eden 里被這個 TLAB 所管理的區(qū)域,卡住eden里的一塊空間不讓其它線程來這里分配。而 top 就是里面的分配指針,一開始指向跟 start 同樣的位置,然后逐漸分配,直到再要分配下一個對象就會撞上 end 的時候就會觸發(fā)一次 TLAB refill,refill過程后續(xù)會解釋。
      • _desired_size 是指TLAB的內(nèi)存大小。
      • _refill_waste_limit 是指最大的浪費(fèi)空間,假設(shè)為5KB,通俗一點(diǎn)講就是:
        • 1、假如當(dāng)前TLAB已經(jīng)分配96KB,還剩下4KB,但是現(xiàn)在new了一個對象需要6KB的空間,顯然TLAB的內(nèi)存不夠了,這時可以簡單的重新申請一個TLAB,原先的TLAB交給Eden管理,這時只浪費(fèi)4KB的空間,在_refill_waste_limit 之內(nèi)。
        • 2、假如當(dāng)前TLAB已經(jīng)分配90KB,還剩下10KB,現(xiàn)在new了一個對象需要11KB,顯然TLAB的內(nèi)存不夠了,這時就不能簡單的拋棄當(dāng)前TLAB,這11KB會被安排到Eden區(qū)進(jìn)行申請。
      • 在Java代碼中執(zhí)行new Thread()的時候,會觸發(fā)以下代碼
        // The first routine called by a new Java thread void JavaThread::run() { // initialize thread-local alloc buffer related fields this->initialize_tlab(); // used to test validitity of stack trace backs this->record_base_of_stack_pointer(); // Record real stack base and size. this->record_stack_base_and_size(); // Initialize thread local storage; set before calling MutexLocker this->initialize_thread_local_storage(); this->create_stack_guard_pages(); this->cache_global_variables(); }
        void initialize_tlab() { if (UseTLAB) { tlab().initialize(); } }
      • 其中tlab()返回的就是一個ThreadLocalAllocBuffer對象,調(diào)用initialize()初始化TLAB,實(shí)現(xiàn)如下
        • 1、設(shè)置當(dāng)前TLAB的_desired_size,該值通過initial_desired_size()方法計(jì)算;
          • 字段_desired_size的計(jì)算過程分析
            • TLABSize在argument模塊中默認(rèn)會設(shè)置大小為 256 * K,也可以通過JVM參數(shù)選擇進(jìn)行設(shè)置,不過即使設(shè)置了也會和一個最大值max_size進(jìn)行比較,然后取一個較小值,其中max_size計(jì)算如下:
              • 這里明確說明了TLAB的大小不能超過可以容納 int[Integer.MAX_VALUE]
        • 2、設(shè)置當(dāng)前TLAB的_refill_waste_limit,該值通過initial_refill_waste_limit()方法計(jì)算;
          • 字段_refill_waste_limit計(jì)算分析
            size_t initial_refill_waste_limit() { return desired_size() / TLABRefillWasteFraction(默認(rèn)64); }
        • 3、初始化一些統(tǒng)計(jì)字段,如_number_of_refills、_fast_refill_waste、_slow_refill_waste、_gc_waste和_slow_allocations;
    • 內(nèi)存分配
      • 對象的內(nèi)存分配入口為instanceKlass::allocate_instance(),通過CollectedHeap::obj_allocate()方法在堆內(nèi)存上進(jìn)行分配
      • 其中common_mem_allocate_init()方法最終會調(diào)用CollectedHeap::common_mem_allocate_noinit()方法,實(shí)現(xiàn)如下
        • 根據(jù)UseTLAB的值,決定是否在TLAB上進(jìn)行內(nèi)存分配,如果JVM參數(shù)中沒有手動取消UseTLAB,會調(diào)用allocate_from_tlab()在TLAB上嘗試分配,因?yàn)榭赡艽嬖诜峙涫〉那闆r,比如TLAB容量不足,看下allocate_from_tlab()的實(shí)現(xiàn):
          • 從上述實(shí)現(xiàn)可以看出,先會嘗試調(diào)用ThreadLocalAllocBuffer 的 allocate 方法,如果返回為空,再執(zhí)行allocate_from_tlab_slow()進(jìn)行分配,從這個方法命名可以看出這是比較慢的分配路徑。
            • ThreadLocalAllocBuffer 的 allocate 方法實(shí)現(xiàn)如下:
              • 通過判斷當(dāng)前TLAB的剩余容量是否大于需要分配的大小,來決定分配結(jié)果,如果當(dāng)前剩余容量不夠,就返回NULL,表示分配失敗。
          • 慢分配allocate_from_tlab_slow()實(shí)現(xiàn)如下
            • 1、如果當(dāng)前TLAB的剩余容量大于浪費(fèi)閾值,就不在當(dāng)前TLAB分配,直接在共享的Eden區(qū)進(jìn)行分配,并且記錄慢分配的內(nèi)存大??;
            • 2、如果剩余容量小于浪費(fèi)閾值,說明可以丟棄當(dāng)前TLAB了;
            • 3、通過allocate_new_tlab()方法,從eden新分配一塊裸的空間出來(這一步可能會失?。?,如果失敗說明eden沒有足夠空間來分配這個新TLAB,就會觸發(fā)YGC。
            • 申請好新的TLAB內(nèi)存之后,執(zhí)行TLAB的fill()方法,實(shí)現(xiàn)如下:
              • 1、統(tǒng)計(jì)refill的次數(shù)
              • 2、初始化重新申請到的內(nèi)存塊
              • 3、將當(dāng)前TLAB拋棄(retire)掉,這個過程中最重要的動作是將TLAB末尾尚未分配給Java對象的空間(浪費(fèi)掉的空間)分配成一個假的“filler object”(目前是用int[]作為filler object)。這是為了保持GC堆可以線性parse(heap parseability)用的。
  • RednaxelaFX、你假笨關(guān)于TLAB的一些分析總結(jié)

    • TLAB refill包括下述幾個動作:
      • 將當(dāng)前TLAB拋棄(retire)掉。這個過程中最重要的動作是將TLAB末尾尚未分配給Java對象的空間(浪費(fèi)掉的空間)分配成一個假的“filler object”(目前是int[]作為filler object)。這是為了保持GC堆可以線性parse(heap parseability)用的。
      • 從eden新分配一塊裸的空間出來(這一步可能會失?。?/li>
      • 將新分配的空間范圍記錄到ThreadLocalAllocBuffer里TLAB refill不成功(eden沒有足夠空間來分配這個新TLAB)就會觸發(fā)YGC。
    • 注意“撞上”指的是在某次分配請求中,top + new_obj_size >= end 的情況,也就是說在被判定“撞上”的時候,top 常常離 end 還有一段距離,只是這之間的空間不足以滿足新對象的分配請求 new_obj_size 的大小。這意味著在觸發(fā)TLAB refill的時候,有可能會浪費(fèi)掉位于該TLAB末尾的一部分空間:該TLAB已經(jīng)占用了這塊空間所以其它線程無法在這里分配Java對象,但該TLAB要refill的話它自己也不會在這塊空間繼續(xù)分配Java對象,從應(yīng)用層面看這塊空間就浪費(fèi)了。
    • 每次分配TLAB的大小不是固定的,而是每個線程根據(jù)該線程啟動開始到現(xiàn)在的歷史統(tǒng)計(jì)信息來自己單獨(dú)調(diào)整的。如果一個線程上跑的代碼的內(nèi)存分配速率非常高,則該線程會選擇使用更大的TLAB以達(dá)到均攤同步開銷的效果,反之亦然;同時它還會統(tǒng)計(jì)浪費(fèi)比例,并且將其放入計(jì)算新TLAB大小的考慮因素當(dāng)中,把浪費(fèi)比例控制在一定范圍內(nèi)。
    • GC很重要的一點(diǎn)是對heap parseability的依賴。GC做某些需要線性掃描堆里的對象的操作時,需要知道堆里哪些地方有對象而哪些地方是空洞。一種辦法是使用外部數(shù)據(jù)結(jié)構(gòu),例如freelist或者allocation BitMap之類來記錄哪里有空洞;另一種辦法是把空洞部分也假裝成有對象,這樣GC在線性遍歷時會看到一個“對象總是連續(xù)分配的”的假象,就可以以統(tǒng)一的方式來遍歷:遍歷到一個對象時,通過其對象頭記錄的信息找出該對象的大小,然后跳到該大小之后就可以找到下一個對象的對象頭,依此類推。HotSpot選擇的是后者的做法,假裝成有對象的這種東西就叫做filler object(填充對象)。
    • PLAB也是個非常有趣的東西,提到TLAB的話也可以順帶說下PLAB。HotSpot里的TLAB是只在eden里分配的,用于給新建的小對象用。(本來其實(shí)也有考慮讓TLAB在任意位置分配,但后來沒實(shí)現(xiàn))。PLAB則是在old gen里分配的一種臨時的結(jié)構(gòu)。就是笨神說的promotion LAB。
    • 在多GC線程并行做YGC的時候,大家都要為了晉升對象而在old gen里分配空間,于是old gen的分配指針就熱起來了。大量的競爭會使得并行度降低,所以跟TLAB用同樣的思路,old gen在處理YGC的晉升對象的分配也一樣可以用(GC)線程私有的分配區(qū)。這就是PLAB。另外在CMS里old gen的剩余空間不是連續(xù)的,而是有很多空洞。這些剩余空間是通過freelist來管理的。
    • 如果ParNew要把對象晉升到CMS管理的old gen,不優(yōu)化的話就得在freelist上做分配。于是就可以通過類似PLAB的方式,每個GC線程先從freelist申請一塊大空間,然后在這塊大空間里線性分配(bump pointer)。這樣就既降低了對分配指針/freelist的競爭,又可以降低freelist分配的頻率而轉(zhuǎn)為用線性分配。
  • References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容