垃圾收集中的代际差异
去年,我一直在幫助新興公司Instana創(chuàng)建一個(gè)Java代理,該代理可跟蹤Java應(yīng)用程序中的執(zhí)行情況。 收集并結(jié)合此執(zhí)行數(shù)據(jù)以生成用戶(hù)請(qǐng)求以及系統(tǒng)所有者半球內(nèi)服務(wù)之間的最終通信的跟蹤。 這樣,可以可視化非結(jié)構(gòu)化通信,從而顯著簡(jiǎn)化了由多個(gè)交互服務(wù)組成的分布式系統(tǒng)的操作。
為了生成這些跟蹤,Java代理重寫(xiě)了讀取外部請(qǐng)求或發(fā)起外部請(qǐng)求的所有代碼。 顯然,需要記錄這些進(jìn)入或離開(kāi)系統(tǒng)的入口和出口,此外,還需要交換元數(shù)據(jù)以跨系統(tǒng)唯一地標(biāo)識(shí)請(qǐng)求。 例如,當(dāng)跟蹤HTTP請(qǐng)求時(shí),代理會(huì)添加一個(gè)包含唯一ID的標(biāo)頭,然后由接收服務(wù)器記錄該ID作為請(qǐng)求來(lái)源的證明。 從廣義上講,它與Zipkin建模的相似,但是不需要用戶(hù)更改代碼。
在最簡(jiǎn)單的情況下,這種跟蹤很容易實(shí)現(xiàn)。 多虧了我的庫(kù)Byte Buddy ,該庫(kù)可以完成繁重的工作,所有注入的代碼都是用普通的舊Java編寫(xiě)的 ,然后在運(yùn)行時(shí)使用Java工具API復(fù)制到相關(guān)的方法中。 例如,在檢測(cè)Servlet時(shí),我們知道只要調(diào)用service方法就進(jìn)入JVM。 我們也知道,當(dāng)此完全相同的方法退出時(shí),條目已完成。 因此,只需在方法的開(kāi)頭和結(jié)尾添加一些代碼,以將任何此類(lèi)條目記錄到VM進(jìn)程中即可。 我的大部分工作是瀏覽許多Java庫(kù)和框架,以增加對(duì)其通信方式的支持。 從Akka到Zookeeper,在過(guò)去的一年中,我遍歷了整個(gè)Java生態(tài)系統(tǒng)。 我什至必須為所有服務(wù)器編寫(xiě)EJB! 而且我不得不理解Sun的CORBA實(shí)現(xiàn)。 (劇透:沒(méi)有道理。)
但是,在跟蹤異步執(zhí)行時(shí),事情確實(shí)很快變得更加困難。 如果一個(gè)線(xiàn)程接收到一個(gè)請(qǐng)求,但另一個(gè)線(xiàn)程內(nèi)部對(duì)該請(qǐng)求進(jìn)行了響應(yīng),則僅跟蹤入口和出口不再足夠。 因此,我們的代理還需要跟蹤通過(guò)線(xiàn)程池, 派生聯(lián)接任務(wù)或自定義并發(fā)框架進(jìn)行的并發(fā)系統(tǒng)中的所有上下文切換。 同樣,調(diào)試異步執(zhí)行很困難,這對(duì)我們來(lái)說(shuō)也是很多工作。 我認(rèn)為我花在處理并發(fā)上的時(shí)間與記錄入口和出口所花的時(shí)間一樣多。
對(duì)垃圾收集的影響
但是這一切如何影響垃圾回收? 在實(shí)現(xiàn)性能監(jiān)視器時(shí),面臨著一個(gè)權(quán)衡,即在解釋虛擬機(jī)的工作與通過(guò)這樣做導(dǎo)致該機(jī)器的工作之間進(jìn)行權(quán)衡。 盡管大部分處理是在代理程序向其報(bào)告數(shù)據(jù)的監(jiān)視器后端中完成的,但我們必須在與受監(jiān)視的應(yīng)用程序共享的Java進(jìn)程中做最少的工作。 您已經(jīng)可以猜到:通過(guò)分配對(duì)象,我們不可避免地會(huì)對(duì)VM的垃圾回收產(chǎn)生影響。 幸運(yùn)的是,現(xiàn)代垃圾收集算法正在做得很好,并且通過(guò)主要避免對(duì)象分配以及通過(guò)自適應(yīng)采樣我們的跟蹤工作,我們的代碼更改的影響對(duì)于絕大多數(shù)用戶(hù)而言可以忽略不計(jì)。 理想情況下,我們只消耗一些未使用的處理器周期來(lái)完成工作。 實(shí)際上,很少有應(yīng)用程序能夠充分發(fā)揮其全部處理潛力,而我們很樂(lè)意抓住其中的一小部分。
編寫(xiě)垃圾回收友好的應(yīng)用程序通常不太困難。 顯而易見(jiàn),避免垃圾的最簡(jiǎn)單方法是完全避免對(duì)象分配。 但是,對(duì)象分配本身也不錯(cuò)。 分配內(nèi)存是一項(xiàng)相當(dāng)便宜的操作,并且由于任何處理器都擁有自己的分配緩沖區(qū)( 即所謂的TLAB),因此在從線(xiàn)程中僅分配一點(diǎn)內(nèi)存時(shí),我們不會(huì)強(qiáng)加不必要的同步。 如果對(duì)象僅存在于方法范圍內(nèi),則JVM甚至可以完全擦除對(duì)象分配,就好像將對(duì)象的字段直接放入堆棧中一樣。 但是,即使沒(méi)有這種逃逸分析 ,也可以通過(guò)稱(chēng)為“ 年輕一代”收集器的特殊垃圾收集器來(lái)捕獲短命對(duì)象,該收集器可以非常有效地進(jìn)行處理。 坦白說(shuō),這就是我大多數(shù)對(duì)象的結(jié)局,因?yàn)槲医?jīng)常將代碼的可讀性視為逃避分析所提供的細(xì)微改進(jìn)。 目前,逃逸分析Swift觸及了邊界。 但是,我希望將來(lái)的HotSpots能夠得到改進(jìn),以在不更改我的代碼的情況下獲得兩全其美。 手指交叉!
在編寫(xiě)Java程序時(shí),我通常不會(huì)考慮對(duì)垃圾回收的影響,但是上述準(zhǔn)則往往會(huì)體現(xiàn)在我的代碼中。 對(duì)于我們大多數(shù)代理商而言,這一直很好。 我們正在運(yùn)行大量示例應(yīng)用程序和集成測(cè)試,以確保我們的代理具有良好的行為,并且在運(yùn)行示例時(shí),我也會(huì)關(guān)注GC。 在我們現(xiàn)代,使用飛行記錄器和JIT手表等工具進(jìn)行性能分析變得相當(dāng)容易。
短命的相對(duì)性
在早期版本的代理中,有一天我注意到一個(gè)觸發(fā)永久收集周期的應(yīng)用程序,如果沒(méi)有它,它就不會(huì)觸發(fā)。 結(jié)果,收集暫停增加了許多。 但是,最終存留在集合中的對(duì)象僅是受監(jiān)視應(yīng)用程序本身的對(duì)象。 但是,由于我們的代理主要是與應(yīng)用程序線(xiàn)程隔離運(yùn)行的,因此起初這對(duì)我來(lái)說(shuō)沒(méi)有任何意義。
深入研究時(shí),我發(fā)現(xiàn)我們對(duì)用戶(hù)對(duì)象的分析觸發(fā)了對(duì)象的其他逃逸,但影響很小。 該應(yīng)用程序已經(jīng)產(chǎn)生了大量的對(duì)象,主要是通過(guò)使用NIO和使用派生聯(lián)接池。 后一框架的共同之處在于它們依賴(lài)于許多短期對(duì)象的分配。 例如,fork-join任務(wù)通常將自身拆分為多個(gè)子任務(wù),這些子任務(wù)重復(fù)此過(guò)程,直到每個(gè)任務(wù)的有效載荷足夠小以至于可以直接計(jì)算。 每個(gè)這樣的任務(wù)都由一個(gè)有狀態(tài)的對(duì)象表示。 活躍的fork聯(lián)接池每分鐘可以產(chǎn)生數(shù)百萬(wàn)個(gè)此類(lèi)對(duì)象。 但是,由于任務(wù)計(jì)算速度很快,因此代表對(duì)象可以快速進(jìn)行收集,并因此被年輕的收集器捕獲。
那么這些對(duì)象是如何突然變成永久性收藏的呢? 這時(shí),我正在制作一個(gè)新的縫合工具的原型,以跟蹤此類(lèi)fork聯(lián)接任務(wù)之間的上下文切換。 遵循fork聯(lián)接任務(wù)的路徑并非易事。 派生聯(lián)接池的每個(gè)工作線(xiàn)程都會(huì)應(yīng)用工作竊取,并且可能會(huì)將任務(wù)從其他任何任務(wù)的隊(duì)列中移出。 同樣,任務(wù)可能在完成時(shí)向其父任務(wù)提供反饋。 結(jié)果,跟蹤任務(wù)的擴(kuò)展和交互是一個(gè)相當(dāng)復(fù)雜的過(guò)程,還因?yàn)榇嬖谒^的連續(xù)線(xiàn)程,其中單個(gè)任務(wù)可能會(huì)在幾毫秒內(nèi)將作業(yè)反彈到數(shù)百個(gè)線(xiàn)程。 我想出了一個(gè)相當(dāng)優(yōu)雅的解決方案,該解決方案依賴(lài)于許多短期對(duì)象的分配,每當(dāng)將任務(wù)回溯到源頭時(shí),這些對(duì)象就會(huì)以突發(fā)方式分配。 事實(shí)證明,這些爆發(fā)本身觸發(fā)了許多年輕的收藏。
這就是我沒(méi)有考慮的問(wèn)題:每個(gè)年輕一代的收集都會(huì)增加此時(shí)不符合垃圾收集條件的任何對(duì)象的使用期限。 一個(gè)對(duì)象不會(huì)隨時(shí)間而老化,而是取決于觸發(fā)的年輕集合的數(shù)量。 并非所有的收集算法都適用,但對(duì)于其中的許多算法(例如HotSpot的所有默認(rèn)收集器 )而言并非如此。 通過(guò)觸發(fā)這么多集合,代理可以線(xiàn)程化受監(jiān)視應(yīng)用程序的“過(guò)早成熟”的對(duì)象,盡管這些對(duì)象與代理的對(duì)象無(wú)關(guān)。 以某種方式,運(yùn)行代理“過(guò)早地成熟了”目標(biāo)應(yīng)用程序的對(duì)象。
解決問(wèn)題
起初我確實(shí)不知道該如何解決。 最后,無(wú)法告訴垃圾回收器分別處理“您的對(duì)象”。 只要代理線(xiàn)程以比主機(jī)進(jìn)程更快的速度分配壽命較短的對(duì)象,它就會(huì)破壞原始對(duì)象進(jìn)入租期集合,從而增加垃圾回收暫停。 為了避免這種情況,因此我開(kāi)始合并我正在使用的對(duì)象。 通過(guò)池化,我很快將自己的對(duì)象成熟到了永久性集合中,垃圾回收行為恢復(fù)了其正常狀態(tài)。 傳統(tǒng)上,使用池來(lái)避免分配的費(fèi)用,這在我們這個(gè)時(shí)代變得很便宜。 我重新發(fā)現(xiàn)了它,以消除“外來(lái)進(jìn)程”對(duì)垃圾回收的影響,而這僅占用了幾千字節(jié)的內(nèi)存。
我們的跟蹤器已經(jīng)在其他地方合并對(duì)象。 例如,我們將入口和出口表示為線(xiàn)程局部值,其中包含一堆原始值,這些變量在不分配單個(gè)對(duì)象的情況下進(jìn)行了變異。 盡管這種可變的,通常是過(guò)程式的和對(duì)象池式的編程已不再流行,但事實(shí)證明它對(duì)性能非常友好。 最后,使位更接近處理器的實(shí)際操作。 通過(guò)使用固定大小的預(yù)分配數(shù)組而不是不可變集合,我們?yōu)槲覀児?jié)省了很多往返內(nèi)存的時(shí)間,同時(shí)還保留了僅包含在少數(shù)高速緩存行中的狀態(tài)。
這是“現(xiàn)實(shí)世界”的問(wèn)題嗎?
您可能會(huì)認(rèn)為這是一個(gè)相當(dāng)具體的問(wèn)題,大多數(shù)人無(wú)需擔(dān)心。 但實(shí)際上,我描述的問(wèn)題適用于大量Java應(yīng)用程序。 例如,在應(yīng)用程序容器內(nèi),我們通常在單個(gè)Java進(jìn)程中部署多個(gè)應(yīng)用程序。 與上述情況一樣,垃圾收集算法不會(huì)按應(yīng)用程序?qū)?duì)象進(jìn)行分組,因?yàn)樗鼪](méi)有這種部署模型的概念。 因此,共享一個(gè)容器的兩個(gè)隔離應(yīng)用程序進(jìn)行的對(duì)象分配確實(shí)會(huì)干擾彼此的預(yù)期收集模式。 如果每個(gè)應(yīng)用程序依靠其對(duì)象而早逝,則堆的共享會(huì)導(dǎo)致短期生存期的強(qiáng)烈相關(guān)性。
我不是微服務(wù)的擁護(hù)者。 實(shí)際上,我認(rèn)為對(duì)于大多數(shù)應(yīng)用程序來(lái)說(shuō),這是一個(gè)壞主意。 在我看來(lái),除非有充分的技術(shù)理由,否則理想情況下應(yīng)該將只能存在于交互中的例程一起部署。 即使隔離的應(yīng)用程序簡(jiǎn)化了開(kāi)發(fā),您也可以快速付出運(yùn)營(yíng)成本。 我只是提到這一點(diǎn),以避免對(duì)上述經(jīng)驗(yàn)的道德產(chǎn)生誤解。
這種經(jīng)驗(yàn)告訴我的是,如果一個(gè)應(yīng)用程序是異構(gòu)的,則在單個(gè)Java進(jìn)程中部署多個(gè)應(yīng)用程序可能不是一個(gè)好主意。 例如,在與Web服務(wù)器并行運(yùn)行批處理時(shí),應(yīng)考慮在各自的進(jìn)程中運(yùn)行它們,而不是將它們都部署在同一容器中。 通常,批處理過(guò)程以與Web服務(wù)器不同的速率分配對(duì)象。 但是,許多企業(yè)框架仍在宣傳解決此類(lèi)問(wèn)題的多合一解決方案,這些解決方案不應(yīng)共享一個(gè)開(kāi)始的過(guò)程。 在2016年,附加進(jìn)程的開(kāi)銷(xiāo)通常不是問(wèn)題,并且由于內(nèi)存便宜,因此請(qǐng)升級(jí)服務(wù)器而不是共享堆。 否則,您可能最終會(huì)獲得孤立開(kāi)發(fā),運(yùn)行和測(cè)試應(yīng)用程序時(shí)無(wú)法預(yù)期的收集模式。
翻譯自: https://www.javacodegeeks.com/2016/10/generational-disparity-garbage-collection.html
總結(jié)
以上是生活随笔為你收集整理的垃圾收集中的代际差异的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: xaas_从XaaS到Java EE –
- 下一篇: JDK 9 @不建议使用的注释增强功能