لماذا يتسرب كود جافا؟ إنشاء واكتشاف تسربات الذاكرة في 2025

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: "الكسر أسهل من البناء"

\n

كل مطور جافا قد لعن تسرب الذاكرة (memory leak) في تطبيقه الإنتاجي مرة واحدة على الأقل. التطبيق يتعطل مع OutOfMemoryError، الخادم يعيد التشغيل، المستخدمون يكتبون رسائل غاضبة. ولكن هناك وجه آخر للعملة: كيفية إنشاء تسرب للذاكرة في جافا عن قصد؟ يبدو الأمر جنونيًا، لكن هذا السؤال بالذات (الأصلي من Stack Overflow) جمع آلاف المشاهدات.

\n

لماذا تحتاج إلى معرفة هذا؟ لفهم آلية عمل جامع القمامة (GC) والأنماط المضادة التي تحول كودك إلى غربال. في عام 2025، عندما أصبحت Java 21 مع الخيوط الافتراضية و Loom معيارًا، لم تختفِ التسريبات القديمة - إنها فقط تتنكر في هيئة ميزات جديدة.

\n

في هذه المقالة، لن نعرض فقط كيفية "إفساد" تطبيق جافا، بل سنقدم أيضًا أفضل الممارسات لاكتشاف التسريبات في عام 2025. هل أنت مستعد؟ هيا بنا!

\n\n

القليل من النظرية: لماذا جافا ليست "أبدية"

\n

يعتقد العديد من المبتدئين اعتقادًا راسخًا أن جامع القمامة (GC) يزيل تلقائيًا كل ما هو غير ضروري. الواقع قاسٍ: تسرب الذاكرة في جافا ممكن إذا بقي الكائن قابلًا للوصول (reachable) من المراجع الجذرية (GC roots)، ولكنه لم يعد ضروريًا للتطبيق.

\n

في عام 2025، مع ظهور Value Objects (Project Valhalla) و ZGC المحسّن (جامع بزمن توقف < 1 مللي ثانية)، تحسن أداء النظام، لكن منطق التسريبات بقي كما هو. GC ليس عرافًا - إنه لا يعلم أنك نسيت إفراغ المرجع في HashMap.

\n\n

الطريقة رقم 1: الكلاسيكية - HashMap "النسّاء"

\n

النمط المضاد الأكثر شيوعًا الذي ينتقل من مقال إلى آخر. لكن لا يمكن الاستغناء عنه.

\n

كيف يعمل التسرب

\n

تضع الكائنات في HashMap (أو HashSet) كمفاتيح، لكنك تنسى حذفها. إذا كانت المفاتيح كائنات قابلة للتغيير (mutable) تغير رمز التجزئة (hash code) بعد الإدراج، فستحصل بالتأكيد على تسرب ذاكرة جافا.

\n\n
import java.util.HashMap;\nimport java.util.Map;\n\npublic class LeakyMapExample {\n    public static void main(String[] args) {\n        Map<StringBuilder, String> map = new HashMap<>();\n\n        for (int i = 0; i < 100_000; i++) {\n            StringBuilder key = new StringBuilder("key_" + i);\n            map.put(key, "value_" + i);\n            // "نسينا" الحذف — المفاتيح معلقة إلى الأبد\n        }\n        System.out.println("حجم الخريطة: " + map.size());\n        // تسرب: 100_000 كائن StringBuilder + String في الكومة\n    }\n}\n
\n\n

لماذا هذا تسرب؟ طالما أن المرجع إلى map حي، تبقى جميع المفاتيح والقيم في heap. حتى لو لم يعد منطق التطبيق يستخدم هذه البيانات، لن يلمسها GC.

\n\n

أفضل ممارسة لعام 2025

\n
    \n
  • استخدم WeakHashMap إذا كانت المفاتيح كائنات مؤقتة.
  • \n
  • للتخزين المؤقت - استخدم Caffeine أو Guava Cache مع حدود للحجم/الوقت.
  • \n
  • لا تستخدم أبدًا StringBuilder كمفتاح - استخدم فقط الأنواع غير القابلة للتغيير (immutable).
  • \n
\n\n

الطريقة رقم 2: المجموعات الثابتة - القاتل الصامت

\n

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

\n\n
import java.util.ArrayList;\nimport java.util.List;\n\npublic class StaticLeak {\n    private static final List<byte[]> CACHE = new ArrayList<>();\n\n    public static void addData() {\n        // كل استدعاء يضيف 10 ميجابايت إلى القائمة الثابتة\n        CACHE.add(new byte[10 * 1024 * 1024]);\n    }\n\n    public static void main(String[] args) throws Exception {\n        for (int i = 0; i < 1_000; i++) {\n            addData();\n            Thread.sleep(100);\n        }\n        // OutOfMemoryError لا مفر منه\n    }\n}\n
\n\n

<

المدونات

توصيات الكتب