如何低開銷的監控JVM對象分配及分配對象的線程
概要
提供一種低開銷的Java堆分配采樣方式,可通過JVMTI訪問。
目標
提供一種從JVM獲取Java對象堆分配信息的方法:
- 開銷足夠低,可以在默認情況下連續啟用,
- 可以通過定義良好的編程接口訪問,
- 可以對所有的分配進行抽樣(即,不局限于一個特定堆區域中的分配或以一種特定方式分配的分配),
- 可以以一種與實現無關的方式定義(即,不依賴于任何特定的GC算法或VM實現),以及
- 可以提供有關活的和死的Java對象的信息。
動機
用戶非常需要理解堆的內容。糟糕的堆管理可能會導致堆耗盡和GC抖動等問題。因此,人們開發了許多工具來允許用戶自省他們的堆,例如Java Flight Recorder、jmap、YourKit和VisualVM工具。
大多數現有工具缺少的一個信息是特定分配的調用站點。堆轉儲和堆直方圖不包含此信息。此信息對于調試內存問題非常重要,因為它告訴開發人員代碼中發生特定(特別糟糕的)分配的確切位置。
目前有兩種方法從熱點獲取這些信息:
- 首先,您可以使用字節碼重寫器(例如Allocation Instrumenter)來檢測應用程序中的所有分配。然后,您可以讓插裝進行堆棧跟蹤(當您需要時)。
- 其次,您可以使用Java Flight Recorder,它在TLAB重新填充和直接分配到老一代時進行堆棧跟蹤。這樣做的缺點是:a)它綁定到特定的分配實現(TLABs),并且錯過了不符合該模式的分配;B)它不允許用戶自定義采樣間隔;c)它只記錄分配,所以你無法區分活對象和死對象。
該建議通過提供可擴展的JVMTI接口來緩解這些問題,該接口允許用戶定義采樣間隔并返回一組活動堆棧跟蹤。
描述
新的JVMTI事件和方法
這里提出的面向用戶的堆采樣特性API由JVMTI的擴展組成,該擴展允許進行堆分析。以下系統依賴于提供回調的事件通知系統,例如:
說明:
- thread是分配對象的線程
- object是對采樣對象的引用
- object_klass是jobject的類
- size是分配的大小
新的API還包括一個新的JVMTI方法:
其中sampling_interval是兩次采樣之間分配的平均字節數。該方法的規格為:
- 如果不為零,采樣間隔將被更新,并將用sampling_interval字節的新平均采樣間隔發送回調給用戶
- 例如,如果用戶希望每兆字節采樣一次,則sampling_interval將是1024 * 1024。
- 如果將0傳遞給方法,采樣器在考慮到新的間隔后對每個分配進行采樣,這可能需要一定數量的分配
注意,采樣間隔是不精確的。每次出現一個樣本時,在下一個樣本被選擇之前的字節數將是給定平均間隔的偽隨機。這是為了避免抽樣偏差;例如,如果相同的分配每512KB發生一次,512KB采樣間隔將始終對相同的分配進行采樣。因此,雖然采樣間隔并不總是選擇的間隔,但在大量的樣本之后,它會趨向于它。
用例示例
要啟用此功能,用戶將使用通常的事件通知調用來操作:
該事件將在分配初始化并正確設置時發送,因此略晚于實際代碼執行分配之后。缺省情況下,平均采樣間隔為512KB。
啟用采樣事件系統的最低要求是使用JVMTI_ENABLE和事件類型
JVMTI_EVENT_SAMPLED_OBJECT_ALLOC調用SetEventNotificationMode。要修改采樣間隔,用戶調用SetHeapSamplingInterval方法。
禁用方式,
禁用事件通知并自動禁用采樣器。
通過SetEventNotificationMode再次調用采樣器將使用當前設置的采樣間隔重新啟用采樣器(默認為512KB或用戶通過SetHeapSamplingInterval傳遞的最后一個值)。
新功能
為了保護新特性并使其成為VM實現的可選特性,在jvmtiCapabilities中引入了名為
can_generate_sampled_object_alloc_events的新功能。
全局/線程級采樣
使用通知系統提供了一種僅為特定線程發送事件的直接方法。這是通過SetEventNotificationMode完成的,并提供第三個參數,其中包含要修改的線程。
完整的例子
下面的部分提供代碼片段來演示采樣器的API。首先,啟用功能和事件通知:
禁用采樣器(禁用事件和采樣器):
要重新啟用1024 * 1024字節采樣間隔的采樣器,需要一個簡單的調用來啟用事件:
抽樣分配的用戶存儲
當事件生成時,回調可以使用JVMTI GetStackTrace方法捕獲堆棧跟蹤。回調獲得的jobject引用也可以包裝成JNI弱引用,以幫助確定對象何時已被垃圾收集。這種方法允許用戶收集關于采樣對象的數據,以及仍然被認為是活動的對象的數據,這是了解作業行為的好方法。
例如,可以這樣做:
如果internal_storage是一個可以處理采樣對象的數據結構,請考慮是否需要清理任何垃圾收集的樣本,等等。該實現的內部是特定于使用的,超出了這個JEP的范圍。
采樣間隔可以用作減少分析開銷的一種手段。使用512KB的采樣間隔,開銷應該足夠低,用戶可以合理地在默認情況下打開系統。
實現細節
目前的原型和實現證明了該方法的可行性。它包括五個部分:
- 由于ThreadLocalAllocationBuffer (TLAB)結構中字段名稱的更改,導致架構相關的更改。這些更改是最小的,因為它們只是名稱更改。
- TLAB結構增加了一個新的allocation_end指針,以補充現有的結束指針。如果禁用采樣,則兩個指針始終相等,代碼將像以前一樣執行。如果啟用了采樣,end將被修改為請求下一個采樣點的位置。然后,任何快速路徑都會“認為”TLAB在此時已經滿了,然后沿著慢路徑走,這在(3)中解釋過。
- gc/shared/collectedHeap代碼被更改,因為它被用作分配慢路徑的入口點。當TLAB被認為已滿(因為分配已傳遞結束指針)時,代碼進入collectedHeap并嘗試分配一個新的TLAB。此時,TLAB將恢復到其原始大小,并嘗試進行分配。如果分配成功,代碼對分配進行采樣,然后返回。如果沒有,則意味著TLAB的分配已經結束,需要一個新的TLAB。代碼路徑繼續其對新TLAB的正常分配,并確定該分配是否需要示例。如果分配被認為對TLAB來說太大,系統也會對分配進行抽樣,從而覆蓋TLAB分配內和TLAB分配外進行抽樣。
- 當請求一個示例時,堆棧上有一個收集器對象設置在一個安全的位置,用于將信息發送到本機代理。收集器跟蹤采樣分配,并在銷毀自己的幀時向代理發送回調。該機制確保對象被正確初始化。
- 如果JVMTI代理為SampledObjectAlloc事件注冊了回調,則該事件將被觸發,并且它將獲得抽樣分配。在libHeapMonitorTest.c文件中可以找到一個示例實現,該文件用于JTreg測試。
選擇
對于這個JEP中提出的系統,有多種替代方案。介紹中已經介紹了兩個:Flight Recorder提供了一個有趣的替代方案。這個實現提供了幾個優點。首先,JFR不允許設置抽樣大小或提供回調。其次,當緩沖區耗盡時,JFR使用緩沖區系統可能導致分配丟失。最后,JFR事件系統沒有提供跟蹤已被垃圾收集的對象的方法,這意味著不可能使用它來提供有關活動對象和垃圾收集對象的信息。
另一種替代方法是使用ASM的字節碼插裝。它的開銷讓人望而卻步,不是一個可行的解決方案。
這個JEP向JVMTI添加了一個新特性,JVMTI是用于各種開發和監視工具的重要API/框架。有了它,JVMTI代理可以使用低開銷的堆分析API以及其他JVMTI功能,這為工具提供了極大的靈活性。例如,由代理決定是否需要在每個事件點收集堆棧跟蹤。
測試
JTreg框架中針對該特性有16個測試:使用多個線程打開/關閉,同時分配多個線程,測試數據是否以正確的間隔采樣,以及收集的堆棧是否反映正確的程序信息。
風險和假設
禁用該特性不會造成性能損失或風險。沒有啟用系統的用戶不會感知到性能差異。
但是,啟用該特性會有潛在的性能/內存損失。在最初的原型實現中,開銷是最小的(<2%)。這使用了一個更重量級的機制來修改JIT代碼。在這里給出的最終版本中,系統依賴于TLAB代碼,并且不應該經歷這種回歸。
目前對Dacapo基準測試的評估顯示開銷為:
- 禁用時為0%
- 1%,當以默認的512KB間隔啟用該特性,但不執行回調動作(即SampledAllocEvent方法為空,但已注冊到JVM)。
- 3%開銷,使用抽樣回調,執行簡單的實現來存儲數據(使用測試中的實現)