مقدمة: "الكسر أسهل من البناء"
\nكل مطور جافا قد لعن تسرب الذاكرة (memory leak) في تطبيقه الإنتاجي مرة واحدة على الأقل. التطبيق يتعطل مع OutOfMemoryError، الخادم يعيد التشغيل، المستخدمون يكتبون رسائل غاضبة. ولكن هناك وجه آخر للعملة: كيفية إنشاء تسرب للذاكرة في جافا عن قصد؟ يبدو الأمر جنونيًا، لكن هذا السؤال بالذات (الأصلي من Stack Overflow) جمع آلاف المشاهدات.
لماذا تحتاج إلى معرفة هذا؟ لفهم آلية عمل جامع القمامة (GC) والأنماط المضادة التي تحول كودك إلى غربال. في عام 2025، عندما أصبحت Java 21 مع الخيوط الافتراضية و Loom معيارًا، لم تختفِ التسريبات القديمة - إنها فقط تتنكر في هيئة ميزات جديدة.
\nفي هذه المقالة، لن نعرض فقط كيفية "إفساد" تطبيق جافا، بل سنقدم أيضًا أفضل الممارسات لاكتشاف التسريبات في عام 2025. هل أنت مستعد؟ هيا بنا!
\n\nالقليل من النظرية: لماذا جافا ليست "أبدية"
\nيعتقد العديد من المبتدئين اعتقادًا راسخًا أن جامع القمامة (GC) يزيل تلقائيًا كل ما هو غير ضروري. الواقع قاسٍ: تسرب الذاكرة في جافا ممكن إذا بقي الكائن قابلًا للوصول (reachable) من المراجع الجذرية (GC roots)، ولكنه لم يعد ضروريًا للتطبيق.
\nفي عام 2025، مع ظهور Value Objects (Project Valhalla) و ZGC المحسّن (جامع بزمن توقف < 1 مللي ثانية)، تحسن أداء النظام، لكن منطق التسريبات بقي كما هو. GC ليس عرافًا - إنه لا يعلم أنك نسيت إفراغ المرجع في HashMap.
الطريقة رقم 1: الكلاسيكية - HashMap "النسّاء"
\nالنمط المضاد الأكثر شيوعًا الذي ينتقل من مقال إلى آخر. لكن لا يمكن الاستغناء عنه.
\nكيف يعمل التسرب
\nتضع الكائنات في HashMap (أو HashSet) كمفاتيح، لكنك تنسى حذفها. إذا كانت المفاتيح كائنات قابلة للتغيير (mutable) تغير رمز التجزئة (hash code) بعد الإدراج، فستحصل بالتأكيد على تسرب ذاكرة جافا.
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.
أفضل ممارسة لعام 2025
\n- \n
- استخدم
WeakHashMapإذا كانت المفاتيح كائنات مؤقتة. \n - للتخزين المؤقت - استخدم
CaffeineأوGuava Cacheمع حدود للحجم/الوقت. \n - لا تستخدم أبدًا
StringBuilderكمفتاح - استخدم فقط الأنواع غير القابلة للتغيير (immutable). \n
الطريقة رقم 2: المجموعات الثابتة - القاتل الصامت
\nالحقول الثابتة (static) تعيش حتى نهاية عمل JVM. إذا قمت بتخزين البيانات في static List أو static Map ولم تقم بتنظيفها، فستحصل على تسرب ذاكرة جافا على مستوى الفئة.
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<