JVM TI: वर्चुअल मशीन के लिए प्लगइन कैसे बनाया जाए



क्या आप JVM में कुछ उपयोगी सुविधा जोड़ना चाहेंगे? सैद्धांतिक रूप से, प्रत्येक डेवलपर OpenJDK में योगदान दे सकता है, हालांकि, व्यवहार में, हॉटस्पॉट में कोई भी गैर-तुच्छ परिवर्तन पक्ष की ओर से बहुत स्वागत योग्य नहीं हैं, और वर्तमान लघु रिलीज चक्र के साथ भी, JDD उपयोगकर्ताओं को आपकी सुविधा देखने से पहले वर्षों लग सकते हैं।

फिर भी, कुछ मामलों में एक वर्चुअल मशीन की कार्यक्षमता का विस्तार करना संभव है, यहां तक ​​कि इसके कोड को छूने के बिना भी। जेवीएम टूल इंटरफेस, जेवीएम के साथ बातचीत के लिए मानक एपीआई, मदद करता है।

लेख में, मैं ठोस उदाहरणों के साथ दिखाऊंगा कि इसके साथ क्या किया जा सकता है, बताएं कि जावा 9 और 11 में क्या बदलाव आया है, और ईमानदारी से कठिनाइयों के बारे में चेतावनी देता है (बिगाड़ने: मुझे सी ++ से निपटना होगा)।

मैंने JPoint पर इस सामग्री के बारे में भी बात की। यदि आप वीडियो पसंद करते हैं, तो आप वीडियो रिपोर्ट देख सकते हैं।

प्रविष्टि


सामाजिक नेटवर्क Odnoklassniki, जहां मैं एक प्रमुख इंजीनियर के रूप में काम करता हूं, लगभग पूरी तरह से जावा में लिखा गया है। लेकिन आज मैं आपको सिर्फ एक और हिस्से के बारे में बताता हूं, जो पूरी तरह से जावा में नहीं है।

जैसा कि आप जानते हैं, जावा डेवलपर्स के साथ सबसे लोकप्रिय समस्या NullPointerException है। एक बार, पोर्टल पर ड्यूटी पर रहते हुए, मैं उत्पादन में एनपीई के पार भी आया। त्रुटि इस स्टैक ट्रेस की तरह कुछ के साथ थी:



बेशक, स्टैक ट्रेस पर, आप उस स्थान का पता लगा सकते हैं जहां अपवाद कोड में एक विशिष्ट लाइन तक हुआ था। केवल इस मामले में इसने मुझे कोई बेहतर महसूस नहीं कराया, क्योंकि यहां NPE बहुत कुछ मिल सकता है:



यह बहुत अच्छा होगा यदि JVM ने सुझाव दिया कि यह त्रुटि कहाँ थी, उदाहरण के लिए, इस तरह:
java.lang.NullPointerException: Called 'getUsers()' method on null object

लेकिन, दुर्भाग्य से, एनपीई में अब कुछ भी नहीं है। हालांकि वे लंबे समय से इसके लिए पूछ रहे हैं, कम से कम जावा 1.4 के साथ: यह बग 16 साल का हो गया है। समय-समय पर, इस विषय पर अधिक से अधिक बग खोले गए, लेकिन उन्हें "W't Fix" के रूप में हमेशा के लिए बंद कर दिया गया।



ऐसा हर जगह नहीं होता है। एसएपी से वोल्कर सिमोनिस ने बताया कि कैसे उन्होंने लंबे समय तक एसएपी जेवीएम में इस सुविधा को लागू किया और एक से अधिक बार इसकी मदद की। SAP के एक अन्य कर्मचारी ने एक बार फिर से OpenJDK में एक बग प्रस्तुत किया और SAP JVM में एक तंत्र के समान कार्यान्‍वयन करने के लिए स्‍वयंसेवित किया। और, लो और निहारना, इस बार बग को बंद नहीं किया गया था - एक मौका है कि यह सुविधा जेडीके 14 में प्रवेश करेगी।

लेकिन जेडीके 14 कब जारी किया जाएगा, और हम इसे कब स्विच करेंगे? अगर आप यहां और अब समस्या की जांच करना चाहते हैं तो क्या करें?

आप निश्चित रूप से, OpenJDK के अपने कांटे को बनाए रख सकते हैं। NPE रिपोर्टिंग सुविधा अपने आप में इतनी जटिल नहीं है, हम इसे अच्छी तरह से लागू कर सकते हैं। लेकिन एक ही समय में, अपनी खुद की विधानसभा का समर्थन करने की सभी समस्याएं होंगी। यह सुविधा को एक बार लागू करने के लिए बहुत अच्छा होगा, और फिर इसे जेवीएम के किसी भी संस्करण में एक प्लगइन के रूप में कनेक्ट करें। और यह वास्तव में संभव है! जेवीएम में एक विशेष एपीआई (मूल रूप से सभी प्रकार के डिबगर्स और प्रोफाइलर्स के लिए विकसित) है: जेवीएम टूल इंटरफेस।

सबसे महत्वपूर्ण बात, यह एपीआई मानक है। उसके पास एक सख्त विनिर्देश है , और इसके अनुसार एक सुविधा को लागू करते समय, आप यह सुनिश्चित कर सकते हैं कि यह जेवीएम के नए संस्करणों में काम करेगा।

इस इंटरफ़ेस का उपयोग करने के लिए, आपको एक छोटा (या बड़ा, आपके कार्यों के आधार पर) कार्यक्रम लिखने की आवश्यकता है। देशी: आमतौर पर इसे C या C ++ में लिखा जाता है। मानक JDK jdk/include/jvmti.h में jdk/include/jvmti.h हैडर फ़ाइल है जिसे आप शामिल करना चाहते हैं।

कार्यक्रम को एक गतिशील पुस्तकालय में संकलित किया गया है, और जेवीएम की शुरुआत के दौरान -agentpath पैरामीटर द्वारा जुड़ा हुआ है। यह महत्वपूर्ण है कि इसे अन्य समान पैरामीटर के साथ भ्रमित न करें: -javaagent । वास्तव में, जावा एजेंट JVM TI एजेंटों का एक विशेष मामला है। "एजेंट" शब्द के तहत पाठ में आगे देशी एजेंट का मतलब है।

कहां से शुरू करें


आइए अभ्यास में देखें कि सबसे सरल जेवीएम टीआई एजेंट कैसे लिखें, "हेल्लो वर्ल्ड"।

 #include <jvmti.h> #include <stdio.h> JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { jvmtiEnv* jvmti; vm->GetEnv((void**) &jvmti, JVMTI_VERSION_1_0); char* vm_name = NULL; jvmti->GetSystemProperty("java.vm.name", &vm_name); printf("Agent loaded. JVM name = %s\n", vm_name); fflush(stdout); return 0; } 

पहली पंक्ति में मैं एक ही हेडर फ़ाइल शामिल करता हूँ। इसके बाद मुख्य फ़ंक्शन आता है जिसे एजेंट में लागू करने की आवश्यकता होती है: Agent_OnLoad() । वर्चुअल मशीन अपने आप को कॉल करता है जब एजेंट बूट करता है, एक पॉइंटर को JavaVM* ऑब्जेक्ट पर।

इसका उपयोग करके, आप JVM TI पर्यावरण के लिए एक संकेतक प्राप्त कर सकते हैं: jvmtiEnv* । और इसके माध्यम से, बदले में, पहले से ही जेवीएम टीआई-फ़ंक्शन को कॉल करें। उदाहरण के लिए, GetSystemProperty का उपयोग करते हुए , सिस्टम गुण का मान पढ़ें।

अगर अब मैं इस "हैलो वर्ल्ड" को -agentpath , तो संकलित dll फ़ाइल को -agentpath , जावा प्रोग्राम के चलने से पहले हमारे एजेंट द्वारा मुद्रित लाइन कंसोल में दिखाई देगी:



समृद्ध एनपीई


चूंकि हैलो वर्ल्ड सबसे दिलचस्प उदाहरण नहीं है, आइए अपने अपवादों पर वापस जाएं। पूरा एजेंट कोड जो NPE रिपोर्ट को पूरक करता है , GitHub पर है

यदि मैं सभी अपवादों को सूचित करने के लिए वर्चुअल मशीन से पूछना चाहता हूं तो यह Agent_OnLoad() जैसा दिखता है:

 JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) { jvmtiEnv* jvmti; vm->GetEnv((void**) &jvmti, JVMTI_VERSION_1_0); jvmtiCapabilities capabilities = {0}; capabilities.can_generate_exception_events = 1; jvmti->AddCapabilities(&capabilities); jvmtiEventCallbacks callbacks = {0}; callbacks.Exception = ExceptionCallback; jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks)); jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, NULL); return 0; } 

पहले मैं JVM TI को संबंधित क्षमता (can_generate_exception_events) के लिए कहता हूं। हम अलग से क्षमता के बारे में बात करेंगे।

अगला चरण अपवाद घटनाओं की सदस्यता ले रहा है। जब भी JVM अपवादों को फेंकता है (चाहे वे पकड़े गए हों या नहीं), हमारे ExceptionCallback() फ़ंक्शन को कहा जाएगा।

अंतिम चरण सूचनाओं के वितरण को सक्षम करने के लिए SetEventNotificationMode() को कॉल करना है।

ExceptionCallback में, JVM उन सभी चीजों को पास करता है जिन्हें हमें अपवादों को संभालने की आवश्यकता होती है।
 void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, jthread thread, jmethodID method, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location) { jclass NullPointerException = env->FindClass("java/lang/NullPointerException"); if (!env->IsInstanceOf(exception, NullPointerException)) { return; } jclass Throwable = env->FindClass("java/lang/Throwable"); jfieldID detailMessage = env->GetFieldID(Throwable, "detailMessage", "Ljava/lang/String;"); if (env->GetObjectField(exception, detailMessage) != NULL) { return; } char buf[32]; sprintf(buf, "at location %id", (int) location); env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf)); } 


यहाँ उस थ्रेड की दोनों ऑब्जेक्ट है जिसने अपवाद (थ्रेड) को फेंक दिया, और वह स्थान जहाँ ऐसा हुआ (विधि, स्थान), और अपवाद (अपवाद) का ऑब्जेक्ट, और यहां तक ​​कि कोड में वह स्थान जो इस अपवाद को पकड़ता है (catch_modod, catch_location)।

क्या महत्वपूर्ण है: इस कॉलबैक में, जेवीएम टीआई पर्यावरण के लिए सूचक के अलावा, जेएनआई पर्यावरण (एनवी) को भी पारित किया जाता है। इसका मतलब है कि हम इसमें सभी जेएनआई कार्यों का उपयोग कर सकते हैं। अर्थात्, जेवीएम टीआई और जेएनआई सह-कलाकार पूरी तरह से एक दूसरे के पूरक हैं।

मेरे एजेंट में मैं दोनों का उपयोग करता हूं। विशेष रूप से, detailMessage के detailMessage मैं detailMessage हूं कि मेरा अपवाद NullPointerException प्रकार का है, और फिर मैं एक त्रुटि संदेश के detailMessage फ़ील्ड detailMessage प्रतिस्थापित करता detailMessage

चूँकि JVM स्वयं ही हमें स्थान देता है - बाइटकोड इंडेक्स जिसके आधार पर अपवाद हुआ, तो मैंने इस स्थान को संदेश में यहाँ रखा:



66 नंबर बाइटकोड में सूचकांक को इंगित करता है जहां यह अपवाद हुआ। लेकिन मैन्युअल रूप से बायटेकोड का विश्लेषण करना अस्पष्ट है: आपको क्लास फाइल को डिकम्पोज करने की जरूरत है, 66 वें निर्देश की तलाश करें, यह समझने की कोशिश करें कि यह क्या कर रहा था ... यह बहुत अच्छा होगा यदि हमारा एजेंट खुद कुछ और मानव-पठनीय दिखा सके।

हालांकि, इस मामले में, जेवीएम टीआई के पास आपकी जरूरत की सभी चीजें हैं। सच है, आपको JVM TI की अतिरिक्त सुविधाओं का अनुरोध करना होगा: बायटेकोड और स्थिर पूल विधि प्राप्त करें।

 jvmtiCapabilities capabilities = {0}; capabilities.can_generate_exception_events = 1; capabilities.can_get_bytecodes = 1; capabilities.can_get_constant_pool = 1; jvmti->AddCapabilities(&capabilities); 

अब मैं ExceptionCallback का विस्तार करूंगा: JVM TI फ़ंक्शन GetBytecodes() माध्यम से मुझे पता GetBytecodes() के लिए विधि का शरीर मिलेगा कि इसमें स्थान सूचकांक क्या है। इसके बाद एक बड़ा स्विच बाइटकोड निर्देश आता है: यदि यह एरे तक पहुंच है, तो एक त्रुटि संदेश होगा, यदि फ़ील्ड पर पहुंच दूसरा संदेश है, यदि विधि कॉल तीसरा है, और इसी तरह।

अपवाद कोड कोड
 jint bytecode_count; u1* bytecodes; if (jvmti->GetBytecodes(method, &bytecode_count, &bytecodes) != 0) { return; } if (location >= 0 && location < bytecode_count) { const char* message = get_exception_message(bytecodes[location]); if (message != NULL) { ... env->SetObjectField(exception, detailMessage, env->NewStringUTF(buf)); } } jvmti->Deallocate(bytecodes); 


यह केवल क्षेत्र या विधि का नाम स्थानापन्न करने के लिए रहता है। आप इसे निरंतर पूल से प्राप्त कर सकते हैं, जो जेवीएम टीआई के लिए फिर से उपलब्ध है।

 if (jvmti->GetConstantPool(holder, &cpool_count, &cpool_bytes, &cpool) != 0) { return strdup("<unknown>"); } 

इसके बाद थोड़ा सा जादू आता है, लेकिन वास्तव में कुछ भी मुश्किल नहीं है, बस वर्ग फ़ाइल प्रारूप विनिर्देश के अनुसार हम निरंतर पूल का विश्लेषण करते हैं और वहां से हम लाइन को अलग करते हैं - विधि का नाम।

लगातार पूल विश्लेषण
 u1* ref = get_cpool_at(cpool, get_u2(bytecodes + 1)); // CONSTANT_Fieldref u1* name_and_type = get_cpool_at(cpool, get_u2(ref + 3)); // CONSTANT_NameAndType u1* name = get_cpool_at(cpool, get_u2(name_and_type + 1)); // CONSTANT_Utf8 size_t name_length = get_u2(name + 1); char* result = (char*) malloc(name_length + 1); memcpy(result, name + 3, name_length); result[name_length] = 0; 


एक और महत्वपूर्ण बिंदु: कुछ जेवीएम टीआई फ़ंक्शंस, उदाहरण के लिए GetConstantPool() या GetBytecodes() , देशी मेमोरी में एक निश्चित संरचना आवंटित करते हैं, जिसे तब समाप्त करने की आवश्यकता होती है जब आप इसके साथ काम करते हैं।

 jvmti->Deallocate(cpool); 

हमारे विस्तारित एजेंट के साथ स्रोत कार्यक्रम को चलाएं, और यहां अपवाद का पूरी तरह से अलग वर्णन है: यह रिपोर्ट करता है कि हमने अशक्त वस्तु पर longValue () विधि कहा।



अन्य अनुप्रयोगों


आमतौर पर, डेवलपर्स अक्सर अपने तरीके से अपवादों को संभालना चाहते हैं। उदाहरण के लिए, यदि StackOverflowError , तो स्वचालित रूप से JVM को पुनरारंभ करें।

इस इच्छा को समझा जा सकता है, क्योंकि StackOverflowError OutOfMemoryError जैसी ही घातक त्रुटि है, इसकी घटना के बाद कार्यक्रम के सही संचालन की गारंटी देना अब संभव नहीं है। या, उदाहरण के लिए, कभी-कभी समस्या का विश्लेषण करने के लिए, मैं एक अपवाद होने पर थ्रेड डंप या ढेर डंप प्राप्त करना चाहता हूं।



निष्पक्षता में, IBM JDK के पास ऐसा अवसर है। लेकिन अब हम पहले से ही जानते हैं कि जेवीएम टीआई एजेंट का उपयोग करके आप हॉटस्पॉट में एक ही चीज को लागू कर सकते हैं। यह अपवाद कॉलबैक की सदस्यता लेने और अपवाद का विश्लेषण करने के लिए पर्याप्त है। लेकिन हमारे एजेंट से थ्रेड डंप या हीप डंप कैसे निकालें? जेवीएम टीआई के पास इस मामले के लिए आपकी जरूरत की सभी चीजें हैं:



ढेर को बायपास करने और डंप बनाने के पूरे तंत्र को लागू करना बहुत सुविधाजनक नहीं है। लेकिन मैं इसे आसान और तेज़ बनाने के रहस्य को साझा करूँगा। सच है, यह अब मानक JVM TI में शामिल नहीं है, लेकिन हॉटस्पॉट का एक निजी विस्तार है।

आपको हैडर फ़ाइल jmm.h को हॉटस्पॉट स्रोतों से कनेक्ट करने और JVM_GetManagement() फ़ंक्शन को कॉल करने की JVM_GetManagement() :

 #include "jmm.h" JNIEXPORT void* JNICALL JVM_GetManagement(jint version); void JNICALL ExceptionCallback(jvmtiEnv* jvmti, JNIEnv* env, ...) { JmmInterface* jmm = (JmmInterface*) JVM_GetManagement(JMM_VERSION_1_0); jmm->DumpHeap0(env, env->NewStringUTF("dump.hprof"), JNI_FALSE); } 

यह हॉटस्पॉट प्रबंधन इंटरफ़ेस को एक पॉइंटर लौटाएगा, जो एक कॉल में हीप डंप या थ्रेड डंप उत्पन्न करेगा। उदाहरण के लिए पूरा कोड स्टैक ओवरफ्लो के मेरे उत्तर में पाया जा सकता है।

स्वाभाविक रूप से, आप न केवल अपवादों को संभाल सकते हैं, बल्कि जेवीएम ऑपरेशन से संबंधित अन्य विभिन्न घटनाओं का एक गुच्छा भी हो सकता है: थ्रेड्स, लोडिंग क्लासेस, कचरा संग्रह शुरू करना, तरीकों को संकलित करना, प्रवेश / बाहर निकलने के तरीकों, यहां तक ​​कि जावा ऑब्जेक्ट्स के विशिष्ट क्षेत्रों तक पहुंचना या मॉडरेट करना।

मेरे पास एक और vmtrace एजेंट का उदाहरण है जो कई मानक JVM TI घटनाओं की सदस्यता लेता है और उन्हें लॉग करता है। यदि मैं इस एजेंट के साथ एक सरल प्रोग्राम चलाता हूं, तो मुझे एक विस्तृत लॉग मिलेगा, जो कि, जब टाइमस्टैम्प के साथ किया जाता है:



जैसा कि आप देख सकते हैं, बस हैलो दुनिया को प्रिंट करने के लिए, सैकड़ों कक्षाएं भरी हुई हैं, दसियों और सैकड़ों विधियां उत्पन्न और संकलित हैं। यह स्पष्ट हो जाता है कि जावा को चलाने में इतना समय क्यों लगता है। हर चीज के बारे में दो सौ मिलीसेकंड से अधिक का समय लगा।

JVM TI क्या कर सकता है


इवेंट हैंडलिंग के अलावा, JVM TI में अन्य सुविधाओं का एक समूह है। उन्हें दो समूहों में विभाजित किया जा सकता है।

एक अनिवार्य है, जो कोई भी JVM जो JVM TI का समर्थन करता है, को लागू करना चाहिए। इनमें तरीकों, क्षेत्रों, प्रवाह, नए वर्गों को क्लासपाथ में जोड़ने की क्षमता और इसी तरह के विश्लेषण के संचालन शामिल हैं।

वैकल्पिक विशेषताएं हैं जिन्हें प्रारंभिक क्षमताओं के अनुरोध की आवश्यकता होती है। जेवीएम को उन सभी का समर्थन करने की आवश्यकता नहीं है, हालांकि हॉटस्पॉट पूरे विनिर्देश को पूरे तरीके से लागू करता है। वैकल्पिक सुविधाओं को दो उपसमूहों में विभाजित किया जाता है: जिन्हें केवल जेवीएम की शुरुआत में जोड़ा जा सकता है (उदाहरण के लिए, ब्रेकपॉइंट सेट करने या स्थानीय चर का विश्लेषण करने की क्षमता), और वे जो किसी भी समय (विशेष रूप से, क्वेटाकोड या निरंतर पूल द्वारा कनेक्ट किए जा सकते हैं, जिसे मैं ऊपर इस्तेमाल किया गया)।



आप देख सकते हैं कि सुविधाओं की सूची डीबगर की क्षमताओं के समान है। वास्तव में, एक जावा डीबगर JVM TI एजेंट के विशेष मामले से ज्यादा कुछ नहीं है, जो इन सभी क्षमताओं का लाभ उठाता है और सभी क्षमताओं का अनुरोध करता है।

क्षमताओं का पृथक्करण, जिन्हें किसी भी समय सक्षम किया जा सकता है, और जो केवल बूट समय पर हैं, उद्देश्य पर किया जाता है। सभी सुविधाएँ मुफ्त नहीं हैं, कुछ ओवरहेड हैं।

यदि सुविधा के उपयोग के साथ प्रत्यक्ष ओवरहेड्स के साथ सब कुछ स्पष्ट है, तो कम स्पष्ट अप्रत्यक्ष भी हैं जो दिखाई देते हैं भले ही आप सुविधा का उपयोग न करें, लेकिन बस क्षमताओं के माध्यम से आप घोषणा करते हैं कि भविष्य में कुछ समय की आवश्यकता होगी। यह इस तथ्य के कारण है कि वर्चुअल मशीन कोड को अलग तरीके से संकलित कर सकती है या रनटाइम के लिए अतिरिक्त चेक जोड़ सकती है।

उदाहरण के लिए, अपवादों के लिए सदस्यता लेने के लिए पहले से ही विचार की गई क्षमता (can_generate_exception_events) इस तथ्य की ओर ले जाती है कि सभी अपवादों को फेंकने का काम धीमी गति से होगा। सिद्धांत रूप में, यह इतना डरावना नहीं है, क्योंकि एक अच्छे जावा कार्यक्रम में अपवाद एक दुर्लभ चीज है।

स्थानीय चरों के साथ स्थिति थोड़ी खराब है। Can_access_local_variables, जो आपको किसी भी समय स्थानीय चर के मान प्राप्त करने की अनुमति देता है, के लिए आपको कुछ महत्वपूर्ण अनुकूलन अक्षम करने की आवश्यकता है। विशेष रूप से, एस्केप एनालिसिस पूरी तरह से काम करना बंद कर देता है, जो ध्यान देने योग्य ओवरहेड दे सकता है: आवेदन के आधार पर, 5-10%।

इसलिए निष्कर्ष: यदि आप जावा को डिबग एजेंट के साथ चालू करते हैं, तो इसका उपयोग किए बिना, अनुप्रयोग धीमी गति से चलेंगे। वैसे भी, उत्पादन में डिबगिंग एजेंट को शामिल करना एक अच्छा विचार नहीं है।

कई सुविधाएँ, उदाहरण के लिए, एक ब्रेकपॉइंट सेट करना या एक विधि से सभी इनपुट / आउटपुट को ट्रेस करना, अधिक गंभीर ओवरहेड ले जाती हैं। विशेष रूप से, कुछ JVM TI इवेंट्स (FieldAccess, MethodEntry / Exit) केवल दुभाषिया में काम करते हैं।

एक एजेंट अच्छा है, और दो बेहतर है


आप कई एजेंटों को केवल -agentpath मापदंडों को निर्दिष्ट करके एक ही प्रक्रिया से -agentpath सकते हैं। हर किसी का अपना JVM TI पर्यावरण होगा। इसका मतलब है कि हर कोई अपनी क्षमताओं की सदस्यता ले सकता है और अपनी घटनाओं को स्वतंत्र रूप से रोक सकता है।

और अगर दो एजेंटों ने ब्रेकपॉइंट इवेंट की सदस्यता ली है, और एक में ब्रेकपॉइंट को किसी विधि में सेट किया गया है, तो जब इस विधि को निष्पादित किया जाता है, तो क्या दूसरा एजेंट इवेंट प्राप्त करेगा?

वास्तव में, ऐसी स्थिति उत्पन्न नहीं हो सकती है (कम से कम हॉटस्पॉट जेवीएम में)। क्योंकि कुछ क्षमताएं हैं जो किसी एक एजेंट के पास किसी भी समय हो सकती हैं। इनमें विशेष रूप से ब्रेकपॉइंट_वियर्स शामिल हैं। इसलिए, यदि दूसरा एजेंट समान क्षमता का अनुरोध करता है, तो उसे प्रतिक्रिया में त्रुटि प्राप्त होगी।

यह एक महत्वपूर्ण निष्कर्ष है: एजेंट को हमेशा क्षमताओं के अनुरोध के परिणाम की जांच करनी चाहिए, भले ही आप हॉटस्पॉट पर चल रहे हों और जानते हों कि वे सभी उपलब्ध हैं। JVM TI विनिर्देश अनन्य क्षमताओं के बारे में कुछ नहीं कहता है, लेकिन हॉटस्पॉट में ऐसा कार्यान्वयन सुविधा है।

सच है, एजेंट अलगाव हमेशा पूरी तरह से काम नहीं करता है। Async-profiler के विकास के दौरान , मैं इस समस्या में आया था: जब हमारे पास दो एजेंट होते हैं और एक विधि संकलन की घटनाओं की पीढ़ी का अनुरोध करता है, तो सभी एजेंट इन घटनाओं को प्राप्त करते हैं। बेशक, मैंने एक बग दर्ज किया, लेकिन आपको यह ध्यान रखना चाहिए कि जिन घटनाओं की आपको उम्मीद नहीं है, वे आपके एजेंट में हो सकती हैं।

एक नियमित कार्यक्रम में उपयोग


जेवीएम टीआई डिबगर्स और प्रोफाइलरों के लिए एक बहुत ही विशिष्ट चीज की तरह लग सकता है, लेकिन इसका उपयोग नियमित जावा प्रोग्राम में भी किया जा सकता है। एक उदाहरण पर विचार करें।

प्रतिक्रियाशील प्रोग्रामिंग प्रतिमान अब व्यापक है जब सब कुछ अतुल्यकालिक है, लेकिन इस प्रतिमान के साथ एक समस्या है।

 public class TaskRunner { private static void good() { CompletableFuture.runAsync(new AsyncTask(GOOD)); } private static void bad() { CompletableFuture.runAsync(new AsyncTask(BAD)); } public static void main(String[] args) throws Exception { good(); bad(); Thread.sleep(200); } } 

मैं दो अतुल्यकालिक कार्यों को चलाता हूं जो केवल मापदंडों में भिन्न होते हैं। और अगर कुछ गलत होता है, तो एक अपवाद उठाया जाता है:



स्टैक ट्रेस से, यह पूरी तरह से स्पष्ट नहीं है कि इनमें से कौन सा कार्य समस्या का कारण बना। क्योंकि अपवाद पूरी तरह से अलग थ्रेड में होता है, जहां हमारे पास कोई संदर्भ नहीं है। किस कार्य में कैसे समझें?

समाधानों में से एक के रूप में, आप इस बारे में जानकारी जोड़ सकते हैं कि हमने इसे अपने अतुल्यकालिक कार्य के निर्माता को कहां बनाया है:

 public AsyncTask(String arg) { this.arg = arg; this.location = getLocation(); } 

यही है, स्थान याद रखें - कोड में एक विशिष्ट स्थान, उस रेखा के ठीक नीचे जहां से निर्माणकर्ता को बुलाया गया था। और इसे गिरवी रखने के अपवाद के मामले में:

 try { int n = Integer.parseInt(arg); } catch (Throwable e) { System.err.println("ParseTask failed at " + location); e.printStackTrace(); } 

अब, जब कोई अपवाद होता है, तो हम देखेंगे कि टास्करनर (जहां बीएडी पैरामीटर के साथ कार्य बनाया गया है) में लाइन 14 पर ऐसा हुआ है:



लेकिन कोड में उस जगह को कैसे प्राप्त किया जाए जहां से निर्माणकर्ता को बुलाया जाता है? जावा 9 से पहले, ऐसा करने का एकमात्र कानूनी तरीका था: स्टैक ट्रेस प्राप्त करें, कुछ अप्रासंगिक फ़्रेमों को छोड़ दें, और स्टैक पर थोड़ा कम जगह होगी जिसे हमारे कोड कहा जाता है।

 String getLocation() { StackTraceElement caller = Thread.currentThread().getStackTrace()[3]; return caller.getFileName() + ':' + caller.getLineNumber(); } 

लेकिन एक समस्या है। पूर्ण StackTrace प्राप्त करना बहुत धीमा है। मेरे पास इसकी पूरी रिपोर्ट है

यह इतनी बड़ी समस्या नहीं होती अगर ऐसा शायद ही कभी होता। लेकिन, उदाहरण के लिए, हमारे पास एक वेब सेवा है - एक दृश्यपटल जो HTTP अनुरोधों को स्वीकार करता है। यह एक महान अनुप्रयोग, कोड की लाखों लाइनें है। और रेंडरिंग त्रुटियों को पकड़ने के लिए, हम एक समान तंत्र का उपयोग करते हैं: रेंडरिंग के लिए घटकों में, हम उस जगह को याद करते हैं जहां वे बनाए जाते हैं। हमारे पास इस तरह के लाखों घटक हैं, इसलिए सभी स्टैक के निशान को प्राप्त करने में एक मिनट नहीं, बल्कि आवेदन शुरू करने में समय लगता है। इसलिए, यह सुविधा पहले उत्पादन में अक्षम थी, हालांकि समस्याओं के विश्लेषण के लिए उत्पादन में इसकी आवश्यकता होती है।

जावा 9 ने स्ट्रीम स्टैक को बायपास करने के लिए एक नया तरीका पेश किया: StackWalker, जो स्ट्रीम एपीआई के माध्यम से यह सब आलसी, मांग पर कर सकता है। यही है, हम सही संख्या में फ़्रेम को छोड़ सकते हैं और केवल एक ही प्राप्त कर सकते हैं जो हमें रुचिकर बनाता है।

 String getLocation() { return StackWalker.getInstance().walk(s -> { StackWalker.StackFrame frame = s.skip(3).findFirst().get(); return frame.getFileName() + ':' + frame.getLineNumber(); }); } 

यह पूर्ण स्टैक ट्रेस प्राप्त करने से थोड़ा बेहतर काम करता है, लेकिन परिमाण के क्रम या कई बार नहीं। हमारे मामले में, यह लगभग डेढ़ गुना तेजी से निकला:



StackWalker के सबॉप्टीमल कार्यान्वयन के साथ एक ज्ञात समस्या है , और सबसे अधिक संभावना है कि यह JDK 13. में भी तय हो जाएगी। लेकिन फिर, हमें जावा 8 में अभी क्या करना चाहिए, जहां StackWalker भी धीमा नहीं है?

जेवीएम टीआई फिर से बचाव में आता है। एक GetStackTrace() फ़ंक्शन है जो आपके लिए आवश्यक सब कुछ कर सकता है: किसी निर्दिष्ट लंबाई के स्टैक ट्रेस का एक टुकड़ा प्राप्त करें, निर्दिष्ट फ्रेम से शुरू करें, और अधिक कुछ न करें।

 GetStackTrace(jthread thread, jint start_depth, jint max_frame_count, jvmtiFrameInfo* frame_buffer, jint* count_ptr) 

केवल एक ही प्रश्न शेष है: हमारे जावा प्रोग्राम से जेवीएम टीआई फ़ंक्शन को कैसे कॉल करें? किसी भी अन्य मूल विधि की तरह: System.loadLibrary() साथ देशी पुस्तकालय को लोड करें, जहां हमारी विधि का System.loadLibrary() कार्यान्वयन होगा।

 public class StackFrame { public static native String getLocation(int depth); static { System.loadLibrary("stackframe"); } } 

JVM TI पर्यावरण के लिए एक पॉइंटर न केवल Agent_OnLoad () से प्राप्त किया जा सकता है , बल्कि प्रोग्राम के चलने के दौरान, और सामान्य देशी JNI विधियों से इसका उपयोग जारी रखने के लिए:

 JNIEXPORT jstring JNICALL Java_StackFrame_getLocation(JNIEnv* env, jclass unused, jint depth) { jvmtiFrameInfo frame; jint count; jvmti->GetStackTrace(NULL, depth, 1, &frame, &count); 

:



, JDK : - . -. , , , JDK. JDK 8u112, JVM TI-, (GetMethodName, GetMethodDeclaringClass ), .

, , : JVM TI- , , -. , C++, jvmtiEnter.xsl .

: HotSpot XSLT-. HotSpot.

? , . , - jmethodID , . , .


, JVM TI Java- , System.loadLibrary .

, , JVM TI- -agentpath JVM.

: (dynamic attach).

? , - , , JVM TI- .

JDK 9, jcmd:

 jcmd <pid> JVMTI.agent_load /path/to/agent.so [arguments] 

JDK jattach . , async-profiler , - JVM-, jattach.

JVM TI- , , Agent_OnLoad() , Agent_OnAttach() . : Agent_OnAttach() capabilities, .

, , Agent_OnAttach() .

. IntelliJ IDEA: Java-, , - .

process ID IDEA, jattach JVM TI- patcher.dll:
jattach 8648 load patcher.dll true

:



? Java- ( javax.swing.AbstractButton ) JNI setBackground() . .

Java 9


JVM TI , , , API, . Java 9.

, Java 9 , . , «» JDK, .

, JDK Direct ByteBuffer. API:



, Cassandra , MappedByteBuffer, , JVM .

JDK 9, IllegalAccessError:



Reflection: .

, Java Linux. - java.io.FileDescriptor JNI - . , JDK 9, :



, JVM, . , . , Cassandra Java 11, :

 --add-exports java.base/jdk.internal.misc=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-exports java.management.rmi/com.sun.jmx.remote.internal.rmi=ALL-UNNAMED --add-exports java.rmi/sun.rmi.registry=ALL-UNNAMED --add-exports java.rmi/sun.rmi.server=ALL-UNNAMED --add-exports java.sql/java.sql=ALL-UNNAMED --add-opens java.base/java.lang.module=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.math=ALL-UNNAMED --add-opens java.base/jdk.internal.module=ALL-UNNAMED --add-opens java.base/jdk.internal.util.jar=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED 

JVM TI :

  • GetAllModules
  • AddModuleExports
  • AddModuleOpens
  • . .

, : JVM, , , .

Direct ByteBuffer:

 public static void main(String[] args) { ByteBuffer buf = ByteBuffer.allocateDirect(1024); ((sun.nio.ch.DirectBuffer) buf).cleaner().clean(); System.out.println("Buffer cleaned"); } 

, IllegalAccessError. agentpath antimodule , . .

Java 11


Java 11. , ! : SampledObjectAlloc , , .

callback , : , , , , . SetHeapSampingInterval , .



? , , . Java Flight Recorder.

, , , , .

Thread Local Allocation Buffer . TLAB , . , .



, TLAB, . JVM runtime .

, , — 5%.

, , JDK 7, Flight Recorder. API async-profiler. , JDK 11, API , JVM TI, . , YourKit . API, , .

. , , , , .



निष्कर्ष


JVM TI — .

, ++, JVM . , JVM TI .

GitHub . , .

Source: https://habr.com/ru/post/hi458812/


All Articles