جامع القمامة محلي الصنع لـ OpenJDK

هذه ترجمة للمقالة التي كتبها أليكسي شيبيليف "افعلها بنفسك (OpenJDK) Garbage Collector" ، والتي نُشرت بموافقة المؤلف. الإبلاغ عن أي أخطاء مطبعية وغيرها من الأخطاء في PM - سنقوم بإصلاحها.

عملية إنشاء شيء ما في وقت التشغيل هي ممارسة ممتعة. على الأقل إنشاء الإصدار الأول! تعد عملية إنشاء نظام فرعي موثوق به وعالي الأداء وآمن للفشل ، والذي يمكن ملاحظته وتصحيح سلوكه بسهولة ، مهمة بالغة الصعوبة.


إن إنشاء أداة تجميع مجمعات نفايات بسيطة أمر بسيط بشكل خادع ، والآن أريد القيام بذلك في هذه المقالة. أجرى رومان كينك في FOSDEM 2019 حديثًا وعرضًا بعنوان "الكتابة في GC في 20 دقيقة" باستخدام إصدار سابق من هذا التصحيح. على الرغم من أن الكود المطبق هناك يوضح الكثير ويتم التعليق عليه بكل وفرة ، هناك حاجة لوصف جيد رفيع المستوى لما يحدث - هكذا ظهرت هذه المقالة.


سيساعد الفهم الأساسي لعمل جامعي القمامة بشكل كبير في فهم ما هو مكتوب هنا. ستستخدم المقالة تفاصيل وأفكار في تطبيق معين لـ HotSpot ، ولكن لن تكون هناك دورة تمهيدية حول تصميم GC هنا. خذ كتيب GC وقراءة الفصول الأولى حول أساسيات GC ، وسيبدأ مقال Wikipedia بشكل أسرع.



المحتويات




1. ما يتكون من GC


الآن بعد أن تم كتابة العديد من GCs المختلفة ، أصبح من السهل جدًا إنشاء عناصر خاصة بك - يمكن إعادة استخدام (إعادة) العديد من العناصر المكتوبة بالفعل لتحويل بعض المخاوف بشأن تفاصيل التنفيذ إلى شفرة مجربة ومختبرة.



1.1. ابسيلون جي سي


يقدم OpenJDK 11 JEP 318 جديدًا: "Epsilon: A Gar-Collector Collector (التجريبية) . " وتتمثل مهمتها في توفير الحد الأدنى من التنفيذ للحالة عند تحرير الذاكرة ليست ضرورية أو محظورة. يناقش JEP بمزيد من التفاصيل لماذا قد يكون مفيدًا.


من وجهة نظر التنفيذ ، يعد "أداة تجميع البيانات المهملة" اسمًا سيئًا ، وسيكون من الأصح استخدام مصطلح "مدير الذاكرة التلقائي" ، المسؤول عن تخصيص الذاكرة وتحريرها. تطبق شركة إبسيلون جي سي "التخصيص" فقط ، ولا تتعامل مع "الإطلاق" على الإطلاق. لذلك ، يمكنك استخدام Epsilon GC والبدء في تنفيذ خوارزميات "الإطلاق" من البداية.



1.1.1. تخصيص الذاكرة


الجزء الأكثر تطوراً من إبسيلون جي سي هو المسؤول عن تخصيص الذاكرة . يخدم الطلبات الخارجية لتخصيص ذاكرة بحجم تعسفي وإنشاء مؤشر ترابط تخصيص مؤشر ترابط محلي (TLAB) بالحجم المطلوب. يحاول التنفيذ نفسه عدم توسيع TLAB كثيرًا ، حيث لن تكون هناك ذاكرة حرة ولن يقوم أحد بإرجاع البايتات المفقودة.



1.1.2. الحواجز


تتطلب بعض أدوات تجميع البيانات المهملة التفاعل مع التطبيق للحفاظ على متغيرات GC ، مما يفرض وقت التشغيل والتطبيق على إنشاء ما يسمى بالحواجز عند محاولة الوصول إلى الكومة. هذا صحيح بالنسبة لجميع المجمعين متعددة الخيوط ، وكذلك بالنسبة للعديد من هواة الجمع مع الأجيال ووقف العالم.


لا يتطلب إبسيلون حواجز ، لكن وقت التشغيل والمترجم لا يزال يريد أن يعرف أن الحواجز لا تفعل شيئًا. يمكن التعامل معها في كل مرة في كل مكان تكون متعبة. لحسن الحظ ، بدءًا من OpenJDK 11 ، هناك JEP-304 جديدة : "Garbage Collection Interface" ، مما يجعل إدخال الحواجز أسهل بكثير. على وجه الخصوص ، فإن الحاجز الذي تم تعيينه في إبسيلون فارغ ، ويمكن تفويض جميع الأعمال التافهة - حفظ ، تحميل ، CAS ، صفيف - إلى تطبيقات الحواجز التافهة من فئة فائقة موجودة. إذا كنت تقوم بإنشاء GC ولا تحتاج أيضًا إلى حواجز ، يمكنك ببساطة إعادة استخدام الرمز من إبسيلون.



1.1.3. مراقبة الاتصال


الجزء الأخير الممل من تطبيق القيادة العامة هو ربط مجموعة من آليات المراقبة داخل JVM: صناديق MX ، أوامر التشخيص ، إلخ. لقد فعلت Epsilon بالفعل كل هذا من أجلك.



1.2. وقت التشغيل و GC



1.2.1. عناصر الجذر


يحتاج جامع البيانات المهملة ، في الحالة العامة ، إلى معرفة بالضبط ما يحتوي في وقت تشغيل Java على مراجع الكومة. يمكن أن تكون هذه العناصر الجذرية ، التي تسمى جذور GC ، فتحات على مجموعات الدفق والمتغيرات المحلية (بما في ذلك تلك الموجودة في التعليمات البرمجية المترجمة من JIT!) والفئات الأصلية ومحمل الفصول الدراسية والمراجع في JNI وما إلى ذلك. يمكن أن تكون محاولات تحديد هذه العناصر معقدة ومملة للغاية. ولكن في Hotspot ، يتم تتبعهم جميعًا باستخدام أنظمة VM المناسبة ، بحيث يمكنك ببساطة معرفة كيفية عمل تطبيقات GC الحالية معهم. كذلك في النص سوف نرى ذلك.



1.2.2. كائن الزحف


يجب على جامع البيانات المهملة تجاوز الروابط الصادرة في كائنات Java. تم العثور على هذه العملية في كل مكان ، لذلك توفر الأجزاء الشائعة من وقت التشغيل حلولًا جاهزة ؛ لا تحتاج إلى كتابة أي شيء بنفسك. أدناه سيكون هناك قسم مع تطبيق معين ، ويمكنك أن تجد ، على سبيل المثال ، المكالمات obj→oop_iterate .



1.2.3. النزوح


يحتاج جامع البيانات المهملة المتحركة إلى كتابة العناوين الجديدة للكائنات المنقولة في مكان ما. هناك العديد من الأماكن التي يمكنك من خلالها كتابة بيانات إعادة التوجيه هذه.


  1. يمكنك إعادة استخدام "علامة الكلمة" في الكائن نفسه (المسلسل ، الموازي ، إلخ). بعد توقف العالم ، يتم التحكم في جميع عمليات الوصول إلى الكائن ، ويضمن أنه لا يوجد أي مؤشر Java يمكنه رؤية البيانات المؤقتة التي قررنا إدخالها في كلمة علامة. يمكنك إعادة استخدامها لتخزين بيانات إعادة التوجيه.
  2. يمكنك الاحتفاظ بجدول حركة أصلية منفصل ( ZGC ، C4 ، وغيرها). هذا يعزل GC تمامًا عن وقت التشغيل وبقية التطبيق ، حيث إن GC فقط يعلم بوجود مثل هذا الجدول. عادةً ما يستخدم المجمعون التنافسيون مثل هذا المخطط - فهم لا يريدون أن يعانون مع مجموعة من المشكلات غير الضرورية.
  3. يمكنك إضافة كلمة أخرى إلى الكائن ( Shenandoah وغيرها). هذا المزيج من النهجين السابقين لا يسمح فقط لوقت التشغيل والتطبيق بالعمل مع الرؤوس الموجودة دون مشاكل ، ولكن أيضًا يحفظ بيانات إعادة التوجيه.


1.2.4. بيانات علامة


يحتاج جامع البيانات المهملة إلى كتابة بيانات وضع العلامات في مكان ما. ومرة أخرى ، هناك عدة طرق لإنقاذهم:


  1. يمكنك إعادة استخدام كلمة علامة في الكائن نفسه (المسلسل ، Parallel ، إلخ). مرة أخرى ، في وضع إيقاف العالم ، يمكنك استخدام البتات في كلمة علامة لتشفير حقيقة تسمية. علاوة على ذلك ، إذا كنت بحاجة إلى الالتفاف على جميع الكائنات الحية ، فإننا نسير على طول الكومة ، كائن بعد كائن - وهذا ممكن نظرًا لحقيقة أن الكومة قابلة للتحليل .
  2. يمكنك الاحتفاظ بهيكل منفصل لتخزين بيانات الوسم (G1 ، Shenandoah ، إلخ). يتم ذلك عادةً باستخدام صورة نقطية منفصلة ، والتي تعيّن كل بايت N من الكومة إلى 1 بت من البطاقة. عادةً ، يتم محاذاة كائنات Java بمقدار 8 بايت ، لذا تقوم البطاقة بتخطيط كل 64 بت من الكومة إلى 1 بت من البطاقة ، بحيث تشغل 1/64 من حجم الكومة في الذاكرة الأصلية. هذه النفقات العامة تؤتي ثمارها جيدًا عند مسح الكومة بحثًا عن وجود كائنات حية ، وخاصة الكائنات المتفرقة: إن تجاوز الخريطة غالبًا ما يكون أسرع بكثير من تجاوز الكومة التي يتم تفكيكها كائنًا.
  3. قم بترميز الملصقات إلى روابط (ZGC ، C4 وغيرها). يتطلب ذلك التنسيق مع التطبيق ، فأنت بحاجة إلى قطع كل هذه التسميات عن الروابط أو القيام ببعض الحيل الأخرى للحفاظ على صحتها. بمعنى آخر ، نحتاج إما إلى حواجز أو بعض الأعمال الإضافية من GC.


2. الخطة العامة


على الأرجح ، أسهل تطبيق على قمة إبسيلون هو Mark-Compact ، بأسلوب LISP2. يتم وصف الفكرة الأساسية لهذا GC في ويكيبيديا وفي كتيب GC (الفصل 3.2). سيكون هناك رسم تخطيطي للخوارزمية في القسم مع التنفيذ أدناه ، لكنني أوصي بشدة بقراءة القليل من ويكيبيديا أو كتيب GC لفهم ما سنفعله.


الخوارزمية قيد البحث هي GC المناوبة: الكائنات المتحركة تتحرك في صفيف إلى بداية الكومة. لها إيجابيات وسلبيات:


  • يحافظ على ترتيب تخصيصات الذاكرة. هذا مفيد جدًا للتحكم في التنسيق في الذاكرة ، إذا كان الأمر يهمك (التحكم في النزوات ، فهذا هو وقتك!). الجانب السلبي هو أنك لن تحصل على رابط محلي تلقائي بهذه الطريقة.
  • تعقيدها هو O (N) من عدد الكائنات. ومع ذلك ، فإن الخطي يأتي بسعر: مطلوب من GC تجاوز مجموعة من 4 مرات لكل دورة بناء.
  • لا يتطلب ذاكرة حرة على الكومة! ليست هناك حاجة إلى حجز الذاكرة على الكومة لإخلاء الكائنات الحية ، حتى يمكنك العمل مع كومة الذاكرة المؤقتة التي فاضت بنسبة 99. (9) ٪. إذا تناولنا أفكارًا أخرى لهواة الجمع البسيطة ، على سبيل المثال ، الزبال ذو الفضاء شبه (زبال الفضاء شبه) ، فسيتعين علينا إعادة كتابة عرض الكومة قليلاً وحجز مساحة صغيرة للإخلاء ، ولكن هذا خارج نطاق هذا التمرين.
  • إذا كنت تعمل قليلاً حول هذه المشكلة ، فيمكنك تحقيق صفر من الذاكرة واستهلاك الوقت خلال الفترات التي يكون فيها GC غير نشط. يبدأ على ذاكرة في حالة عشوائية ، ويتوقف عن ضغطها بشكل كبير. يتناسب هذا بشكل جيد مع كيفية عمل إبسيلون: إنه يحتفظ بالتمييز فقط بعد الكائن الأخير. هذا أيضًا ناقص: تؤدي بعض الكائنات الميتة في بداية الكومة إلى عدد كبير من الحركات.
  • انها فقط لا تتطلب حواجز جديدة ، يمكنك إعادة استخدام EpsilonBarrierSet كما هي.

للبساطة ، سوف يستخدم تطبيق GC نقطة توقف كاملة للعالم (وقف العالم ، STW) ، ولن يكون هناك أجيال أو مؤشرات ترابط متعددة. في هذه الحالة ، من المنطقي استخدام صورة نقطية لتخزين العلامات وإعادة استخدام كلمة علامة لتخزين بيانات الحركة.



3. تنفيذ GC الأساسية


قد تكون قراءة وفهم التنفيذ الكامل معقدة للغاية بالنسبة لشخص جاهل. في هذا القسم ، سوف نفهم ذلك خطوة بخطوة.



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(); } 

بما أننا نستخدم صورة نقطية لتتبع قابلية الوصول إلى الكائنات ، نحتاج إلى مسحها قبل الاستخدام. أو في حالتنا ، نظرًا لأننا نهدف إلى عدم طلب الموارد مطلقًا قبل بدء دورة GC ، فسوف يتعين علينا الالتزام بنقط الصورة إلى الذاكرة مقدمًا. يوفر هذا العديد من المزايا المثيرة للاهتمام ، على الأقل على نظام Linux ، حيث تشير معظم الصورة النقطية إلى الصفحة صفر ، لا سيما بالنسبة للكوام المتفرق.


يجب على مؤشرات الترابط تحرير TLABs الخاصة بهم واطلب من GC أن تكون جديدة بعد اكتمال البناء.


لا تخلط بين TLAB و java.lang.ThreadLocal . من وجهة نظر GC ، فإن ThreadLocal هي كائنات عادية ، ولن يتم تجميعها بواسطة GC إلا إذا تطلب الأمر خلاف ذلك في كود Java.

بعض أجزاء وقت التشغيل ، خاصة تلك التي تحتوي على ارتباطات إلى كومة Java ، ستنقطع أثناء جمع القمامة ، لذا عليك تحذيرهم على وجه التحديد من أن GC ستبدأ العمل قريبًا. سيسمح ذلك للأنظمة الفرعية ذات الصلة بإعداد وحفظ جزء من حالتها قبل أن ينتقل GC.



3.2. وضع العلامات


يصبح وضع العلامات في وضع التوقف في العالم أمرًا بسيطًا للغاية عندما يكون كل شيء قد تم فعله بالفعل من أجلنا. يعتبر وضع العلامات قياسيًا ، والأرجح أنه في العديد من التطبيقات ، يعد GC هو الخطوة الأولى.


 { 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 ، "القمم" هي كائنات ، و "الحواف" هي روابط بينهما.


من الناحية الفنية ، يمكننا أن نذهب إلى الرسم البياني للكائنات بشكل متكرر ، ولكن هذه فكرة سيئة للرسوم البيانية التعسفية التي يمكن أن تحتوي على أقطار كبيرة جدًا. تخيل قائمة مرتبطة من مليار قمم! لذلك ، للحد من عمق العودية ، نستخدم كومة وسم تسجل الكائنات المكتشفة.


المجموعة الأولية من الكائنات القابلة للوصول هي جذور GC. الآن لا تتطرق إلى ماهية العملية ، أكثر في ما بعد. الآن ، دعنا نقول فقط أنه يتجاوز كل الروابط التي يمكن الوصول إليها من جانب VM.


تعمل الصورة النقطية مع العلامات على حد سواء كأداة لتسجيل واجهة الموجة الوسم (الكثير من الكائنات التي تمت زيارتها بالفعل) ، وفي النهاية - كمستودع للنتيجة المرغوبة ، مجموعة من جميع الكائنات القابلة للوصول. العمل الحقيقي يحدث في EpsilonScanOopClosure ، يتم تطبيقه على جميع الكائنات المثيرة للاهتمام وتكرارها على جميع روابط الكائن المحدد.


انظر ، عرفت Java كيفية الإغلاق (الإغلاق) قبل أن تصبح عصرية!

 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(); } 

الشيء الوحيد الذي يلفت انتباهك هو أننا قررنا تخزين عناوين جديدة في كلمة تمييز كائنات Java ، ويمكن أن تشغل هذه الكلمة بالفعل شيء مهم ، على سبيل المثال ، معلومات حول الأقفال. لحسن الحظ ، فإن مثل هذه الكلمات غير التوفيقية نادرة جدًا ، ويمكننا ببساطة تخزينها بشكل منفصل ، إذا لزم الأمر على الإطلاق: هذا ما تستخدمه العلامات 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(); } 

هناك نوعان من الإشارات إلى الكائنات التي تم نقلها: الصادرة إما من الكائنات الموجودة على الكومة نفسها أو من جذور 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); } 

بعد ذلك مباشرة ، يمكنك سحب كومة الذاكرة المؤقتة الخاصة بنقطة الضغط ، مما يتيح تخصيص ذاكرة مباشرة من هذا المكان ، فور انتهاء دورة تجميع البيانات المهملة.


لاحظ أنه في مجموعة التحويل ، يمكننا الكتابة فوق محتويات الكائنات الموجودة ، ولكن نظرًا لأن المسح يتم في نفس الاتجاه ، يتم نسخ الكائنات المكتوبة بالفعل إلى المكان الصحيح.


قد تتقاطع المواقع القديمة والجديدة لنفس المنشأة. على سبيل المثال ، إذا قمت بنقل كائن 100 بايت بواسطة 8 بايت. يجب أن يعمل إجراء النسخ بنفسه ، ويجب نسخ المحتوى المتقاطع بشكل صحيح ، 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. قم بتوصيل GC إلى VM



4.1. الجذر ترافيرسال


تذكر أنك تحتاج إلى تجاوز الروابط الخاصة التي يمكن الوصول إليها من VM؟ يمكنك أن تطلب من كل نظام فرعي خاص VM تجاوز الروابط المخفية عن كائنات Java الأخرى. تبدو قائمة شاملة بعناصر الجذر هذه في نقطة الاتصال الحالية مثل هذا:


 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/ar443250/


All Articles