صيد تسربات ذاكرة جافا

نشرت: 2022-03-11

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

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

بعبارة أخرى ، يصعب تحديد التسريبات أو تحديدها بعبارات محددة للغاية بحيث لا تكون مفيدة.

توجد في الواقع أربع فئات من مشكلات الذاكرة ذات الأعراض المتشابهة والمتداخلة ، ولكن هناك أسباب وحلول متنوعة:

  • الأداء : يرتبط عادةً بإنشاء كائن مفرط وحذفه ، وتأخيرات طويلة في جمع البيانات المهملة ، والتبديل المفرط لصفحات نظام التشغيل ، والمزيد.

  • قيود الموارد : تحدث عندما يكون هناك القليل من الذاكرة المتاحة أو أن ذاكرتك مجزأة جدًا بحيث لا يمكن تخصيص كائن كبير - يمكن أن يكون هذا أصليًا أو مرتبطًا بجافا بشكل أكثر شيوعًا.

  • تسريبات كومة Java : تسرب الذاكرة الكلاسيكي ، حيث يتم إنشاء كائنات Java باستمرار دون تحريرها. يحدث هذا عادة بسبب مراجع الكائنات الكامنة.

  • تسريبات الذاكرة الأصلية : مرتبطة بأي استخدام متزايد للذاكرة خارج كومة Java ، مثل عمليات التخصيص التي تم إجراؤها بواسطة كود JNI أو برامج التشغيل أو حتى تخصيصات JVM.

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

ولكن قبل أن تتمكن من منع تسرب الذاكرة والعثور عليه ، يجب أن تفهم كيف ولماذا تحدث. ( ملاحظة: إذا كان لديك تعامل جيد مع تعقيدات تسريبات الذاكرة ، فيمكنك التخطي للأمام. )

تسربات الذاكرة: كتاب تمهيدي

بالنسبة للمبتدئين ، فكر في تسرب الذاكرة على أنه مرض OutOfMemoryError (OOM ، للإيجاز) كعرض. ولكن كما هو الحال مع أي مرض ، لا تشير جميع OOMs بالضرورة إلى تسرب للذاكرة : يمكن أن تحدث OOM بسبب توليد عدد كبير من المتغيرات المحلية أو أحداث أخرى من هذا القبيل. من ناحية أخرى ، لا تظهر جميع حالات تسرب الذاكرة بالضرورة على أنها OOMs ، خاصة في حالة تطبيقات سطح المكتب أو تطبيقات العميل (التي لا تعمل لفترة طويلة بدون إعادة تشغيل).

فكر في تسريب الذاكرة على أنه مرض وأن خطأ OutOfMemoryError هو أحد الأعراض. ولكن لا تشير جميع أخطاء OutOfMemoryErr إلى حدوث تسرب في الذاكرة ، ولا تظهر جميع حالات تسرب الذاكرة نفسها على أنها أخطاء OutOfMemoryErrors.

لماذا هذه التسريبات سيئة للغاية؟ من بين أمور أخرى ، غالبًا ما يؤدي تسريب كتل من الذاكرة أثناء تنفيذ البرنامج إلى تدهور أداء النظام بمرور الوقت ، حيث يجب تبديل كتل الذاكرة المخصصة ولكن غير المستخدمة بمجرد نفاد الذاكرة الفعلية للنظام. في النهاية ، قد يستنفد البرنامج مساحة العنوان الافتراضية المتاحة ، مما يؤدي إلى OOM.

فك رموز OutOfMemoryError

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

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

  • java.lang.OutOfMemoryError: Java heap space

  • java.lang.OutOfMemoryError: PermGen space

  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit

  • java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

  • java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)

"مساحة كومة Java"

لا تعني رسالة الخطأ هذه بالضرورة حدوث تسرب للذاكرة. في الواقع ، يمكن أن تكون المشكلة بسيطة مثل مشكلة التكوين.

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

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

ينشأ مصدر محتمل آخر لـ OOMs "مساحة كومة Java" هذه مع استخدام المصححات النهائية . إذا كان للفصل طريقة finalize ، فلن يتم استعادة مساحة الكائنات من هذا النوع في وقت جمع البيانات المهملة. بدلاً من ذلك ، بعد جمع البيانات المهملة ، يتم وضع الكائنات في قائمة الانتظار للإنهاء ، والذي يحدث لاحقًا. في تطبيق Sun ، يتم تنفيذ Finalizers بخيط خفي. إذا لم يتمكن مؤشر ترابط Finalizer من مواكبة قائمة انتظار الإنهاء ، فيمكن أن تمتلئ كومة Java ويمكن إلقاء OOM.

"مساحة PermGen"

تشير رسالة الخطأ هذه إلى امتلاء الإنشاء الدائم. التوليد الدائم هو مساحة الكومة التي تخزن كائنات الفئة والطريقة. إذا قام أحد التطبيقات بتحميل عدد كبير من الفئات ، فقد يلزم زيادة حجم الجيل الدائم باستخدام الخيار -XX:MaxPermSize .

يتم أيضًا تخزين كائنات java.lang.String الداخلية في الجيل الدائم. تحتفظ فئة java.lang.String بمجموعة من السلاسل. عندما يتم استدعاء التابع intern ، تتحقق الطريقة من التجمع لترى ما إذا كانت هناك سلسلة مكافئة أم لا. إذا كان الأمر كذلك ، فسيتم إعادته بواسطة طريقة المتدرب ؛ إذا لم يكن كذلك ، يتم إضافة السلسلة إلى التجمع. بعبارات أكثر دقة ، تُرجع طريقة java.lang.String.intern التمثيل الأساسي لسلسلة نصية ؛ والنتيجة هي إشارة إلى نفس مثيل الفئة الذي سيتم إرجاعه إذا ظهرت هذه السلسلة على أنها حرفية. إذا كان أحد التطبيقات يتدرب على عدد كبير من السلاسل ، فقد تحتاج إلى زيادة حجم الجيل الدائم.

ملاحظة: يمكنك استخدام الأمر jmap -permgen لطباعة الإحصائيات المتعلقة بالإنشاء الدائم ، بما في ذلك معلومات حول مثيلات السلسلة الداخلية.

"حجم المصفوفة المطلوب يتجاوز حد VM"

يشير هذا الخطأ إلى أن التطبيق (أو واجهات برمجة التطبيقات المستخدمة بواسطة هذا التطبيق) حاول تخصيص مصفوفة أكبر من حجم الكومة. على سبيل المثال ، إذا حاول أحد التطبيقات تخصيص صفيف بحجم 512 ميجا بايت ولكن الحد الأقصى لحجم الكومة هو 256 ميجا بايت ، فسيتم طرح OOM مع رسالة الخطأ هذه. في معظم الحالات ، تكون المشكلة إما مشكلة تكوين أو خطأ ينتج عندما يحاول أحد التطبيقات تخصيص مصفوفة ضخمة.

"طلب <الحجم> بايت لـ <السبب>". هل نفدت مساحة المقايضة؟ "

يبدو أن هذه الرسالة OOM. ومع ذلك ، فإن HotSpot VM يطرح هذا الاستثناء الظاهري عندما يفشل تخصيص من كومة الذاكرة المؤقتة الأصلية وقد تكون الكومة الأصلية قريبة من الاستنفاد. تتضمن الرسالة الحجم (بالبايت) للطلب الذي فشل وسبب طلب الذاكرة. في معظم الحالات ، يكون <reason> هو اسم الوحدة النمطية المصدر التي تبلغ عن فشل التخصيص.

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

  • تم تكوين نظام التشغيل بمساحة مبادلة غير كافية.

  • عملية أخرى على النظام تستهلك جميع موارد الذاكرة المتاحة.

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

<السبب> <تتبع المكدس> (الطريقة الأصلية)

إذا رأيت رسالة الخطأ هذه وكان الإطار العلوي لتتبع المكدس طريقة أصلية ، فهذا يعني أن هذه الطريقة الأصلية واجهت فشلًا في التخصيص. الفرق بين هذه الرسالة والرسالة السابقة هو أنه تم اكتشاف فشل تخصيص ذاكرة Java في JNI أو طريقة أصلية بدلاً من كود Java VM.

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

تعطل التطبيق بدون OOM

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

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

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

تشخيص التسريبات

في معظم الحالات ، يتطلب تشخيص تسرب الذاكرة معرفة مفصلة للغاية بالتطبيق المعني. تحذير: يمكن أن تكون العملية طويلة ومتكررة.

ستكون استراتيجيتنا لتعقب حالات تسرب الذاكرة واضحة نسبيًا:

  1. تعرف على الأعراض

  2. تمكين مطول جمع القمامة

  3. تمكين التنميط

  4. تحليل التتبع

1. التعرف على الأعراض

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

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

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

2. تمكين Verbose Garbage Collection

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

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

إليك بعض المخرجات النموذجية التي تم إنشاؤها باستخدام خيار –verbosegc :

الإخراج المطول لجمع القمامة

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

3. تمكين التنميط

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

4. تحليل التتبع

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

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

كيف يعمل جمع القمامة في JVM؟

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

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

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

بناءً على هذا الافتراض ، تقوم Java بتقسيم الكائنات إلى أجيال متعددة. هنا تفسير مرئي:

أجزاء جافا إلى أجيال متعددة

  • جيل الشباب - هذا هو المكان الذي تبدأ فيه الأشياء. لها جيلان فرعيان:

    • عدن الفضاء - تبدأ الكائنات هنا. يتم إنشاء معظم الكائنات وتدميرها في Eden Space. هنا ، يقوم GC بعمل GCs الصغرى ، وهي مجموعات قمامة محسّنة. عند تنفيذ GC الثانوية ، يتم ترحيل أي إشارات إلى الكائنات التي لا تزال مطلوبة إلى إحدى مسافات الناجين (S0 أو S1).

    • Survivor Space (S0 and S1) - الكائنات التي نجت من عدن تنتهي هنا. هناك نوعان من هذه ، وواحد فقط قيد الاستخدام في أي وقت معين (ما لم يكن لدينا تسرب خطير للذاكرة). تم تحديد أحدهما على أنه فارغ ، والآخر على أنه حي ، بالتناوب مع كل دورة GC.

  • الجيل المؤيد - يُعرف أيضًا بالجيل القديم (المساحة القديمة في الشكل 2) ، تحتوي هذه المساحة على كائنات أقدم ذات أعمار أطول (يتم نقلها من أماكن الناجين ، إذا كانت تعيش لفترة كافية). عندما يتم ملء هذه المساحة ، يقوم GC بعمل GC كاملة ، والتي تكلف أكثر من حيث الأداء. إذا نمت هذه المساحة دون قيود ، فإن JVM سوف يرمي OutOfMemoryError - Java heap space .

  • الجيل الدائم - الجيل الثالث المرتبط ارتباطًا وثيقًا بالجيل الدائم ، يعتبر الجيل الدائم خاصًا لأنه يحتوي على البيانات المطلوبة بواسطة الجهاز الظاهري لوصف الكائنات التي ليس لها معادلة على مستوى لغة Java. على سبيل المثال ، يتم تخزين الكائنات التي تصف الفئات والطرق في الجيل الدائم.

تعد Java ذكية بما يكفي لتطبيق طرق مختلفة لجمع القمامة لكل جيل. يتم التعامل مع جيل الشباب باستخدام أداة جمع النسخ والقطع المتتبعة والتي يطلق عليها اسم Parallel New Collector . هذا الجامع يوقف العالم ، ولكن لأن جيل الشباب صغير بشكل عام ، فإن فترة التوقف قصيرة.

لمزيد من المعلومات حول أجيال JVM وكيفية عملها بمزيد من التفاصيل ، قم بزيارة إدارة الذاكرة في وثائق Java HotSpot Virtual Machine.

كشف تسرب الذاكرة

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

التنميط عن بعد الكومة باستخدام Java VisualVM

VisualVM هي أداة توفر واجهة مرئية لعرض معلومات مفصلة حول التطبيقات المستندة إلى تقنية Java أثناء تشغيلها.

باستخدام VisualVM ، يمكنك عرض البيانات المتعلقة بالتطبيقات المحلية وتلك التي تعمل على الأجهزة المضيفة البعيدة. يمكنك أيضًا التقاط بيانات حول مثيلات برنامج JVM وحفظ البيانات في نظامك المحلي.

من أجل الاستفادة من جميع ميزات Java VisualVM ، يجب عليك تشغيل Java Platform ، الإصدار القياسي (Java SE) الإصدار 6 أو أعلى.

ذات صلة: لماذا تحتاج إلى الترقية إلى Java 8 بالفعل

تمكين الاتصال عن بعد لـ JVM

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

أولاً ، نحتاج إلى منح أنفسنا وصولاً إلى JVM على الجهاز المستهدف. للقيام بذلك ، قم بإنشاء ملف يسمى jstatd.all.policy بالمحتوى التالي:

 grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };

بمجرد إنشاء الملف ، نحتاج إلى تمكين الاتصالات البعيدة إلى الجهاز الظاهري المستهدف باستخدام أداة jstatd - Virtual Machine jstat Daemon ، على النحو التالي:

 jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>

علي سبيل المثال:

 jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy

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

الاتصال بمضيف بعيد

في جهاز العميل ، افتح موجهًا واكتب jvisualvm لفتح أداة VisualVM.

بعد ذلك ، يجب أن نضيف مضيفًا بعيدًا في VisualVM. نظرًا لأنه تم تمكين JVM الهدف للسماح بالاتصالات عن بُعد من جهاز آخر باستخدام J2SE 6 أو أعلى ، فإننا نبدأ أداة Java VisualVM ونتصل بالمضيف البعيد. إذا كان الاتصال بالمضيف البعيد ناجحًا ، فسنرى تطبيقات Java التي تعمل في JVM الهدف ، كما هو موضح هنا:

تشغيل في jvm الهدف

لتشغيل ملف تعريف الذاكرة في التطبيق ، ما عليك سوى النقر نقرًا مزدوجًا فوق اسمه في اللوحة الجانبية.

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

MemLeak

بالطبع ، هناك عدد من الطرق لإنشاء تسرب للذاكرة في Java. من أجل التبسيط ، سنحدد فئة لتكون مفتاحًا في HashMap ، لكننا لن نحدد أساليب equals () و hashcode ().

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

من الضروري أن توفر فئة المفاتيح لدينا التنفيذ الصحيح لطرقتي equals equals() و hashcode() . بدونها ، ليس هناك ما يضمن إنشاء مفتاح جيد.

من خلال عدم تحديد عمليات equals() و hashcode() ، نضيف نفس المفتاح إلى HashMap مرارًا وتكرارًا ، وبدلاً من استبدال المفتاح كما ينبغي ، تنمو HashMap باستمرار ، وتفشل في تحديد هذه المفاتيح المتطابقة وإلقاء خطأ OutOfMemoryError .

ها هي فئة MemLeak:

 package com.post.memory.leak; import java.util.Map; public class MemLeak { public final String key; public MemLeak(String key) { this.key =key; } public static void main(String args[]) { try { Map map = System.getProperties(); for(;;) { map.put(new MemLeak("key"), "value"); } } catch(Exception e) { e.printStackTrace(); } } }

ملاحظة: لا يرجع تسرب الذاكرة إلى الحلقة اللانهائية في السطر 14: يمكن أن تؤدي الحلقة اللانهائية إلى استنفاد الموارد ، ولكن ليس تسرب الذاكرة. إذا قمنا بتطبيق أساليب equals equals() و hashcode() بشكل صحيح ، فستعمل الشفرة بشكل جيد حتى مع الحلقة اللانهائية حيث سيكون لدينا عنصر واحد فقط داخل HashMap.

(للمهتمين ، إليك بعض الوسائل البديلة لتوليد التسريبات (عن قصد)).

باستخدام Java VisualVM

باستخدام Java VisualVM ، يمكننا مراقبة الذاكرة في Java Heap وتحديد ما إذا كان سلوكها يشير إلى تسرب للذاكرة.

إليك تمثيل رسومي لمحلل Java Heap الخاص بـ MemLeak بعد التهيئة مباشرة (تذكر مناقشتنا للأجيال المختلفة):

مراقبة تسرب الذاكرة باستخدام java visualvm

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

تظهر إحدى وسائل الكشف عن سبب هذا التسرب في الصورة التالية ( انقر للتكبير ) ، التي تم إنشاؤها باستخدام Java VisualVM مع heapdump . هنا ، نرى أن 50٪ من كائنات Hashtable $ Entry موجودة في الكومة ، بينما يوجهنا السطر الثاني إلى فئة MemLeak . وبالتالي ، يحدث تسرب الذاكرة بسبب جدول التجزئة المستخدم في فئة MemLeak .

تسرب ذاكرة جدول التجزئة

أخيرًا ، لاحظ Java Heap بعد OutOfMemoryError مباشرة حيث تمتلئ الأجيال الشابة والقديمة تمامًا .

خطأ عدم وجود ذاكرة كافية

خاتمة

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

الملحق

إلى جانب Java VisualVM ، هناك العديد من الأدوات الأخرى التي يمكنها أداء اكتشاف تسرب الذاكرة. تعمل العديد من أجهزة الكشف عن التسريب على مستوى المكتبة عن طريق اعتراض المكالمات إلى إجراءات إدارة الذاكرة. على سبيل المثال ، HPROF ، عبارة عن أداة سطر أوامر بسيطة مرفقة مع Java 2 Platform Standard Edition (J2SE) لتوصيف الكومة ووحدة المعالجة المركزية. يمكن تحليل إخراج HPROF مباشرةً أو استخدامه كمدخل لأدوات أخرى مثل JHAT . عندما نعمل مع تطبيقات Java 2 Enterprise Edition (J2EE) ، هناك عدد من حلول محلل تفريغ الكومة الأكثر ودية ، مثل IBM Heapdumps لخوادم تطبيق Websphere.