ما تحتاج إلى معرفته أولا
1. يقوم مبرمجو C/C++ بإدارة الذاكرة بأنفسهم، بينما يتم استعادة ذاكرة Java تلقائيًا بواسطة GC.
على الرغم من أنني لست على دراية جيدة بـ C++، إلا أنني ربما لم أرتكب خطأً منطقيًا في هذا الأمر.
2. ما هو تسرب الذاكرة؟
ويشير تسرب الذاكرة إلى وجود ذاكرة في النظام لا يمكن إعادة تدويرها، مما يؤدي في بعض الأحيان إلى عدم كفاية الذاكرة أو تعطل النظام.
في C/C++، يحدث تسرب للذاكرة عندما لا يتم تحرير الذاكرة المخصصة.
3. يوجد تسرب للذاكرة في Java. يجب أن نعترف بذلك أولاً قبل أن نتمكن من مواصلة مناقشته. على الرغم من أن Java تعاني من تسرب الذاكرة، إلا أنك لا تحتاج إلى الاهتمام بها، خاصة أولئك الذين ليسوا مهتمين بالكود نفسه.
من المؤكد أن تسرب الذاكرة في Java يعني: وجود كائنات عديمة الفائدة لا يمكن لجامع البيانات المهملة إعادة تدويرها.
وحتى لو كانت هناك مشكلة تسرب للذاكرة، فقد لا تظهر.
4. يتم تمرير جميع المعلمات في Java حسب القيمة.
ليس هناك أي اعتراض على الأنواع الأساسية، ولكن لا يمكننا أن يكون لدينا أي اعتراض على الأنواع المرجعية.
تسرب ذاكرة جافا
1. تجاوز سعة ذاكرة الكومة (outOfMemoryError: مساحة كومة جافا)
في مواصفات JVM، يتم استخدام الذاكرة الموجودة في الكومة لإنشاء مثيلات ومصفوفات الكائن.
إذا تم تقسيم ذاكرة الكومة إلى أقسام فرعية، فيمكن أيضًا تقسيمها إلى جيل الشباب والجيل القديم. يشتمل الجيل الشاب على منطقة عدن ومنطقتين للناجين.
عند إنشاء كائن جديد، تكون عملية تطبيق الذاكرة كما يلي:
أ. يحاول jvm أولاً تخصيص الذاكرة المطلوبة للكائن الجديد في منطقة عدن؛
ب. إذا كان حجم الذاكرة كافيا، ينتهي التطبيق، وإلا فإن الخطوة التالية هي؛
ج. يبدأ JVM برنامج YoungGC ويحاول إطلاق كائنات غير نشطة في منطقة Eden بعد الإصدار، إذا كانت مساحة Eden لا تزال غير كافية لوضع كائنات جديدة، فإنه يحاول وضع بعض الكائنات النشطة في Eden في منطقة Survivor.
د. يتم استخدام منطقة الناجين كمنطقة تبادل وسيطة بين عدن والقديم عندما يكون لدى المنطقة القديمة مساحة كافية، سيتم نقل الكائنات الموجودة في منطقة الناجين إلى المنطقة القديمة، وإلا فسيتم الاحتفاظ بها في منطقة الناجين؛
هـ. عندما لا تكون هناك مساحة كافية في المنطقة القديمة، سيقوم JVM بتنفيذ GC بالكامل في المنطقة القديمة؛
f. بعد GC الكامل، إذا كانت مناطق Survivor وOLD لا تزال غير قادرة على تخزين بعض الكائنات المنسوخة من Eden، مما يتسبب في عدم قدرة JVM على إنشاء منطقة ذاكرة للكائنات الجديدة في منطقة Eden، فسيظهر "خطأ نفاد الذاكرة":
outOfMemoryError: مساحة كومة جافا
2. تجاوز سعة الذاكرة في منطقة الطريقة (outOfMemoryError: permgem space)
في مواصفات JVM، تقوم منطقة الطريقة بشكل أساسي بتخزين معلومات الفئة والثوابت والمتغيرات الثابتة وما إلى ذلك.
لذلك، إذا قام البرنامج بتحميل عدد كبير جدًا من الفئات، أو استخدم تقنية إنشاء الوكيل الديناميكي مثل الانعكاس أو gclib، فقد يتسبب ذلك في تجاوز سعة الذاكرة في هذه المنطقة. بشكل عام، رسالة الخطأ عند حدوث تجاوز سعة الذاكرة في هذه المنطقة هي:
outOfMemoryError: مساحة بيرجم
3. تجاوز سعة مكدس الخيط (java.lang.StackOverflowError)
مكدس الخيط عبارة عن بنية ذاكرة فريدة للخيط، لذا يجب أن تكون مشاكل مكدس الخيط عبارة عن أخطاء تم إنشاؤها عند تشغيل الخيط.
بشكل عام، يحدث تجاوز سعة مكدس مؤشر الترابط بسبب التكرار العميق جدًا أو وجود مستويات كثيرة جدًا من استدعاءات الأسلوب.
رسالة الخطأ عند حدوث تجاوز سعة المكدس هي:
java. لانج. StackOverflowError
عدة سيناريوهات لتسرب الذاكرة:
1. الكائنات طويلة العمر تحمل إشارات إلى كائنات قصيرة العمر.
هذا هو السيناريو الأكثر شيوعًا لتسرب الذاكرة ومشكلة شائعة في تصميم التعليمات البرمجية.
على سبيل المثال: إذا تم تخزين المتغيرات المحلية مؤقتًا في خريطة ثابتة عامة ولم تكن هناك عملية مسح، فستصبح الخريطة أكبر وأكبر بمرور الوقت، مما يتسبب في تسرب الذاكرة.
2. قم بتعديل قيمة المعلمة للكائن في مجموعة التجزئة، والمعلمة هي الحقل المستخدم لحساب قيمة التجزئة.
بعد تخزين كائن في مجموعة HashSet، لا يمكن تعديل الحقول الموجودة في الكائن والتي تشارك في حساب قيمة التجزئة، وإلا فإن قيمة التجزئة المعدلة للكائن ستكون مختلفة عن قيمة التجزئة عندما تم تخزينها في الأصل في مجموعة HashSet ، في هذه الحالة، حتى إذا كانت الطريقة تحتوي على المرجع الحالي للكائن كمعلمة لاسترداد الكائن من مجموعة HashSet، فسوف تُرجع النتيجة التي لا يمكن العثور على الكائن، مما سيؤدي أيضًا إلى الفشل. حذف الكائن الحالي من مجموعة HashSet، مما يتسبب في تسرب الذاكرة.
3. قم بتعيين عدد الاتصالات ووقت إيقاف تشغيل الجهاز
قد يؤدي فتح اتصال كثيف الاستخدام للموارد لفترة طويلة أيضًا إلى حدوث تسرب للذاكرة.
دعونا نلقي نظرة على مثال لتسرب الذاكرة:
public class Stack { public Object[] Elements=new Object[10]; public void Push(Object e){ EnsureCapacity(); items[size++] = e } public Object pop(){ if( size == 0) throw new EmptyStackException(); return items[--size]; = العناصر؛ العناصر = كائن جديد[2 * العناصر. الطول+1]; arraycopy(oldElements,0, Elements, 0, size);
يجب أن يكون المبدأ أعلاه بسيطًا جدًا. إذا تمت إضافة 10 عناصر إلى المكدس ثم تم إخراجها جميعًا، على الرغم من أن المكدس فارغ ولا يوجد شيء نريده، فلا يمكن إعادة تدوير هذا الكائن، وهذا يلبي متطلبات تسرب الذاكرة. الحالة: عديمة الفائدة، لا يمكن إعادة تدويرها.
ولكن حتى وجود مثل هذا الشيء قد لا يؤدي بالضرورة إلى أي عواقب إذا تم استخدام هذه المكدس بشكل أقل.
إنها مجرد مضيعة لعدد قليل من K من الذاكرة على أي حال، ذاكرتنا تصل بالفعل إلى G، فما هو التأثير الذي سيحدثه؟ علاوة على ذلك، سيتم إعادة تدوير هذا الشيء قريبًا، فما المهم؟ دعونا نلقي نظرة على مثالين أدناه.
مثال 1
public class Bad{ public static Stack s=Stack(); static{ s. Push(new Object()); pop(); // يوجد تسرب للذاكرة في الكائن هنا. Push(new Object()); // يمكن إعادة تدوير الكائن أعلاه، وهو ما يعادل الشفاء الذاتي}}
نظرًا لأنه ثابت، فإنه سيظل موجودًا حتى خروج البرنامج، ولكن يمكننا أيضًا أن نرى أن لديه وظيفة الشفاء الذاتي.
وهذا يعني أنه إذا كان المكدس الخاص بك يحتوي على 100 كائن على الأكثر، فلا يمكن إعادة تدوير 100 كائن فقط على الأكثر، في الواقع، يجب أن يكون من السهل فهم ذلك لأنه بمجرد أن نضع تقدمًا جديدًا، ستختفي المراجع السابقة بشكل طبيعي!
مثال 2
public class NotTooBad{ public void doSomething(){ Stack s=new Stack(); Push(new Object()); // رمز آخر s. pop();// يؤدي هذا أيضًا إلى عدم إمكانية إعادة تدوير الكائن وتسريب الذاكرة. }// الخروج من الطريقة، s غير صالح تلقائيًا، ويمكن إعادة تدوير s، والمراجع الموجودة داخل المكدس تختفي بشكل طبيعي، لذلك // يمكن أيضًا إجراء الشفاء الذاتي هنا، ويمكن القول أن هذه الطريقة لا تحتوي على مشكلة تسرب الذاكرة، ولكن سيتم تسليمها لاحقًا // يتم إعطاؤها فقط إلى GC، لأنها مغلقة وليست مفتوحة للعالم الخارجي الكود 99. 9999٪ من المواقف // لن يكون لها أي تأثير بالطبع، إذا كتبت مثل هذا الرمز، فلن يكون له أي آثار سيئة، ولكن // يمكن القول بالتأكيد أنه رمز قمامة! سأضيف واحدة فيها، لن يكون للحلقة الفارغة أي تأثير كبير، أليس كذلك؟}
المثالان المذكوران أعلاه تافهان فقط، لكن تسرب الذاكرة في C/C++ ليس سيئًا، ولكنه الأسوأ.
إذا لم يتم إعادة تدويرها في مكان واحد، فلن تتمكن أبدًا من إعادة تدويرها. إذا قمت باستدعاء هذه الطريقة بشكل متكرر، فسيتم استهلاك الذاكرة!
نظرًا لأن Java تتمتع أيضًا بوظيفة الإصلاح الذاتي (سميتها بنفسي ولم أتقدم بطلب للحصول على براءة اختراع بعد)، يمكن تجاهل مشكلة تسرب الذاكرة في Java تقريبًا، لكن يجب على أولئك الذين يعرفون ذلك ألا يرتكبوها.
لتجنب تسرب الذاكرة، يمكنك الرجوع إلى الاقتراحات التالية عند كتابة التعليمات البرمجية:
1. قم بإصدار المراجع إلى الأشياء عديمة الفائدة في أقرب وقت ممكن؛
2. استخدم معالجة السلسلة، وتجنب استخدام String، واستخدم StringBuffer على نطاق واسع، ويجب أن يشغل كل كائن سلسلة مساحة مستقلة من الذاكرة؛
3. استخدم أقل قدر ممكن من المتغيرات الثابتة، لأنه يتم تخزين المتغيرات الثابتة في الجيل الدائم (منطقة الطريقة)، ولا يشارك الجيل الدائم بشكل أساسي في جمع البيانات المهملة؛
4. تجنب إنشاء الكائنات في الحلقات؛
5. يمكن أن يؤدي فتح ملفات كبيرة أو أخذ الكثير من البيانات من قاعدة البيانات في وقت واحد إلى تجاوز سعة الذاكرة بسهولة، لذلك في هذه الأماكن، يجب عليك حساب الحد الأقصى لكمية البيانات تقريبًا، وتعيين الحد الأدنى والحد الأقصى لقيم مساحة الذاكرة المطلوبة.