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
- Mark Word(對象自身運(yùn)行時數(shù)據(jù): hashcode, gc年齡, 鎖狀態(tài)標(biāo)識, 線程持有的鎖, 偏向線程ID, 偏向時間戳等)
-
實(shí)例數(shù)據(jù)
- 存儲順序
- 分配策略參數(shù)
- longs/doubles, ints, shorts/chars, bytes/booleans, oops(ordinary object pointer) (默認(rèn)分配策略)
- 相同字寬的字段總是分配到一起, 父類定義的變量在子類之前
- 字段在java源代碼中定義順序
- 分配策略參數(shù)
- 存儲順序
-
對齊填充
- 對象起始地址必須是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 使用第二種,直接指針定位方式
- Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象(只規(guī)定了reference一個指向?qū)ο蟮囊?
-
-
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]
- TLABSize在argument模塊中默認(rèn)會設(shè)置大小為 256 * K,也可以通過JVM參數(shù)選擇進(jìn)行設(shè)置,不過即使設(shè)置了也會和一個最大值max_size進(jìn)行比較,然后取一個較小值,其中max_size計(jì)算如下:
- 字段_desired_size的計(jì)算過程分析
- 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); }
- 字段_refill_waste_limit計(jì)算分析
- 3、初始化一些統(tǒng)計(jì)字段,如_number_of_refills、_fast_refill_waste、_slow_refill_waste、_gc_waste和_slow_allocations;
- 1、設(shè)置當(dāng)前TLAB的_desired_size,該值通過initial_desired_size()方法計(jì)算;
- 內(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,表示分配失敗。
- ThreadLocalAllocBuffer 的 allocate 方法實(shí)現(xiàn)如下:
- 慢分配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)用的。
- 從上述實(shí)現(xiàn)可以看出,先會嘗試調(diào)用ThreadLocalAllocBuffer 的 allocate 方法,如果返回為空,再執(zhí)行allocate_from_tlab_slow()進(jì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):
- 什么是TLAB
-
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)為用線性分配。
- TLAB refill包括下述幾個動作:
-
References