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)存相互交錯,維護一個列表記錄哪些內(nèi)存是可用的,空閑列表 free-list
- 垃圾回收器是否帶有壓縮整理功能
- Serial, ParNew等帶有Compact過程的收集器時 采用分配算法是 指針碰撞
- CMS這種基于標記-清除的通常采用 空閑列表 算法
- 并發(fā)問題
- 即便上面指針碰撞也會出現(xiàn)并發(fā)問題 因為創(chuàng)建對象分配空間太頻繁了
- solution
- 1.分配空間的動作進行同步處理(實際上VM采用cas+失敗重試的方式保證原子性)
- 2诵竭,預(yù)先給線程分配一塊空間TLAB,后面分配空間先在TLAB上分配,TLAB不夠了再從堆上分配
- -XX:+UseTLAB
- 內(nèi)存分配完后,vm將分配到的內(nèi)存空間都初始化為0(不包括對象頭) TLAB的情況下清0過程可以提前至TLAB分配時
- vm對對象進行必要的設(shè)置:設(shè)置對象頭信息等
- 執(zhí)行<init>方法按程序員的意愿初始化對象
- bytecodeinterpreter.cpp源碼
-
對象在內(nèi)存中的布局
-
對象頭
- Mark Word(對象自身運行時數(shù)據(jù): hashcode, gc年齡, 鎖狀態(tài)標識, 線程持有的鎖, 偏向線程ID, 偏向時間戳等)
- mark word在32谷徙,64bits JVM中分別是32bit, 64bit(為開啟指針壓縮)
- 對象自身運行時需要存儲的信息很多,所以mark word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu),根據(jù)對象狀態(tài)復(fù)用存儲空間
- 類型指針(哪個類的實例) 但不是所有的查找對象的元數(shù)據(jù)信息都有經(jīng)過對象本身
- 記錄數(shù)組長度
- 如果對象是一個JAVA數(shù)組
- 通過普通java對象的元數(shù)據(jù)信息可以確定java對象的大小,但是從數(shù)組的元數(shù)據(jù)無法確定數(shù)組的大小(數(shù)組長度存儲在對象頭中)
- markOop.cpp
- mark word
- Mark Word(對象自身運行時數(shù)據(jù): hashcode, gc年齡, 鎖狀態(tài)標識, 線程持有的鎖, 偏向線程ID, 偏向時間戳等)
-
實例數(shù)據(jù)
- 存儲順序
- 分配策略參數(shù)
- longs/doubles, ints, shorts/chars, bytes/booleans, oops(ordinary object pointer) (默認分配策略)
- 相同字寬的字段總是分配到一起, 父類定義的變量在子類之前
- 字段在java源代碼中定義順序
- 分配策略參數(shù)
- 存儲順序
-
對齊填充
- 對象起始地址必須是8字節(jié)的整數(shù)倍 也就是對象的大小必須是8字節(jié)的整數(shù)倍 而對象頭部分正好是8字節(jié)的倍數(shù)
-
-
對象訪問
- Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象(只規(guī)定了reference一個指向?qū)ο蟮囊?
- 句柄
- reference(java棧本地變量表)->句柄池(java heap)->兩個指針分別指向:實例池,方法區(qū)
- 對象被移動,reference本身不需要修改;reference存儲的是穩(wěn)定的句柄地址
- 直接指針
- reference->堆中對象->方法區(qū)中的對象類型
- 速度更快 節(jié)省一次指針定位時間 對象的訪問太頻繁
- 句柄
- HotSopt VM 使用第二種驱敲,直接指針定位方式
- Java程序通過棧上的reference數(shù)據(jù)來操作堆上的具體對象(只規(guī)定了reference一個指向?qū)ο蟮囊?
-
-
TLAB細節(jié)
- 什么是TLAB
- TLAB全稱ThreadLocalAllocBuffer提完,是線程的一塊私有內(nèi)存百新,如果設(shè)置了虛擬機參數(shù) -XX:+UseTLAB珊燎,在線程初始化時,同時也會申請一塊指定大小的內(nèi)存姑廉,只給當前線程使用缺亮,這樣每個線程都單獨擁有一個Buffer,如果需要分配內(nèi)存桥言,就在自己的Buffer上分配萌踱,這樣就不存在競爭的情況,可以大大提升分配效率号阿,當Buffer容量不夠的時候虫蝶,再重新從Eden區(qū)域申請一塊繼續(xù)使用,這個申請動作還是需要原子操作的(CAS+重試)
- TLAB的目的是在為新對象分配內(nèi)存空間時倦西,讓每個Java應(yīng)用線程能在使用自己專屬的分配指針來分配空間能真,均攤對GC堆(eden區(qū))里共享的分配指針做更新而帶來的同步開銷
- TLAB只是讓每個線程有私有的分配指針,但底下存對象的內(nèi)存空間還是給所有線程訪問的,只是其它線程無法在這個區(qū)域分配而已粉铐。當一個TLAB用滿(分配指針top撞上分配極限end了)疼约,就新申請一個TLAB,而在老TLAB里的對象還留在原地什么都不用管——它們無法感知自己是否是曾經(jīng)從TLAB分配出來的蝙泼,而只關(guān)心自己是在eden里分配的程剥。
- TLAB實現(xiàn)
- 源碼 openjdk/hotspot/src/share/vm/memory/threadLocalAllocBuffer.hpp
- TLAB簡單來說本質(zhì)上就是三個指針:start,top 和 end汤踏,每個線程都會從Eden分配一大塊空間织鲸,例如說100KB,作為自己的TLAB溪胶,其中 start 和 end 是占位用的搂擦,標識出 eden 里被這個 TLAB 所管理的區(qū)域,卡住eden里的一塊空間不讓其它線程來這里分配哗脖。而 top 就是里面的分配指針瀑踢,一開始指向跟 start 同樣的位置,然后逐漸分配才避,直到再要分配下一個對象就會撞上 end 的時候就會觸發(fā)一次 TLAB refill橱夭,refill過程后續(xù)會解釋。
- _desired_size 是指TLAB的內(nèi)存大小桑逝。
- _refill_waste_limit 是指最大的浪費空間棘劣,假設(shè)為5KB,通俗一點講就是:
- 1楞遏、假如當前TLAB已經(jīng)分配96KB茬暇,還剩下4KB,但是現(xiàn)在new了一個對象需要6KB的空間橱健,顯然TLAB的內(nèi)存不夠了,這時可以簡單的重新申請一個TLAB沙廉,原先的TLAB交給Eden管理拘荡,這時只浪費4KB的空間,在_refill_waste_limit 之內(nèi)撬陵。
- 2珊皿、假如當前TLAB已經(jīng)分配90KB,還剩下10KB巨税,現(xiàn)在new了一個對象需要11KB蟋定,顯然TLAB的內(nèi)存不夠了,這時就不能簡單的拋棄當前TLAB草添,這11KB會被安排到Eden區(qū)進行申請驶兜。
- 在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抄淑,實現(xiàn)如下
- 1屠凶、設(shè)置當前TLAB的_desired_size,該值通過initial_desired_size()方法計算肆资;
- 字段_desired_size的計算過程分析
- TLABSize在argument模塊中默認會設(shè)置大小為 256 * K矗愧,也可以通過JVM參數(shù)選擇進行設(shè)置,不過即使設(shè)置了也會和一個最大值max_size進行比較郑原,然后取一個較小值唉韭,其中max_size計算如下:
- 這里明確說明了TLAB的大小不能超過可以容納 int[Integer.MAX_VALUE]
- TLABSize在argument模塊中默認會設(shè)置大小為 256 * K矗愧,也可以通過JVM參數(shù)選擇進行設(shè)置,不過即使設(shè)置了也會和一個最大值max_size進行比較郑原,然后取一個較小值唉韭,其中max_size計算如下:
- 字段_desired_size的計算過程分析
- 2、設(shè)置當前TLAB的_refill_waste_limit犯犁,該值通過initial_refill_waste_limit()方法計算属愤;
- 字段_refill_waste_limit計算分析
size_t initial_refill_waste_limit() { return desired_size() / TLABRefillWasteFraction(默認64); }
- 字段_refill_waste_limit計算分析
- 3、初始化一些統(tǒng)計字段栖秕,如_number_of_refills春塌、_fast_refill_waste、_slow_refill_waste簇捍、_gc_waste和_slow_allocations只壳;
- 1屠凶、設(shè)置當前TLAB的_desired_size,該值通過initial_desired_size()方法計算肆资;
- 內(nèi)存分配
- 對象的內(nèi)存分配入口為instanceKlass::allocate_instance(),通過CollectedHeap::obj_allocate()方法在堆內(nèi)存上進行分配
- 其中common_mem_allocate_init()方法最終會調(diào)用CollectedHeap::common_mem_allocate_noinit()方法暑塑,實現(xiàn)如下
- 根據(jù)UseTLAB的值吼句,決定是否在TLAB上進行內(nèi)存分配,如果JVM參數(shù)中沒有手動取消UseTLAB事格,會調(diào)用allocate_from_tlab()在TLAB上嘗試分配惕艳,因為可能存在分配失敗的情況,比如TLAB容量不足驹愚,看下allocate_from_tlab()的實現(xiàn):
- 從上述實現(xiàn)可以看出远搪,先會嘗試調(diào)用ThreadLocalAllocBuffer 的 allocate 方法,如果返回為空逢捺,再執(zhí)行allocate_from_tlab_slow()進行分配谁鳍,從這個方法命名可以看出這是比較慢的分配路徑。
- ThreadLocalAllocBuffer 的 allocate 方法實現(xiàn)如下:
- 通過判斷當前TLAB的剩余容量是否大于需要分配的大小劫瞳,來決定分配結(jié)果倘潜,如果當前剩余容量不夠,就返回NULL志于,表示分配失敗涮因。
- ThreadLocalAllocBuffer 的 allocate 方法實現(xiàn)如下:
- 慢分配allocate_from_tlab_slow()實現(xiàn)如下
- 1、如果當前TLAB的剩余容量大于浪費閾值伺绽,就不在當前TLAB分配养泡,直接在共享的Eden區(qū)進行分配嗜湃,并且記錄慢分配的內(nèi)存大小瓤荔;
- 2净蚤、如果剩余容量小于浪費閾值,說明可以丟棄當前TLAB了输硝;
- 3今瀑、通過allocate_new_tlab()方法,從eden新分配一塊裸的空間出來(這一步可能會失數惆选)橘荠,如果失敗說明eden沒有足夠空間來分配這個新TLAB,就會觸發(fā)YGC郎逃。
- 申請好新的TLAB內(nèi)存之后哥童,執(zhí)行TLAB的fill()方法,實現(xiàn)如下:
- 1褒翰、統(tǒng)計refill的次數(shù)
- 2贮懈、初始化重新申請到的內(nèi)存塊
- 3、將當前TLAB拋棄(retire)掉优训,這個過程中最重要的動作是將TLAB末尾尚未分配給Java對象的空間(浪費掉的空間)分配成一個假的“filler object”(目前是用int[]作為filler object)朵你。這是為了保持GC堆可以線性parse(heap parseability)用的。
- 從上述實現(xiàn)可以看出远搪,先會嘗試調(diào)用ThreadLocalAllocBuffer 的 allocate 方法,如果返回為空逢捺,再執(zhí)行allocate_from_tlab_slow()進行分配谁鳍,從這個方法命名可以看出這是比較慢的分配路徑。
- 根據(jù)UseTLAB的值吼句,決定是否在TLAB上進行內(nèi)存分配,如果JVM參數(shù)中沒有手動取消UseTLAB事格,會調(diào)用allocate_from_tlab()在TLAB上嘗試分配惕艳,因為可能存在分配失敗的情況,比如TLAB容量不足驹愚,看下allocate_from_tlab()的實現(xiàn):
- 什么是TLAB
-
RednaxelaFX揣非、你假笨關(guān)于TLAB的一些分析總結(jié)
- TLAB refill包括下述幾個動作:
- 將當前TLAB拋棄(retire)掉抡医。這個過程中最重要的動作是將TLAB末尾尚未分配給Java對象的空間(浪費掉的空間)分配成一個假的“filler object”(目前是int[]作為filler object)。這是為了保持GC堆可以線性parse(heap parseability)用的早敬。
- 從eden新分配一塊裸的空間出來(這一步可能會失敿缮怠)
- 將新分配的空間范圍記錄到ThreadLocalAllocBuffer里TLAB refill不成功(eden沒有足夠空間來分配這個新TLAB)就會觸發(fā)YGC。
- 注意“撞上”指的是在某次分配請求中搞监,top + new_obj_size >= end 的情況水孩,也就是說在被判定“撞上”的時候,top 常常離 end 還有一段距離琐驴,只是這之間的空間不足以滿足新對象的分配請求 new_obj_size 的大小俘种。這意味著在觸發(fā)TLAB refill的時候,有可能會浪費掉位于該TLAB末尾的一部分空間:該TLAB已經(jīng)占用了這塊空間所以其它線程無法在這里分配Java對象棍矛,但該TLAB要refill的話它自己也不會在這塊空間繼續(xù)分配Java對象安疗,從應(yīng)用層面看這塊空間就浪費了抛杨。
- 每次分配TLAB的大小不是固定的够委,而是每個線程根據(jù)該線程啟動開始到現(xiàn)在的歷史統(tǒng)計信息來自己單獨調(diào)整的。如果一個線程上跑的代碼的內(nèi)存分配速率非常高怖现,則該線程會選擇使用更大的TLAB以達到均攤同步開銷的效果茁帽,反之亦然玉罐;同時它還會統(tǒng)計浪費比例,并且將其放入計算新TLAB大小的考慮因素當中潘拨,把浪費比例控制在一定范圍內(nèi)吊输。
- GC很重要的一點是對heap parseability的依賴。GC做某些需要線性掃描堆里的對象的操作時铁追,需要知道堆里哪些地方有對象而哪些地方是空洞季蚂。一種辦法是使用外部數(shù)據(jù)結(jié)構(gòu),例如freelist或者allocation BitMap之類來記錄哪里有空洞琅束;另一種辦法是把空洞部分也假裝成有對象扭屁,這樣GC在線性遍歷時會看到一個“對象總是連續(xù)分配的”的假象,就可以以統(tǒng)一的方式來遍歷:遍歷到一個對象時涩禀,通過其對象頭記錄的信息找出該對象的大小料滥,然后跳到該大小之后就可以找到下一個對象的對象頭,依此類推艾船。HotSpot選擇的是后者的做法葵腹,假裝成有對象的這種東西就叫做filler object(填充對象)。
- PLAB也是個非常有趣的東西屿岂,提到TLAB的話也可以順帶說下PLAB践宴。HotSpot里的TLAB是只在eden里分配的,用于給新建的小對象用雁社。(本來其實也有考慮讓TLAB在任意位置分配浴井,但后來沒實現(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