从javaagent迁移到JVMTI:我们的经验
當您需要從JVM內部收集數據時,您會發現自己很危險地接近Java虛擬機內部進行工作。 幸運的是,有一些方法可以避免被JVM實現細節所困擾。 Java之父沒有給您提供過兩個漂亮的工具供您使用。
在這篇文章中,我們將說明兩種方法之間的差異,并說明為什么我們最近移植了算法的重要部分。
Java代理
第一種選擇是使用java.lang.instrument接口。 這種方法使用-javaagent啟動參數將監視代碼加載到JVM本身。 作為Java的全部選擇,如果您的背景是Java開發,那么javaagents往往是首選的方法。 說明您如何從該方法中受益的最佳方法是通過示例。
讓我們創建一個真正簡單的代理,該代理將負責監視代碼中的所有方法調用。 當代理面對方法調用時,它將調用記錄到標準輸出流中:
import org.objectweb.asm.*;public class MethodVisitorNotifyOnMethodEntry extends MethodVisitor {public MethodVisitorNotifyOnMethodEntry(MethodVisitor mv) {super(Opcodes.ASM4, mv);mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(MethodVisitorNotifyOnMethodEntry.class), "callback", "()V");}public static void callback() {System.out.println("Method called!"); } }您可以使用上面的示例,將其打包為javaagent(本質上是一個帶有特殊MANIFEST.MF的小JAR文件),然后使用類似于以下內容的代理的premain()方法啟動它:
java -javaagent:path-to/your-agent.jar com.yourcompany.YourClass啟動后,您會看到一堆“方法調用!” 日志文件中的消息。 就我們而言,僅此而已。 但是這個概念很強大,尤其是與上面的示例中的字節碼檢測工具(例如ASM或cgLib)結合使用時。
為了使示例易于理解,我們跳過了一些細節。 但這是相對簡單的-使用java.lang.instrument包時,您首先編寫自己的代理類,并實現public static void premain(String agentArgs,Instrumentation inst) 。 然后,您需要向inst.addTransformer注冊您的ClassTransformer 。 您很可能希望避免直接對類字節碼進行操作,因此可以使用一些字節碼操作庫,例如我們所使用的示例中的ASM。 有了它,您只需要實現幾個接口– ClassVisitor (為簡便起見,略過)和MethodVisitor。
JVMTI
第二種方法最終將帶您進入JVMTI。 JVM工具接口( JVM TI )是標準的本機API,允許本機庫捕獲事件并控制Java虛擬機。 對JVMTI的訪問通常打包在稱為代理的特定庫中。
下面的示例演示了與javaagent部分相同的回調注冊,但是這次將其實現為JVMTI調用:
void JNICALL notifyOnMethodEntry(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method) {fputs("method was called!\n", stdout); }int prepareNotifyOnMethodEntry(jvmtiEnv *jvmti) {jvmtiError error;jvmtiCapabilities requestedCapabilities, potentialCapabilities;memset(&requestedCapabilities, 0, sizeof(requestedCapabilities));if((error = (*jvmti)->GetPotentialCapabilities(jvmti, &potentialCapabilities)) != JVMTI_ERROR_NONE) return 0;if(potentialCapabilities.can_generate_method_entry_events) {requestedCapabilities.can_generate_method_entry_events = 1;}else {//not possible on this JVMreturn 0;}if((error = (*jvmti)->AddCapabilities(jvmti, &requestedCapabilities)) != JVMTI_ERROR_NONE) return 0;jvmtiEventCallbacks callbacks;memset(&callbacks, 0, sizeof(callbacks));callbacks.MethodEntry = notifyOnMethodEntry;if((error = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks))) != JVMTI_ERROR_NONE) return 0;if((error = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, (jthread)NULL)) != JVMTI_ERROR_NONE) return 0;return 1; }兩種方法之間存在一些差異。 例如,通過JVMTI,您可以獲得比代理更多的信息。 但是,兩者之間最關鍵的區別在于加載機制。 在將Instrumentation代理加載到堆內部時,它們由同一JVM進行管理。 JVMTI代理不受JVM規則支配,因此不受JVM內部組件(如GC或運行時錯誤處理)的影響。 這意味著什么,最好通過我們自己的經驗來解釋。
從-javaagent遷移到JVMTI
三年前,當我們開始構建內存泄漏檢測器時,我們并沒有過多地關注這些方法的優缺點。 我們毫不猶豫地將解決方案實現為-javaagent 。
多年來,我們已經開始理解含義。 其中一些不太令人滿意,因此在我們的最新版本中,我們將內存泄漏檢測機制的重要部分移植到了本機代碼。 是什么使我們跳到這樣的結論?
首先-當駐留在堆中時,您需要將自己容納在應用程序自己的內存結構旁邊。 通過痛苦的經驗中學到的東西本身就可能導致問題。 當您的應用程序已將堆填滿到最大程度時,您最后需要做的是一個內存泄漏檢測器,它似乎只會加快OutOfMemoryError的到達速度。
但是增加的堆空間減少了困擾我們的弊端。 真正的問題與以下事實有關:使用受監視的應用程序本身正在使用的同一垃圾收集器清理了我們的數據結構。 這導致更長或更頻繁的GC暫停。
盡管大多數應用程序不介意我們為堆消耗添加的幾個額外的百分點,但我們了解到,需要消除對Full GC暫停產生不可預測的影響。
更糟糕的是– Plumbr的工作方式是監視所有對象的創建和集合。 監視某物時,您需要保持跟蹤。 跟蹤往往會創建對象。 創建的對象將有資格使用GC。 現在,當您監視的是GC時,您剛剛創建了一個惡性循環-收集到更多的對象的垃圾,創建的監視器越多,觸發的GC運行頻率越高,等等。
跟蹤對象時,JVMTI會通知我們有關對象死亡的信息。 但是,JVMTI不允許在這些回調期間使用JNI。 因此,如果我們使用Java保留有關跟蹤對象的統計信息,則當我們收到更改通知時,不可能立即更新統計信息。 相反,當我們知道JVM處于正確狀態時,需要將更改緩存并應用。 這造成了不必要的復雜性和更新實際統計數據的延遲。
翻譯自: https://www.javacodegeeks.com/2014/03/migrating-from-javaagent-to-jvmti-our-experience.html
總結
以上是生活随笔為你收集整理的从javaagent迁移到JVMTI:我们的经验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跟踪异常–第4部分– Spring的邮件
- 下一篇: 目录权限操作命令(目录权限 linux)