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




memory memory-leaks (25)

جئت عبر نوع من تسرب أكثر دقة من الموارد في الآونة الأخيرة. نحن نفتح الموارد عبر 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 ميغابايت ، بغض النظر عن عدد التكرارات.

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

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

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


مرجع كائن احتجاز ثابت في حقل [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 .


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

  • 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 . أفضل جزء من الأخيرين: لا يمكنك العثور عليها عبر أدوات التنميط العادية المتاحة.

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

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


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


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


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

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

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

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

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

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

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


إنشاء خريطة ثابتة ومواصلة إضافة مراجع صلبة إليها. هذه لن تكون أبدا 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); }
}

يمكنك إجراء تسرب للذاكرة مع sun.misc.Unsafe class. في الواقع ، يتم استخدام فئة الخدمة هذه في فئات قياسية مختلفة (على سبيل المثال في فئات java.nio ). لا يمكنك إنشاء مثيل لهذا الفصل مباشرةً ، ولكن يمكنك استخدام التفكير للقيام بذلك .

لا يتم ترجمة التعليمات البرمجية في Eclipse IDE - قم بتجميعها باستخدام javac command (أثناء التحويل البرمجي ستحصل على تحذيرات)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

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

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

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


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

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

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

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

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

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


ربما أحد أبسط الأمثلة على تسرب للذاكرة المحتملة ، وكيفية تجنبها ، هو تنفيذ ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

إذا كنت تقوم بتطبيقه بنفسك ، فهل فكرت في مسح عنصر الصفيف الذي لم يعد مستخدمًا ( elementData[--size] = null )؟ قد تحافظ هذه الإشارة على كائن ضخم على قيد الحياة ...


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

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

الآن في حالة استدعاء Example1 والحصول على Example2 تجاهل Example1 ، ستظل تحتوي على ارتباط إلى كائن Example1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

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


شيء بسيط القيام به هو استخدام 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.

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

  • إنه ناتج عن خلل أو سوء التصميم.
  • إنها مضيعة للذاكرة.
  • يزداد الأمر سوءًا بمرور الوقت.
  • لا يمكن تنظيف مجمّع garbage.

مثال نموذجي:

ذاكرة التخزين المؤقت للكائنات هي نقطة بداية جيدة لتخريب الأمور.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

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

بالتأكيد ، يمكنك أن تجعل الأمور أكثر تعقيدًا:

  • باستخدام التركيبات ThreadLocal .
  • إضافة أشجار مرجعية أكثر تعقيدًا .
  • أو التسريبات الناتجة عن مكتبات الطرف الثالث .

ما يحدث غالبًا:

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


يمكنك إنشاء تسرب للذاكرة المتحركة عن طريق إنشاء مثيل جديد لفئة في طريقة 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());
        }
    }
}

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

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

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

في تارلي


ربما باستخدام رمز خارجي خارجي من خلال JNI؟

مع جافا نقية ، يكاد يكون من المستحيل.

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


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

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 {

}

يمكنني نسخ إجابتي من هنا: أسهل طريقة لإحداث تسرب للذاكرة في Java؟

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

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

جميع الإجابات الأخرى حتى الآن هي في التعريف الخاص بي لا تسرب الذاكرة حقا. انهم جميعا تهدف إلى ملء الذاكرة مع الاشياء لا طائل منه بسرعة حقيقية. ولكن في أي وقت كان لا يزال بإمكانك تحريف الأشياء التي قمت بإنشائها وبالتالي تحرير الذاكرة -> NO LEAK. تأتي إجابة إكونراد قريبة جدا ، على الرغم من أنني يجب أن أعترف لأن حله هو "تحطيم" جامع القمامة عن طريق إجباره في حلقة لا نهاية لها.

الجواب الطويل هو: يمكنك الحصول على تسرب للذاكرة عن طريق كتابة مكتبة لجافا باستخدام JNI ، والتي يمكن أن يكون لها إدارة الذاكرة اليدوية وبالتالي تسرب الذاكرة. إذا قمت بالاتصال بهذه المكتبة ، فإن عملية java الخاصة بك سوف تتسرب من الذاكرة. أو ، يمكنك الحصول على أخطاء في JVM ، بحيث يفقد JVM الذاكرة. ربما تكون هناك أخطاء في JVM ، قد يكون هناك بعض المعروف منها لأن جمع القمامة ليس بهذه السهولة ، ولكن بعد ذلك لا يزال هناك خطأ. حسب التصميم هذا غير ممكن. قد تسأل عن بعض java code الذي يتأثر بمثل هذا الخطأ. عذرًا ، لا أعرف أحدًا ، وقد لا يكون هناك خطأ بعد الآن في إصدار جافا القادم على أي حال.


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

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

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


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


لقد واجهت مؤخرا حالة تسرب الذاكرة تسبب في طريق 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 ، مما تسبب في حدوث تسرب للذاكرة.


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

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

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


ما يلي هو مثال لا طائل منه ، إذا كنت لا تفهم JDBC . أو على الأقل تتوقع JDBC أن يقوم مطور البرامج بإغلاق حالات Connection Statement و ResultSet قبل التخلص منها أو فقدان مراجع لها ، بدلاً من الاعتماد على تنفيذ finalize .

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

المشكلة المذكورة أعلاه هي أن كائن Connection غير مغلق ، وبالتالي سيبقى الاتصال المادي مفتوحًا ، حتى يأتي مجمّع garbage و يرى أنه غير قابل للوصول. ستستدعي GC طريقة finalize ، ولكن هناك برامج تشغيل JDBC لا تقوم بتطبيق finalize ، على الأقل ليس بنفس الطريقة التي يتم بها تطبيق Connection.close . السلوك الناتج هو أنه على الرغم من أنه سيتم استعادة الذاكرة بسبب الكائنات التي يتعذر الوصول إليها والتي يتم تجميعها ، إلا أنه ببساطة لا يمكن استعادة الموارد (بما في ذلك الذاكرة) المقترنة بكائن Connection .

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

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

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

هناك الكثير من الأمثلة التي يمكنك استحضارها - مثل

  • إدارة مثيل List حيث لا تضيف إلا إلى القائمة ولا تحذف منها (على الرغم من أنك يجب أن تتخلص من العناصر التي لم تعد بحاجة إليها) ، أو
  • فتح S Socket أو s File ، ولكن لا إغلاقها عندما لم تعد هناك حاجة (على غرار المثال أعلاه التي تنطوي على فئة Connection ).
  • لا تفريغ Singletons عند إسقاط تطبيق Java EE. على ما يبدو ، سيحتفظ Classloader الذي تم تحميله في فئة singleton بإشارة إلى الفئة ، وبالتالي لن يتم جمع مثيل singleton مطلقًا. عند نشر نسخة جديدة من التطبيق ، يتم إنشاء محمل دراسي جديد عادةً ، وسوف يستمر لودر الفئة السابق في الوجود بسبب المفرد.

على سبيل المثال ، إليك طريقتان:

Integer x = Integer.valueOf(str);
// or
int y = Integer.parseInt(str);

يوجد اختلاف بسيط بين هذه الطرق:

  • إرجاع valueOf مثيل جديد أو تخزين مؤقت من java.lang.Integer
  • parseInt يعود int .

وينطبق الشيء نفسه على جميع الحالات: Short.valueOf / parseShort ، Long.valueOf / parseLong ، إلخ.





java memory memory-leaks