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