المقدمة
عادةً ما يُطلق على تسرب الذاكرة الحالة التي تزداد فيها مساحة الذاكرة المشغولة في كومة الذاكرة المؤقتة أثناء تشغيل التطبيق على المدى الطويل ولا تنقص بعد خروج Garbage Collector. كما تعلم ، تنقسم ذاكرة jvm إلى كومة ومكدس. يخزن المكدس قيم المتغيرات من الأنواع البسيطة والمراجع للكائنات في سياق الدفق ، ويقوم الكومة بتخزين الكائنات نفسها. يوجد أيضًا في الكومة مساحة تسمى Metaspace ، والتي تخزن البيانات حول الفئات المحملة والبيانات المرتبطة بالفئات نفسها ، وليس مثيلاتها ، على وجه الخصوص ، قيم المتغيرات الثابتة. يقوم جامع القمامة (المشار إليه فيما يلي باسم GC) ، والذي يتم تشغيله بشكل دوري بواسطة آلة جافا ، بالعثور على الكائنات في الكومة التي لم يعد يشار إليها وتحرر الذاكرة التي تشغلها هذه الكائنات. خوارزميات عمل GC مختلفة ومعقدة ، على وجه الخصوص ،في المرة التالية التي يبدأ فيها GC ، لا "يفحص" الكومة بأكملها في كل مرة للعثور على كائنات غير مستخدمة ، لذلك لا يستحق الاعتماد على حقيقة أنه سيتم إزالة أي كائن غير مستخدم من الذاكرة بعد بدء تشغيل GC واحد ، ولكن إذا كان حجم الذاكرة المستخدمة بواسطة التطبيق ثابتًا ينمو دون سبب واضح لفترة طويلة ، ثم حان الوقت للتفكير في ما يمكن أن يؤدي إلى مثل هذا الموقف.
يشتمل jvm على أداة Visual VM متعددة الوظائف (يشار إليها فيما يلي باسم VM). يسمح لك VM بمراقبة ديناميكيات المؤشرات الرئيسية لـ jvm في الرسوم البيانية ، على وجه الخصوص ، مقدار الذاكرة الحرة والمشغولة في الكومة ، وعدد الفئات المحملة ، والخيوط ، إلخ. بالإضافة إلى ذلك ، باستخدام الجهاز الظاهري ، يمكنك فحص تفريغ الذاكرة وفحصها. بالطبع ، يسمح VM أيضًا بإغراق الخيوط وتنميط التطبيق ، ولكن نظرة عامة على هذه الميزات خارج نطاق هذه المقالة. كل ما نحتاجه من الجهاز الظاهري في هذا المثال هو الاتصال بالجهاز الظاهري وإلقاء نظرة أولاً على الصورة العامة لاستخدام الذاكرة. أود أن أشير إلى أنه لتوصيل جهاز افتراضي بخادم بعيد ، يجب تكوين معلمات jmxremote عليه ، نظرًا لأن الاتصال يتم عبر jmx.للحصول على وصف لهذه المعلمات ، يمكنك الرجوع إلى وثائق Oracle الرسمية أو العديد من المقالات حول Habré.
لذلك ، لنفترض أننا نجحنا في الاتصال بخادم التطبيق باستخدام VM وإلقاء نظرة على الرسوم البيانية.
في علامة التبويب الكومة ، يمكنك رؤية إجمالي الذاكرة المستخدمة وذاكرة jvm. وتجدر الإشارة إلى أن علامة التبويب هذه تأخذ أيضًا في الاعتبار ذاكرة نوع Metaspace (حسنًا ، كيف بخلاف ذلك ، لأن هذا أيضًا كومة). تعرض علامة التبويب Metaspace معلومات فقط حول الذاكرة التي تشغلها البيانات الوصفية (حسب الفئات نفسها والكائنات المرتبطة بها).
بالنظر إلى الرسم البياني ، يمكننا أن نرى أن إجمالي ذاكرة الكومة هو 10 غيغابايت ، والمساحة المشغولة حاليًا تبلغ 5.8 غيغابايت. تتوافق الحواف في الرسم البياني مع مكالمات GC ، ويمكن لخط مستقيم تقريبًا (بدون حواف) يبدأ حوالي الساعة 10:18 (ولكن ليس بالضرورة!) الإشارة إلى أن خادم التطبيق لم يكن يعمل منذ ذلك الوقت ، نظرًا لعدم وجود تخصيص نشط وإلغاء تخصيص ذاكرة. بشكل عام ، يتوافق هذا الرسم البياني مع التشغيل العادي لخادم التطبيق (إذا كان ، بالطبع ، للحكم على العمل من الذاكرة فقط). سيكون الرسم البياني للمشكلة واحدًا حيث سيكون الخط الأزرق الأفقي المستقيم بدون حواف عند المستوى البرتقالي تقريبًا ، والذي يمثل أقصى قدر من الذاكرة في الكومة.
لنلقِ الآن نظرة على رسم بياني آخر.
هنا نأتي مباشرة إلى تحليل المثال ، وهو الموضوع الرئيسي لهذه المقالة. يوضح الرسم البياني للفئات عدد الفئات التي تم تحميلها في Metaspace ، وهو حوالي 73 ألف عنصر. أود أن ألفت انتباهك إلى حقيقة أننا لا نتحدث عن حالات الفصل ، ولكن عن الفئات نفسها ، أي كائنات من النوع Class <؟>. لا يُظهر الرسم البياني عدد مثيلات كل نوع فردي من ClassA أو ClassB يتم تحميلها في الذاكرة. ربما يتضاعف عدد الفئات المتطابقة من النوع ClassA لسبب ما؟ يجب أن أقول أنه في المثال الذي سيتم وصفه أدناه ، كان 73000 فصل فريد من نوعه حالة طبيعية تمامًا.
الحقيقة هي أنه في أحد المشاريع التي شارك فيها مؤلف هذه المقالة ، تم تطوير آلية لوصف عالمي لكيانات المجال (مثل في 1C) تسمى نظام القاموس ، والمحللين الذين يخصصون النظام لعميل معين أو لمنطقة عمل معينة ، أتيحت الفرصة ، من خلال محرر خاص ، لنمذجة نموذج عمل من خلال إنشاء كيانات جديدة ومتغيرة موجودة ، لا تعمل على مستوى الجداول ، ولكن بمفاهيم مثل "المستند" ، "الحساب" ، "الموظف" ، إلخ. أنشأ نواة النظام جداول في DBMS علائقية لبيانات الكيان ، ويمكن إنشاء عدة جداول لكل كيان ، نظرًا لأن النظام العالمي سمح تاريخيًا بتخزين قيم السمات والمزيد يتطلب إنشاء جداول خدمة إضافية في قاعدة البيانات.
أعتقد أن أولئك الذين اضطروا للعمل مع أطر عمل ORM قد خمّنوا بالفعل ما يدور حوله المؤلف ، وصرف انتباههم عن الموضوع الرئيسي للمقال من خلال الحديث عن الجداول. استخدم المشروع وضع السبات وكان لابد من وجود فئة فول كيان لكل جدول. في الوقت نفسه ، نظرًا لأن الجداول الجديدة تم إنشاؤها ديناميكيًا أثناء عمل النظام من قبل المحللين ، فقد تم إنشاء فئات Hibernate bean ، ولم تتم كتابتها يدويًا بواسطة المطورين. ومع كل جيل قادم ، تم إنشاء حوالي 50-60 ألف فصل جديد. كان هناك عدد أقل بكثير من الجداول في النظام (حوالي 5-6 آلاف) ، ولكن بالنسبة لكل جدول ، لم يتم إنشاء فئة فول الكيان فحسب ، بل أيضًا العديد من الفئات المساعدة ، مما أدى في النهاية إلى رقم مشترك.
كانت آلية العمل على النحو التالي. في بداية النظام ، تم إنشاء فئات فول الكيان والفئات المساعدة (المشار إليها فيما يلي ببساطة فئات الفول) بناءً على البيانات الوصفية في قاعدة البيانات. عندما كان النظام قيد التشغيل ، قام مصنع جلسة Hibernate بإنشاء الجلسات ، وأنشأت الجلسات مثيلات لكائنات فئة الفول. عند تغيير الهيكل (إضافة ، تغيير الجداول) ، تم إعادة إنشاء فئات الفول وإنشاء مصنع جلسة جديد. بعد التجديد ، أنشأ المصنع الجديد جلسات جديدة تستخدم فئات الفول الجديدة ، وتم إغلاق المصنع القديم والجلسات ، وتم إلغاء تحميل فئات الفول القديمة بواسطة GC ، حيث لم يعد يتم الرجوع إليها من كائنات البنية التحتية للإسبات.
في مرحلة ما ، ظهرت مشكلة تتمثل في أن عدد فئات الحاويات بدأ في الزيادة بعد كل عملية تجديد تالية. من الواضح أن هذا يرجع إلى حقيقة أن مجموعة الفئات القديمة ، والتي لم يعد يجب استخدامها ، لسبب ما لم يتم تفريغها من الذاكرة. من أجل فهم أسباب هذا السلوك للنظام ، جاء محلل ذاكرة الكسوف (MAT) لمساعدتنا.
البحث عن تسرب للذاكرة
تعرف MAT كيفية التعامل مع عمليات تفريغ الذاكرة ، وإيجاد المشاكل المحتملة فيها ، ولكن عليك أولاً الحصول على تفريغ الذاكرة هذا ، ولكن في البيئات الحقيقية ، هناك بعض الفروق الدقيقة في الحصول على تفريغ.
إزالة تفريغ الذاكرة
كما ذكرنا سابقًا ، يمكن إزالة تفريغ الذاكرة مباشرة من الجهاز الظاهري بالضغط على الزر
لكن ، نظرًا لحجم التفريغ الكبير ، فقد لا يتعامل الجهاز الظاهري مع هذه المهمة ، ويتجمد بعض الوقت بعد الضغط على زر تفريغ الكومة. بالإضافة إلى ذلك ، ليس من الحقائق على الإطلاق أنه سيكون من الممكن الاتصال عبر jmx بخادم تطبيق المنتج المطلوب لجهاز VM. في هذه الحالة ، هناك أداة مساعدة أخرى من jvm ، والتي تسمى jMap ، تأتي للإنقاذ. أنه يعمل على سطر الأوامر، مباشرة على الملقم حيث JVM قيد التشغيل، ويسمح لك لتعيين المعلمات تفريغ إضافية:
jmap تفريغ: الحية، شكل = ب، ملف = / تمة / heapdump.bin 14616
و تفريغ: الحية المعلمة هي في غاية الأهمية، لأن يسمح لك بتقليل حجمه بشكل كبير ، باستثناء الكائنات التي لم يعد يشار إليها.
هناك موقف شائع آخر عندما لا يكون الإغراق اليدوي ممكنًا نظرًا لحقيقة أن jvm نفسه يتعطل بسبب OutOfMemoryError. في هذه الحالة ، يأتي الخيار -XX: + HeapDumpOnOutOfMemoryError للإنقاذ ، بالإضافة إلى -XX: HeapDumpPath ، والذي يسمح لك بتحديد المسار إلى التفريغ الملتقط.
بعد ذلك ، افتح التفريغ الملتقط باستخدام محلل ذاكرة Eclipse. يمكن أن يكون حجم الملف كبيرًا (عدة غيغابايت) ، لذلك تحتاج إلى توفير ذاكرة كافية في ملف
MemoryAnalyzer.ini : -Xmx4096m
توطين المشكلة باستخدام MAT
لذلك ، دعنا نفكر في الموقف الذي يتضاعف فيه عدد الفئات المحملة مقارنة بالمستوى الأولي ولا ينقص حتى بعد استدعاء قسري لجمع القمامة (يمكن القيام بذلك عن طريق الضغط على الزر المقابل في الجهاز الظاهري).
أعلاه ، تم وصف عملية تجديد فئات الفول واستخدامها من الناحية المفاهيمية. على مستوى أكثر تقنية ، بدا الأمر كما يلي:
- جميع جلسات السبات مغلقة (فئة SessionImpl)
- تم إغلاق مصنع الجلسة القديم (SessionFactoryImpl) وإعادة تعيين المرجع إليه من LocalSessionFactoryBean
- تمت إعادة إنشاء ClassLoader
- تم إلغاء الإشارات إلى فئات الفول القديمة في فئة المولد
- يتم تجديد فئات الفول
في حالة عدم وجود إشارات إلى فئات الفول القديمة ، يجب ألا يزيد عدد الفصول بعد جمع القمامة.
ابدأ تشغيل MAT وافتح ملف تفريغ الذاكرة الذي تم الحصول عليه مسبقًا. بعد فتح التفريغ ، تعرض MAT أكبر سلاسل من الكائنات في الذاكرة.
بعد النقر فوق Leak Suspects ، نرى التفاصيل:
مقطعان دائريان بحجم 265 مترًا يمثل كل منهما مثيلين من SessionFactoryImpl. ليس من الواضح سبب وجود مثيلين لهما ، وعلى الأرجح ، تحتوي كل حالة على إشارات إلى المجموعة الكاملة من فئات وحدات فول الكيان. تخبرنا MAT بالمشاكل المحتملة على النحو التالي.
ألاحظ على الفور أن المشكلة المشتبه بها 3 ليست مشكلة في الحقيقة. قام المشروع بتنفيذ محلل لغته الخاصة ، وهو عبارة عن إضافة متعددة الأنظمة الأساسية عبر SQL ويسمح لك بالعمل ليس مع الجداول ، ولكن مع كيانات النظام ، ويحتل 121M ذاكرة التخزين المؤقت للاستعلام.
دعنا نعود إلى حالتين من SessionFactoryImpl. انقر فوق فئات مكررة ولاحظ أن هناك بالفعل مثيلين لكل فئة من فئات وحدة برامج الكيان. أي أن الروابط إلى الفئات القديمة من وحدات فول الكيان لا تزال قائمة ، وعلى الأرجح هذه روابط من SesssionFactoryImpl. بناءً على الكود المصدري لهذه الفئة ، يجب تخزين الإشارات إلى فئات الفول في حقل classMetaData.
انقر فوق المشكلة المشتبه بها 1 ، ثم على فئة SessionFactoryImpl وحدد List Objects-> With outgouing المراجع من قائمة السياق. بهذه الطريقة يمكننا رؤية جميع الكائنات المشار إليها بواسطة SessionFactoryImpl.
قم بتوسيع كائن classMetaData وتأكد من أنه يخزن بالفعل مجموعة من فئات وحدات برامج Entity.
نحن الآن بحاجة إلى فهم ما الذي يمنع جامع القمامة من التخلص من مثيل واحد من SessionFactoryImpl. إذا عدنا إلى التسريب المشتبه به-> التسريبات-> المشكلة المشتبه بها 1 ، فسنرى مجموعة من الروابط التي تؤدي إلى ارتباط إلى SessionFactoryImpl.
نرى أن متغير الكيان من وحدة فول SessionInfoImpl الذي يحتوي على سياق جلسة HTTP يحتوي على مصفوفة dbTransactionListeners تستخدم كائنات Hibernate SessionImpl كمفاتيح ، وتشير الجلسات إلى SessionFactoryImpl.
الحقيقة هي أنه تم تخزين كائنات الجلسة مؤقتًا في dbTransactionListeners لأغراض معينة ، وقبل إعادة إنشاء فئات الفول ، يمكن أن تظل الإشارات إليها في هذه المصفوفة. أشارت الجلسات بدورها إلى مصنع الجلسة ، الذي قام بتخزين مجموعة من المراجع لجميع فئات الفول. بالإضافة إلى ذلك ، احتفظت الجلسات بالإشارات إلى أمثلة فئات الكيانات ، وأشارت إلى فئات الفول نفسها.
وبالتالي ، تم العثور على نقطة الدخول إلى المشكلة. اتضح أنها إشارات إلى جلسات قديمة من dbTransactionListeners. بعد إصلاح الخطأ وبدء مسح صفيف dbTransactionListeners ، تم إصلاح المشكلة.
ميزات محلل ذاكرة الكسوف
لذلك ، يتيح لك محلل ذاكرة Eclipse:
- اكتشف أي سلاسل من الكائنات تشغل أقصى قدر من الذاكرة وحدد نقاط الدخول إلى هذه السلاسل (مشتبه في التسرب)
- عرض شجرة لجميع مراجع الكائنات الواردة (أقصر المسارات إلى نقطة التراكم)
- عرض شجرة لجميع المراجع الخارجية لكائن ما (Object-> List Objects-> with outgouing المراجع)
- انظر الفئات المكررة التي تم تحميلها بواسطة ClassLoaders مختلفة (فئات مكررة)