java - خلق تسرب الذاكرة مع جافا




memory memory-leaks (20)

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

ماذا سيكون المثال؟


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

  • File.deleteOnExit() - تسرّب دائمًا السلسلة ، إذا كانت السلسلة عبارة عن سلسلة فرعية ، فإن التسرب أسوأ من ذلك (يتم أيضًا تسريب char الأساسي []) - في السلسلة 7 جافا أيضا نسخ char[] ، لذلك لا ينطبق في وقت لاحق ؛ @ دانيال ، لا يحتاج إلى أصوات ، على الرغم من.

سأركز على الخيوط لإظهار خطر الخيوط غير المدارة في الغالب ، لا ترغب في حتى اللمس التأرجح.

  • Runtime.addShutdownHook وإزالة ... ثم حتى مع removeShutdownHook بسبب خطأ في فئة ThreadGroup فيما يتعلق بالخيوط غير المتلائمة قد لا يتم تجميعها ، تسرب ThreadGroup بشكل فعال. JGroup لديه تسرب في GossipRouter.

  • إنشاء ، ولكن لا تبدأ ، يدخل Thread في نفس الفئة على النحو الوارد أعلاه.

  • إنشاء مؤشر ترابط ThreadGroup ContextClassLoader و AccessControlContext ، بالإضافة إلى ThreadGroup و أي InheritedThreadLocal ، كل هذه المراجع هي التسريبات المحتملة ، جنبا إلى جنب مع فئات كاملة تحميلها من كلدروفيرر وجميع المراجع الثابتة ، و ja-ja. يكون التأثير مرئيًا بشكل خاص مع إطار عمل jucExecutor بأكمله الذي يتميز بواجهة ThreadFactory بسيطة ThreadFactory ، ومع ذلك ، فإن معظم المطورين ليس لديهم أي فكرة عن الخطر ThreadFactory . أيضا الكثير من المكتبات تبدأ مؤشرات الترابط عند الطلب (طريقة الكثير من المكتبات الشعبية في الصناعة).

  • مخابئ ThreadLocal ؛ تلك شر في كثير من الحالات. أنا متأكد من أن الجميع قد رأوا قدرا كبيرا من المخابئ البسيطة القائمة على ThreadLocal ، وأيضا الأخبار السيئة: إذا استمر الخيط في الذهاب أكثر من المتوقع للحياة سياق ClassLoader ، فإنه تسرب بسيط جميل نقي. لا تستخدم ذاكرة التخزين المؤقت ThreadLocal ما لم تكن هناك حاجة حقا.

  • استدعاء ThreadGroup.destroy() عندما لا يحتوي ThreadGroup على مؤشرات الترابط نفسها ، ولكنه لا يزال يحتفظ ThreadGroups التابع. تسرب غير صالح يمنع ThreadGroup لإزالة من الأصل الخاص به ولكن كافة الأطفال تصبح un-enumerateable.

  • باستخدام WeakHashMap والقيمة (في) مراجع مباشرة المفتاح. هذا هو أحد الصعب العثور على دون تفريغ كومة الذاكرة المؤقتة. وينطبق ذلك على جميع Weak/SoftReference التي قد تحافظ على الرجوع الثابت إلى الكائن الذي Weak/SoftReference للحراسة.

  • باستخدام java.net.URL مع بروتوكول HTTP (S) وتحميل المورد من (!). هذا واحد هو خاص ، و KeepAliveCache إنشاء مؤشر ترابط جديد في ThreadGroup النظام الذي يتسبب في تدفق مصنف سلسلة المحادثات الحالية. يتم إنشاء مؤشر الترابط عند الطلب الأول عندما لا يوجد مؤشر ترابط قيد الحياة ، لذلك إما قد تحصل على الحظ أو تسرب فقط. التسرب تم إصلاحه بالفعل في Java 7 والرمز الذي يقوم بإنشاء مؤشر الترابط بشكل صحيح يزيل classloader السياق. هناك عدد قليل من الحالات ( مثل ImageFetcher ، ثابتة أيضا ) من خلق المواضيع المماثلة.

  • باستخدام InflaterInputStream تمرير new java.util.zip.Inflater() في المنشئ ( PNGImageDecoder على سبيل المثال) وعدم استدعاء end() من inflater. حسنا ، إذا كنت تمر في منشئ مع new فقط ، أي فرصة ... ونعم ، لا close() على تيار لا تغلق inflater إذا تم تمريره يدويا كمعلمة منشئ. هذا ليس تسربًا حقيقيًا لأنه سيتم تحريره من قبل finalizer ... عندما يراه ضروريًا. وحتى ذلك الحين ، فإنه يأكل الذاكرة المحلية بشكل سيء للغاية ويمكن أن يسبب لينكس oom_killer لقتل العملية مع الإفلات من العقاب. وتتمثل المشكلة الرئيسية في أن وضع اللمسات الأخيرة في جافا أمر لا يمكن الاعتماد عليه ، وجعل G1 أسوأ حتى 7.0.2. أخلاق القصة: إطلاق الموارد المحلية في أقرب وقت ممكن ؛ المتأهل هو فقير جدا.

  • نفس الحالة مع java.util.zip.Deflater . هذا هو أسوأ بكثير منذ Deflater هو الذاكرة الجياع في جاوة ، أي يستخدم دائما 15 بت (ماكس) و 8 مستويات الذاكرة (9 هو الحد الأقصى) تخصيص عدة مئات من KB من الذاكرة الأصلية. لحسن الحظ ، لا يستخدم Deflater على نطاق واسع وعلى حد علمي لا يحتوي JDK على أي سوء استخدام. دائما استدعاء end() إذا كنت يدويا Deflater أو Inflater . أفضل جزء من الأخيرين: لا يمكنك العثور عليها عبر أدوات التنميط العادية المتاحة.

(يمكنني إضافة المزيد من الوقت للوقت للراحة التي واجهتها عند الطلب.)

حظا موفقا ابقى في امان؛ التسريبات هي الشر!


تعتمد الإجابة كليًا على ما اعتقده القائم بالمقابلة أنهم يسألون.

هل من الممكن في الواقع لجعل جافا التسرب؟ بالطبع ، وهناك الكثير من الأمثلة في الإجابات الأخرى.

ولكن هناك العديد من الأسئلة الفوقية التي ربما تم طرحها؟

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

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

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


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

إذا كنت تريد هذه المفاتيح / العناصر السيئة لتعليقها يمكنك استخدام حقل ثابت مثل

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

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


معظم الأمثلة هنا "معقدة للغاية". هم حالات حافة. مع هذه الأمثلة ، ارتكب المبرمج خطأ (مثل عدم إعادة تعريف equals / hashcode) ، أو تم عضه بواسطة حالة ركنية من JVM / JAVA (تحميل الطبقة مع ثابت ...). أعتقد أن هذا ليس نوع المثال الذي يريده المحاور أو حتى الحالة الأكثر شيوعًا.

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

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

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

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

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

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


وإليك طريقة جيدة لإنشاء تسرب للذاكرة صحيح (كائنات لا يمكن الوصول إليها عن طريق تشغيل التعليمات البرمجية ولكن لا تزال مخزنة في الذاكرة) في جافا نقية:

  1. ينشئ التطبيق مؤشر ترابط تشغيل - طويلة (أو استخدام تجمع مؤشر ترابط إلى تسرب أسرع).
  2. يقوم مؤشر الترابط بتحميل فئة عبر ClassLoader (مخصص اختياري).
  3. يخصص الفصل جزءًا كبيرًا من الذاكرة (مثل new byte[1000000] ) ، ويقوم بتخزين مرجع قوي له في حقل ثابت ، ثم يقوم بتخزين مرجع إلى نفسه في ThreadLocal. تخصيص ذاكرة إضافية أمر اختياري (تسريب مثيل Class يكفي) ، ولكنه سيجعل التسريب يعمل بشكل أسرع.
  4. يقوم مؤشر الترابط بمسح كافة المراجع إلى الفئة المخصصة أو ClassLoader تم تحميله من.
  5. كرر.

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

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

هناك تباين في هذا النمط هو السبب في أن حاويات التطبيق (مثل Tomcat) يمكن أن تسرّب الذاكرة مثل المنخل إذا كنت تقوم بإعادة نشر التطبيقات التي تستخدم ThreadLocals بأي طريقة. (حيث تستخدم حاوية التطبيق مؤشرات الترابط كما هو موضح ، وفي كل مرة تقوم فيها بإعادة التطبيق ، يتم استخدام أداة ClassLoader جديدة.)

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


مرجع كائن احتجاز ثابت في حقل [esp field field]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

استدعاء String.intern() على سلسلة طويلة

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(غير مغلقة) دفق مفتوح (ملف ، شبكة ، إلخ ...)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

اتصالات غير مغلقة

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

المناطق التي يتعذر الوصول إليها من جامع البيانات المهملة في JVM ، مثل الذاكرة المخصصة من خلال الأساليب الأصلية

في تطبيقات الويب ، يتم تخزين بعض الكائنات في نطاق التطبيق حتى يتم إيقاف التطبيق أو إزالته بشكل صريح.

getServletContext().setAttribute("SOME_MAP", map);

خيارات JVM غير صحيحة أو غير مناسبة ، مثل خيار noclassgc على IBM JDK الذي يمنع مجموعة البيانات غير المستخدمة noclassgc

انظر إعدادات IBM jdk .


أعتقد أنه يمكن استخدام مثال صحيح متغيرات ThreadLocal في بيئة حيث يتم تجميع مؤشرات الترابط.

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

بالطبع ، بمجرد تحديدها ، يمكن حل المشكلة بسهولة.


ربما يبحث القائم بإجراء المقابلة عن حل مرجعي دائري:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

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

في تارلي


لقد واجهت مؤخرا حالة تسرب الذاكرة تسبب في طريق log4j.

يحتوي Log4j على هذه الآلية تسمى Nested Diagnostic Context (NDC) وهي أداة لتمييز إخراج السجل المشذّر من مصادر مختلفة. الدقة التي يعمل بها المركز هي خيوط ، لذلك فإنه يميز مخرجات السجل من خيوط مختلفة بشكل منفصل.

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

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

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


إليك رسالة بسيطة / شريرة عبر http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

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

طريقة تجنب تخزين مرجع غير مرغوب فيه للسلسلة الأصلية هي القيام بشيء كالتالي:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

لمزيد من السوء ، يمكنك أيضًا أن .intern()تكون سلسلة فرعية:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

سيؤدي القيام بذلك إلى الاحتفاظ بالسلسلة الطويلة الأصلية والسلسلة الفرعية المشتقة في الذاكرة حتى بعد التخلص من مثيل StringLeaker.


إنشاء خريطة ثابتة ومواصلة إضافة مراجع صلبة إليها. هذه لن تكون أبدا GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

الجميع ينسى دائما مسار الشفرة الأصلية. فيما يلي صيغة بسيطة للتسرب:

  1. تعلن الطريقة الأصلية.
  2. في الطريقة المحلية ، اتصل malloc. لا ندعو free.
  3. استدعاء الأسلوب الأصلي.

تذكر ، تأتي تخصيصات الذاكرة في تعليمة برمجية أصلية من كومة JVM.


جئت عبر نوع من تسرب أكثر دقة من الموارد في الآونة الأخيرة. نحن نفتح الموارد عبر getResourceAsStream الخاص بصنف الطبقة ، وقد حدث أن مقابض تدفق المدخلات لم تكن مغلقة.

أوم ، قد تقول ، ما أحمق.

حسنا ، ما يجعل هذا مثير للاهتمام هو: بهذه الطريقة ، يمكنك تسريب ذاكرة الكومة العملية الأساسية ، بدلا من كومة JVM.

كل ما تحتاجه هو ملف jar مع ملف بداخل والذي سيتم الإشارة إليه من Java البرمجية. كلما كبر ملف الجرة ، يتم تخصيص الذاكرة الأسرع.

يمكنك إنشاء هذه الجرة بسهولة باستخدام الصف التالي:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

ما عليك سوى لصقها في ملف باسم BigJarCreator.java ، وتجميعه وتشغيله من سطر الأوامر:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: تجد أرشيف jar في دليل عملك الحالي مع ملفين بالداخل.

دعونا نخلق فئة ثانية:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

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

إذا كنت مشكوكًا في ذلك ، فحاول تجميع الصفوف وبدءها أعلاه ، ولكن تأكد من اختيار حجم مناسب للكرة (2 ميغابايت):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

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

إذا كنت تريد أن يكون التطبيق آمنًا ، فأغلق تدفق الإدخال مباشرةً حيث تم إنشاؤه:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

ولن تتجاوز العملية 35 ميغابايت ، بغض النظر عن عدد التكرارات.

بسيط جدا ومدهش.


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

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

إحدى الطرق التي استخدمت للعمل - ولا أعرف ما إذا كان لا يزال - تعمل على إنشاء سلسلة دائرية ثلاثية. كما في Object A يحتوي على مرجع إلى كائن B ، يحتوي الكائن B على مرجع إلى الكائن C و C الكائن يحتوي على مرجع إلى كائن A. كان GC ذكيًا بما يكفي لمعرفة سلسلتين عميقتين - كما في A <-> B - يمكن جمعها بأمان إذا كان A و B غير قابل للوصول من قبل أي شيء آخر ، ولكن لا يمكن التعامل مع السلسلة الثلاثية ...


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


لقد حصلت على "تسرب للذاكرة" لطيف بالنسبة إلى تحليل PermGen و XML مرة واحدة. محلل XML الذي استخدمناه (لم أكن أتذكر أي واحد كان) قام به String.intern () على أسماء العلامات ، لإجراء المقارنة بشكل أسرع. كان لدى أحد عملائنا فكرة رائعة لتخزين قيم البيانات لا في سمات XML أو نصها ، ولكن كأسماء علامات ، لذلك كان لدينا وثيقة مثل:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

في الواقع ، لم يستخدموا أرقامًا ، بل أرقامًا تعريفية أطول (حوالي 20 حرفًا) ، والتي كانت فريدة من نوعها ، وجاءت بمعدل 10-15 مليونًا في اليوم. وهذا يجعل 200 ميغابايت من القمامة في اليوم ، والتي لا حاجة إليها مرة أخرى ، ولم يسبق لـ GCed (حيث توجد في PermGen). كان لدينا permgen لتعيين 512 ميغابايت ، لذلك استغرق الأمر حوالي يومين للاستثناء خارج الذاكرة (OOME) للوصول ...


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


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

public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

يمكنك إنشاء تسرب للذاكرة المتحركة عن طريق إنشاء مثيل جديد لفئة في طريقة finalize للفئة. نقاط المكافأة إذا قام صانع الصيغة بإنشاء نسخ متعددة. إليك برنامج بسيط يسرب كومة الذاكرة المؤقتة بالكامل في وقت ما بين بضع ثوانٍ وبضع دقائق حسب حجم كومة الذاكرة المؤقتة:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}




memory-leaks