OpenJDK के लिए घर का बना कचरा कलेक्टर

यह लेखक की सहमति से प्रकाशित एलेक्सी शिपिलेव "डू इट योरसेल्फ ( ओपेन जेडडीके ) कचरा कलेक्टर" के लेख का अनुवाद है। पीएम में किसी भी टाइपोस और अन्य कीड़े की रिपोर्ट करें - हम उन्हें ठीक कर देंगे।

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


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


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



सामग्री




1. क्या जीसी शामिल हैं


अब जबकि कई अलग-अलग GCs लिखे गए हैं, अपना खुद का बनाना काफी सरल है - पहले से ही लिखे गए कुछ तत्वों को फिर से लागू करने और परीक्षण किए गए कोड के बारे में कुछ चिंताओं को स्थानांतरित करने के लिए उपयोग किया जा सकता है।



1.1। एप्सिलॉन जी.सी.


OpenJDK 11 एक नया JEP 318 पेश करता है: "एप्सिलन: ए-ओप गारबेज कलेक्टर (प्रायोगिक)" इसका कार्य उस मामले के लिए न्यूनतम कार्यान्वयन प्रदान करना है जब स्मृति को मुक्त करना आवश्यक नहीं है या यहां तक ​​कि निषिद्ध नहीं है। जेईपी अधिक विस्तार से चर्चा करता है कि यह उपयोगी क्यों हो सकता है।


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



1.1.1। मेमोरी आवंटन


एप्सिलॉन जीसी का सबसे विकसित हिस्सा मेमोरी आवंटित करने के लिए जिम्मेदार है । यह मनमाने आकार की मेमोरी आवंटित करने और वांछित आकार के थ्रेड-लोकल एलोकेशन बफर (TLAB) बनाने के लिए बाहरी अनुरोध करता है। कार्यान्वयन स्वयं ही कोशिश कर रहा है कि टीएलएबी को बहुत अधिक नहीं बढ़ाया जाए, क्योंकि कोई मुफ्त मेमोरी नहीं होगी और कोई भी खोए हुए बाइट्स को वापस नहीं करेगा।



1.1.2। बाधाओं


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


एप्सिलॉन को बाधाओं की आवश्यकता नहीं है, लेकिन रनटाइम और कंपाइलर अभी भी जानना चाहते हैं कि अवरोध कुछ भी नहीं करते हैं। हर समय हर जगह इसे संभालना थकाऊ हो सकता है। सौभाग्य से, OpenJDK 11 के साथ शुरू, एक नया JEP-304 है: "कचरा संग्रह इंटरफ़ेस" , जो बाधाओं को सम्मिलित करने के लिए बहुत आसान, बहुत आसान बनाता है। विशेष रूप से, एप्सिलॉन में स्थापित बाधा खाली है , और सभी तुच्छ काम - बचाओ, लोड, कैस, एरेस्कोपी - को एक मौजूदा सुपरक्लास से तुच्छ बाधाओं के कार्यान्वयन के लिए सौंपा जा सकता है। यदि आप एक जीसी बना रहे हैं, जिसमें बाधाओं की भी जरूरत नहीं है, तो आप बस एप्सिलॉन से कोड का पुन: उपयोग कर सकते हैं।



1.1.3। मॉनिटरिंग कनेक्शन


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



1.2। रनटाइम और जीसी



1.2.1। जड़ तत्व


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



1.2.2। वस्तु क्रॉल


कचरा कलेक्टर को जावा ऑब्जेक्ट में आउटबाउंड लिंक को बायपास करना चाहिए। यह ऑपरेशन हर जगह पाया जाता है, इसलिए रनटाइम के सामान्य हिस्से रेडीमेड वर्कआर्डर प्रदान करते हैं; आपको स्वयं कुछ भी लिखने की आवश्यकता नहीं है। नीचे एक विशिष्ट कार्यान्वयन के साथ एक अनुभाग होगा, और वहां आप पा सकते हैं, उदाहरण के लिए, कॉल obj→oop_iterate



1.2.3। आंदोलनों


मूविंग कचरा संग्रहकर्ता को कहीं-कहीं स्थानांतरित वस्तुओं के नए पते लिखने की आवश्यकता है। कई स्थान हैं जहां आप इस अग्रेषण डेटा को लिख सकते हैं।


  1. आप ऑब्जेक्ट में "मार्कर शब्द" का पुन: उपयोग कर सकते हैं (सीरियल, समानांतर, आदि)। दुनिया बंद होने के बाद, ऑब्जेक्ट तक सभी पहुंच नियंत्रित होती है, और यह गारंटी दी जाती है कि कोई जावा थ्रेड अस्थायी डेटा नहीं देख सकता है जिसे हमने मार्कर शब्द में दर्ज करने का फैसला किया था। आप फ़ॉरवर्डिंग डेटा स्टोर करने के लिए इसका पुनः उपयोग कर सकते हैं।
  2. आप एक अलग मूल आंदोलन तालिका ( ZGC , C4 और अन्य) बनाए रख सकते हैं। यह GC को रनटाइम और बाकी एप्लिकेशन से पूरी तरह से अलग कर देता है, क्योंकि केवल जीसी ऐसी तालिका के अस्तित्व के बारे में जानता है। प्रतियोगी असेम्बलर्स आमतौर पर सिर्फ इस तरह की योजना का उपयोग करते हैं - वे अनावश्यक समस्याओं का एक समूह नहीं होना चाहते हैं।
  3. आप ऑब्जेक्ट ( शेनांडो और अन्य) में एक और शब्द जोड़ सकते हैं। दो पिछले दृष्टिकोणों का यह संयोजन न केवल समस्याओं के बिना मौजूदा हेडर के साथ रनटाइम और एप्लिकेशन को काम करने की अनुमति देता है, बल्कि अग्रेषण डेटा को भी बचाता है।


1.2.4। मार्कर डेटा


कचरा संग्रहकर्ता को अंकन डेटा लिखने की आवश्यकता है। और फिर, उन्हें बचाने के कई तरीके हैं:


  1. आप मार्कर शब्द को ऑब्जेक्ट (सीरियल, समानांतर, आदि) में पुन: उपयोग कर सकते हैं। फिर से, वर्ल्ड स्टॉप मोड में, आप लेबल के तथ्य को एनकोड करने के लिए मार्कर शब्द में बिट्स का उपयोग कर सकते हैं। इसके अलावा, यदि आपको सभी जीवित वस्तुओं के चारों ओर जाने की आवश्यकता है, तो हम ढेर के साथ जाते हैं, ऑब्जेक्ट के बाद ऑब्जेक्ट - यह इस तथ्य के कारण संभव है कि ढेर पार्स करने योग्य है
  2. आप मार्किंग डेटा (G1, Shenandoah, आदि) के भंडारण के लिए एक अलग संरचना बनाए रख सकते हैं। यह आमतौर पर एक अलग बिटमैप का उपयोग करके किया जाता है, जो कार्ड के 1 बिट के ढेर के हर एन बाइट्स को मैप करता है। आमतौर पर, जावा ऑब्जेक्ट्स को 8 बाइट्स द्वारा संरेखित किया जाता है , इसलिए कार्ड प्रत्येक 64 बिट्स को ढेर से कार्ड के 1 बिट तक मैप करता है, जो कि देशी मेमोरी में ढेर के आकार का 1/64 भाग होता है। जीवित वस्तुओं की उपस्थिति के लिए ढेर को स्कैन करते समय ये ओवरहेड अच्छी तरह से भुगतान करते हैं, विशेष रूप से विरल: मानचित्र को दरकिनार करने से अक्सर होता है ढेर को दरकिनार करके ऑब्जेक्ट द्वारा ऑब्जेक्ट को अलग किया जाता है।
  3. अपने आप को लिंक (जेडजीसी, सी 4 और अन्य) में लेबल को एनकोड करें। इसके लिए एप्लिकेशन के साथ समन्वय की आवश्यकता है, फिर आपको लिंक से इन सभी लेबलों को काटने या शुद्धता बनाए रखने के लिए कुछ अन्य चालें करने की आवश्यकता है। दूसरे शब्दों में, हमें या तो बाधाओं या जीसी से कुछ अतिरिक्त काम की आवश्यकता है।


2. सामान्य योजना


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


प्रश्न में एल्गोरिथ्म शिफ्टिंग जीसी है: चलती वस्तुएं एक सरणी में ढेर की शुरुआत में चलती हैं। इसके पेशेवरों और विपक्ष हैं:


  • यह मेमोरी आवंटन के क्रम को बनाए रखता है। यह स्मृति में लेआउट को नियंत्रित करने के लिए बहुत अच्छा है, अगर यह आपके लिए मायने रखता है (नियंत्रण शैतान, यह आपका समय है!)। नकारात्मक पक्ष यह है कि आपको इस तरह से स्वचालित लिंक स्थानीयता नहीं मिलेगी।
  • इसकी जटिलता वस्तुओं की संख्या का O (N) है। हालांकि, रैखिकता एक मूल्य पर आती है: जीसी को प्रत्येक निर्माण चक्र के लिए 4 बार के एक गुच्छा को बायपास करने की आवश्यकता होती है।
  • यह ढेर पर मुफ्त मेमोरी की आवश्यकता नहीं है! जीवित वस्तुओं को खाली करने के लिए ढेर पर मेमोरी को आरक्षित करने की आवश्यकता नहीं है, इसलिए आप 99 के अतिप्रवाह वाले ढेर के साथ भी काम कर सकते हैं। (9)%। यदि हम सरल कलेक्टरों के अन्य विचारों को लेते हैं, उदाहरण के लिए, अर्ध-अंतरिक्ष (अर्ध-अंतरिक्ष मेहतर) के साथ एक मेहतर, तो हमें ढेर की प्रस्तुति को फिर से लिखना होगा और निकासी के लिए थोड़ी जगह आरक्षित करनी होगी, लेकिन यह इस अभ्यास के दायरे से बाहर है।
  • यदि आप समस्या पर थोड़ा काम करते हैं, तो आप जीसी के निष्क्रिय होने पर अवधि के दौरान शून्य मेमोरी और समय की खपत प्राप्त कर सकते हैं। यह एक मनमाना स्थिति में एक मेमोरी पर शुरू होता है, और रुक जाता है, इसे काफी कॉम्पैक्ट करता है। यह बहुत अच्छी तरह से फिट बैठता है कि एप्सिलॉन कैसे काम करता है: यह सिर्फ अंतिम ऑब्जेक्ट के ठीक बाद हाइलाइटिंग रखता है। यह भी एक शून्य है: ढेर की शुरुआत में कुछ मृत वस्तुएं बड़ी संख्या में आंदोलनों की ओर ले जाती हैं।
  • यह सिर्फ नए अवरोधों की आवश्यकता नहीं है, आप EpsilonBarrierSet रूप में पुन: उपयोग कर सकते हैं।

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



3. जीसी कोर का कार्यान्वयन


पूरे कार्यान्वयन को पढ़ना और समझना एक अज्ञानी व्यक्ति के लिए बहुत जटिल हो सकता है। इस खंड में, हम इसे चरण दर चरण समझेंगे।



3.1। प्रस्तावना


कचरा संग्रहकर्ता को संग्रह की तैयारी के लिए आमतौर पर कुछ चीजें करने की आवश्यकता होती है। टिप्पणियों को पढ़ें, उन्हें अपने लिए बोलना चाहिए:


 { GCTraceTime(Info, gc) time("Step 0: Prologue", NULL); //      .      //   :   ,   ,  // «»   ,      //   ,     . if (!os::commit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size(), false)) { log_warning(gc)("Could not commit native memory for marking bitmap, GC failed"); return; } //        ,  , //       TLAB-. ensure_parsability(true); //      ,    GC. CodeCache::gc_prologue(); BiasedLocking::preserve_marks(); //        . //       . DerivedPointerTable::clear(); } 

चूँकि हम वस्तुओं की पुनरावृत्ति को ट्रैक करने के लिए बिटमैप का उपयोग करते हैं, इसलिए हमें उपयोग से पहले इसे साफ़ करना होगा। या हमारे मामले में, चूंकि हमारा उद्देश्य जीसी चक्र शुरू करने से पहले संसाधनों के लिए कभी नहीं पूछना है, इसलिए हमें पहले से मेमोरी के लिए बिटमैप को प्रतिबद्ध करना होगा। यह कई दिलचस्प लाभ प्रदान करता है, कम से कम लिनक्स पर, जहां अधिकांश बिटमैप पृष्ठ शून्य को इंगित करेगा, विशेष रूप से विरल ढेर के लिए।


थ्रेड्स को अपने TLAB को मुक्त करना चाहिए और बिल्ड पूरा होने के बाद GC से नए के लिए पूछना चाहिए।


TLAB और java.lang.ThreadLocal भ्रमित न करें। जीसी के दृष्टिकोण से, थ्रेडलोक सामान्य ऑब्जेक्ट हैं, और उन्हें जीसी द्वारा संकलित नहीं किया जाएगा जब तक कि विशेष रूप से जावा कोड में अन्यथा आवश्यक न हो।

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



3.2। अंकन


दुनिया के स्टॉप मोड में चिह्नित करना काफी सरल हो जाता है जब हमारे लिए लगभग सब कुछ पहले ही हो चुका होता है। लेबलिंग बहुत मानक है, और सबसे अधिक संभावना है, कई कार्यान्वयनों में, जीसी पहला कदम है।


 { GCTraceTime(Info, gc) time("Step 1: Mark", NULL); //    ,     .  //   ,  ,    //      . EpsilonMarkStack stack; EpsilonScanOopClosure cl(&stack, &_bitmap); //      . process_roots(&cl); stat_reachable_roots = stack.size(); //    ,    . //    ,   , //      . while (!stack.is_empty()) { oop obj = stack.pop(); obj->oop_iterate(&cl); stat_reachable_heap++; } //       . DerivedPointerTable::set_active(false); } 

यह किसी भी अन्य ग्राफ़ के समान ही काम करता है: आप ट्रैवर्सल को शुरू करने योग्य वर्टिकल के शुरुआती सेट के साथ शुरू करते हैं, आउटगोइंग किनारों के साथ जाते हैं और सभी विज़िट किए गए कोने रिकॉर्ड करते हैं। ट्रैवर्सल तब तक जारी रहता है जब तक कि सभी गैर-प्रमाणित चोटियां समाप्त नहीं हो जाती हैं। जीसी में, "कोने" ऑब्जेक्ट हैं, और "किनारों" उनके बीच लिंक हैं।


तकनीकी रूप से, हम केवल वस्तुओं के ग्राफ के माध्यम से पुनरावृत्ति कर सकते हैं, लेकिन यह मनमाने ढंग से रेखांकन के लिए एक बुरा विचार है जो बहुत बड़े व्यास हो सकता है। एक बिलियन चोटियों की एक लिंक्ड सूची की कल्पना करो! इसलिए, पुनरावृत्ति की गहराई को सीमित करने के लिए, हम एक अंकन स्टैक का उपयोग करते हैं जो रिकॉर्ड की गई वस्तुओं का पता लगाता है।


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


अंकों के साथ एक बिटमैप अंकन वेवफ्रंट (पहले से देखी गई बहुत सारी वस्तुएं) को रिकॉर्ड करने के लिए एक उपकरण के रूप में कार्य करता है, और अंत में - वांछित परिणाम के भंडार के रूप में, सभी पहुंच योग्य वस्तुओं का एक सेट। असली काम EpsilonScanOopClosure में EpsilonScanOopClosure , यह सभी दिलचस्प वस्तुओं पर लागू होता है और चयनित ऑब्जेक्ट के सभी लिंक पर पुनरावृत्त होता है।


देखो, जावा फैशनेबल बनने से पहले बंद (बंद) करना जानता था!

 class EpsilonScanOopClosure : public BasicOopIterateClosure { private: EpsilonMarkStack* const _stack; MarkBitMap* const _bitmap; template <class T> void do_oop_work(T* p) { // p -     ,   oop,   //      ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //  . ,   .  , //        . //    +, //       . if (!_bitmap->is_marked(obj)) { _bitmap->mark((HeapWord*)obj); _stack->push(obj); } } } }; 

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


 //           . //   ,    ( )  ,  //       1/64  . void EpsilonHeap::walk_bitmap(ObjectClosure* cl) { HeapWord* limit = _space->top(); HeapWord* addr = _bitmap.get_next_marked_addr(_space->bottom(), limit); while (addr < limit) { oop obj = oop(addr); assert(_bitmap.is_marked(obj), "sanity"); cl->do_object(obj); addr += 1; if (addr < limit) { addr = _bitmap.get_next_marked_addr(addr, limit); } } } 


3.3। नए पतों की गणना करें


यह भी एक काफी सरल कदम है, और यह ठीक उसी तरह लागू होता है जैसा कि एल्गोरिथ्म कहता है।



 //    forwarding data (,    ) //   .        . //          . PreservedMarks preserved_marks; //     GC. HeapWord* new_top; { GCTraceTime(Info, gc) time("Step 2: Calculate new locations", NULL); //    ,        //    . ,  - . EpsilonCalcNewLocationObjectClosure cl(_space->bottom(), &preserved_marks); walk_bitmap(&cl); //         . //       ,    //      ,      "" //  . new_top = cl.compact_point(); stat_preserved_marks = preserved_marks.size(); } 

केवल एक चीज जो आपकी आंख को पकड़ती है वह यह है कि हमने जावा वस्तुओं के अंकन शब्द में नए पते संग्रहीत करने का निर्णय लिया है, और यह शब्द पहले से ही किसी महत्वपूर्ण चीज पर कब्जा कर सकता है, उदाहरण के लिए, ताले के बारे में जानकारी। सौभाग्य से, इस तरह के nontrivial अंकन शब्द काफी दुर्लभ हैं, और हम बस उन्हें अलग से स्टोर कर सकते हैं, यदि आवश्यक हो तो: यह वह है जिसके लिए PreservedMarks उपयोग किया जाता है।


वास्तविक एल्गोरिदम काम EpsilonCalcNewLocationObjectClosure द्वारा किया जाता है:


 class EpsilonCalcNewLocationObjectClosure : public ObjectClosure { private: HeapWord* _compact_point; PreservedMarks* const _preserved_marks; public: EpsilonCalcNewLocationObjectClosure(HeapWord* start, PreservedMarks* pm) : _compact_point(start), _preserved_marks(pm) {} void do_object(oop obj) { //    :    . //        (      , //    ),      , //     . if ((HeapWord*)obj != _compact_point) { markOop mark = obj->mark_raw(); if (mark->must_be_preserved(obj)) { _preserved_marks->push(obj, mark); } obj->forward_to(oop(_compact_point)); } _compact_point += obj->size(); } HeapWord* compact_point() { return _compact_point; } }; 

forward_to सबसे महत्वपूर्ण हिस्सा है क्योंकि यह ऑब्जेक्ट के मार्कर शब्द में "मूव एड्रेस" को स्टोर करता है। अगले चरणों में इसकी आवश्यकता होगी।



3.4। संकेत ठीक करें


अब आपको फिर से ढेर से गुजरने की ज़रूरत है और निम्नलिखित एल्गोरिथम के अनुसार अपने नए पते के साथ सभी लिंक को फिर से लिखना होगा:



 { GCTraceTime(Info, gc) time("Step 3: Adjust pointers", NULL); //     _   _,     // « ».      forwarding data, //    .      . EpsilonAdjustPointersObjectClosure cl; walk_bitmap(&cl); //     ,      VM,  //     :      . EpsilonAdjustPointersOopClosure cli; process_roots(&cli); //   ,      , //     . preserved_marks.adjust_during_full_gc(); } 

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


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


 class EpsilonAdjustPointersOopClosure : public BasicOopIterateClosure { private: template <class T> void do_oop_work(T* p) { // p -     ,   oop. //        ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //         . //  ,    . if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); RawAccess<>::oop_store(p, fwd); } } } }; class EpsilonAdjustPointersObjectClosure : public ObjectClosure { private: EpsilonAdjustPointersOopClosure _cl; public: void do_object(oop obj) { //    ,    : obj->oop_iterate(&_cl); } }; 

इस चरण को पूरा करने के बाद, हमने अनिवार्य रूप से ढेर को तोड़ दिया: लिंक "गलत" पते पर इंगित करता है जिस पर ऑब्जेक्ट अभी तक झूठ नहीं बोलते हैं। चलो इसे ठीक करो!



3.5। हम वस्तुओं को स्थानांतरित करते हैं


एल्गोरिदम के अनुसार नए पतों पर वस्तुओं को स्थानांतरित करने का समय:



फिर से ढेर के चारों ओर EpsilonMoveObjectsObjectClosure और सभी जीवित वस्तुओं के लिए EpsilonMoveObjectsObjectClosure क्लोजर लागू करें:


 { GCTraceTime(Info, gc) time("Step 4: Move objects", NULL); //       . //          . EpsilonMoveObjectsObjectClosure cl; walk_bitmap(&cl); stat_moved = cl.moved(); //         ,   // «»      . _space->set_top(new_top); } 

उसके तुरंत बाद, आप संकलन बिंदु के ढेर को खींच सकते हैं, जिससे कचरा संग्रह चक्र समाप्त होने के तुरंत बाद इस जगह से सीधे मेमोरी आवंटित करना संभव हो जाता है।


ध्यान दें कि शिफ्टिंग असेंबली में हम मौजूदा ऑब्जेक्ट्स की सामग्री को ओवरराइट कर सकते हैं, लेकिन चूंकि स्कैनिंग एक ही दिशा में जाती है, इसलिए ओवरराइट की गई वस्तुएँ पहले से ही सही जगह पर कॉपी हो जाती हैं।


एक ही सुविधा के पुराने और नए स्थान एक दूसरे को काट सकते हैं। उदाहरण के लिए, यदि आप 8 बाइट्स द्वारा 100-बाइट ऑब्जेक्ट को शिफ्ट करते हैं। प्रतिलिपि प्रक्रिया को इसके लिए स्वयं काम करना चाहिए, और इंटरसेक्टिंग सामग्री को सही तरीके से कॉपी किया जाना चाहिए, Copy::aligned_*conjoint*_words ध्यान दें Copy::aligned_*conjoint*_words

क्लोजर ही स्थानांतरित वस्तुओं को नए पते पर ले जाएगा:


 class EpsilonMoveObjectsObjectClosure : public ObjectClosure { public: void do_object(oop obj) { //     ,  .   - , //   -  mark word, //      forwarding data. if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); Copy::aligned_conjoint_words((HeapWord*)obj, (HeapWord*)fwd, obj->size()); fwd->init_mark_raw(); } } }; 


3.6। उपसंहार


कचरा संग्रह समाप्त हो गया है, ढेर फिर से लगभग सुसंगत है, अंतिम परिष्करण स्पर्श बाकी हैं:


 { GCTraceTime(Info, gc) time("Step 5: Epilogue", NULL); //    . preserved_marks.restore(); //   ,    . DerivedPointerTable::update_pointers(); BiasedLocking::restore_marks(); CodeCache::gc_epilogue(); JvmtiExport::gc_epilogue(); //     . if (!os::uncommit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size())) { log_warning(gc)("Could not uncommit native memory for marking bitmap"); } //    ,  . //        . if (EpsilonUncommit) { _virtual_space.shrink_by((_space->end() - new_top) * HeapWordSize); _space->set_end((HeapWord*)_virtual_space.high()); } } 

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


और, यदि आप वास्तव में चाहते हैं, तो आप आबंटनों के लिए मेमोरी को एक नए आकार में कम कर सकते हैं, जिससे ऑपरेटिंग सिस्टम को मेमोरी वापस मिल जाएगी!



4. जीसी को वीएम से कनेक्ट करें



4.1। रूट ट्रैवर्सल


याद रखें, आपको वीएम से विशेष, पहुंच योग्य लिंक को बायपास करने की आवश्यकता है? आप प्रत्येक विशेष वीएम सबसिस्टम को अन्य जावा वस्तुओं से छिपे लिंक को बायपास करने के लिए कह सकते हैं। वर्तमान हॉटस्पॉट में ऐसे मूल तत्वों की एक विस्तृत सूची कुछ इस तरह दिखती है:


 void EpsilonHeap::do_roots(OopClosure* cl) { //   ,        1 . StrongRootsScope scope(1); //         . CLDToOopClosure clds(cl, ClassLoaderData::_claim_none); MarkingCodeBlobClosure blobs(cl, CodeBlobToOopClosure::FixRelocations); //      . //        . { MutexLockerEx lock(CodeCache_lock, Mutex::_no_safepoint_check_flag); CodeCache::blobs_do(&blobs); } { MutexLockerEx lock(ClassLoaderDataGraph_lock); ClassLoaderDataGraph::cld_do(&clds); } Universe::oops_do(cl); Management::oops_do(cl); JvmtiExport::oops_do(cl); JNIHandles::oops_do(cl); WeakProcessor::oops_do(cl); ObjectSynchronizer::oops_do(cl); SystemDictionary::oops_do(cl); Threads::possibly_parallel_oops_do(false, cl, &blobs); } 

, . GC .



4.2.


GC , VM . Hotspot VM_Operation , GC VM- :


 // VM_operation,      class VM_EpsilonCollect: public VM_Operation { private: const GCCause::Cause _cause; EpsilonHeap* const _heap; static size_t _last_used; public: VM_EpsilonCollect(GCCause::Cause cause) : VM_Operation(), _cause(cause), _heap(EpsilonHeap::heap()) {}; VM_Operation::VMOp_Type type() const { return VMOp_EpsilonCollect; } const char* name() const { return "Epsilon Collection"; } virtual bool doit_prologue() { //     ,     . //         GC, //          . //   ,         //  .     , //       1%, ,  , //     . Heap_lock->lock(); size_t used = _heap->used(); size_t capacity = _heap->capacity(); size_t allocated = used > _last_used ? used - _last_used : 0; if (_cause != GCCause::_allocation_failure || allocated > capacity / 100) { return true; } else { Heap_lock->unlock(); return false; } } virtual void doit() { _heap->entry_collect(_cause); } virtual void doit_epilogue() { _last_used = _heap->used(); Heap_lock->unlock(); } }; size_t VM_EpsilonCollect::_last_used = 0; void EpsilonHeap::vmentry_collect(GCCause::Cause cause) { VM_EpsilonCollect vmop(cause); VMThread::execute(&vmop); } 

, GC — , .



4.3।


, GC , , GC , . , allocate_work , GC :


 HeapWord* EpsilonHeap::allocate_or_collect_work(size_t size) { HeapWord* res = allocate_work(size); if (res == NULL && EpsilonSlidingGC) { vmentry_collect(GCCause::_allocation_failure); res = allocate_work(size); } return res; } 

!



5.


OpenJDK.


 $ hg clone https://hg.openjdk.java.net/jdk/jdk/ jdk-jdk $ cd jdk-jdk $ curl https://shipilev.net/jvm/diy-gc/webrev/jdk-jdk-epsilon.changeset | patch -p1 

OpenJDK :


 $ ./configure --with-debug-level=fastdebug $ make images 

:


 $ build/linux-x86_64-server-fastdebug/images/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC -version openjdk version "13-internal" 2019-09-17 OpenJDK Runtime Environment (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon) OpenJDK 64-Bit Server VM (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon, mixed mode, sharing 


6.


, GC ? :


  1. . . Hotspot , JVM fastdebug , GC.
  2. . , . , ( ) , .
  3. टेस्ट। , , , . - , .

, , :


 $ CONF=linux-x86_64-server-fastdebug make images run-test TEST=gc/epsilon/ Building targets 'images run-test' in configuration 'linux-x86_64-server-fastdebug' Test selection 'gc/epsilon/', will run: * jtreg:test/hotspot/jtreg/gc/epsilon Running test 'jtreg:test/hotspot/jtreg/gc/epsilon' Passed: gc/epsilon/TestAlwaysPretouch.java Passed: gc/epsilon/TestAlignment.java Passed: gc/epsilon/TestElasticTLAB.java Passed: gc/epsilon/TestEpsilonEnabled.java Passed: gc/epsilon/TestHelloWorld.java Passed: gc/epsilon/TestLogTrace.java Passed: gc/epsilon/TestDieDefault.java Passed: gc/epsilon/TestDieWithOnError.java Passed: gc/epsilon/TestMemoryPools.java Passed: gc/epsilon/TestMaxTLAB.java Passed: gc/epsilon/TestPrintHeapSteps.java Passed: gc/epsilon/TestArraycopyCheckcast.java Passed: gc/epsilon/TestClasses.java Passed: gc/epsilon/TestUpdateCountersSteps.java Passed: gc/epsilon/TestDieWithHeapDump.java Passed: gc/epsilon/TestByteArrays.java Passed: gc/epsilon/TestManyThreads.java Passed: gc/epsilon/TestRefArrays.java Passed: gc/epsilon/TestObjects.java Passed: gc/epsilon/TestElasticTLABDecay.java Passed: gc/epsilon/TestSlidingGC.java Test results: passed: 21 TEST SUCCESS 

? fastdebug . ? - .



7.


- spring-petclinic , Apache Bench GC! , , GC .


: -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC :


:


 Heap: 20480M reserved, 20480M (100.00%) committed, 19497M (95.20%) used GC(2) Step 0: Prologue 2.085ms GC(2) Step 1: Mark 51.005ms GC(2) Step 2: Calculate new locations 71.207ms GC(2) Step 3: Adjust pointers 49.671ms GC(2) Step 4: Move objects 22.839ms GC(2) Step 5: Epilogue 1.008ms GC(2) GC Stats: 70561 (8.63%) reachable from roots, 746676 (91.37%) reachable from heap, 91055 (11.14%) moved, 2237 (0.27%) markwords preserved GC(2) Heap: 20480M reserved, 20480M (100.00%) committed, 37056K (0.18%) used GC(2) Lisp2-style Mark-Compact (Allocation Failure) 20479M->36M(20480M) 197.940ms 

200 ? GC! , . , , : ( — , ). - ( ).


, GC . , -Xlog:gc -XX:+UseSerialGC — , , :


 GC(46) Pause Young (Allocation Failure) 575M->39M(1943M) 2.603ms GC(47) Pause Young (Allocation Failure) 575M->39M(1943M) 2.606ms GC(48) Pause Young (Allocation Failure) 575M->39M(1943M) 2.747ms GC(49) Pause Young (Allocation Failure) 575M->39M(1943M) 2.578ms 

, 2 . , , GC . -Xlog:gc -XX:+UseSerialGC , , :


 GC(3) Pause Full (Allocation Failure) 16385M->34M(18432M) 1969.694ms GC(4) Pause Full (Allocation Failure) 16385M->34M(18432M) 2261.405ms GC(5) Pause Full (Allocation Failure) 16385M->34M(18432M) 2327.577ms GC(6) Pause Full (Allocation Failure) 16385M->34M(18432M) 2328.976ms 

, . .



8. ?


. , GC OpenJDK — , , .


:


  1. . , // . . , , « » , , .

    GC, java.lang.ref.Reference.referent — Java-, , , - . FinalReference , .
    ReferenceProcessor / / .
  2. VM. VM, , , . . , , , - , .


  3. . — , GC, . , , , .

    mark-compact GC Full GC fallbacks Shenandoah ( OpenJDK 8) G1 ( OpenJDK 10, JEP 307: «Parallel Full GC for G1» ).

  4. . , «» , , , - . . , .


  5. . , , , . , «» — «» «» , .


  6. - GC Handbook .




9.


? GC — , , , GC.


, - GC . , , GC (, Serial GC Parallel GC), .


विज्ञापन का मिनट। , 5-6 2019, JPoint — Java-. — OpenJDK, GraalVM, Kotlin . .

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


All Articles