منظف الذاكرة في جافا Garbage Collector
تمتلك لغات البرمجة القديمة أوامر خاصة لإضافة وإزالة البيانات من الذاكرة العشوائية (RAM), ويكون المبرمج هو المسؤول عن إزالة البيانات التي لم يعد يحتاجها, لكن غالباً ما تكون هذه العملية متكررة و لا تحمل قيمة كبيرة فيهملها المبرمج وينتهي الأمر بإمتلاء الذاكرة.
لذلك توفر لغات البرمجة الحديثة طرقاً لإزالة البيانات التي لن يستخدمها البرنامج مجدداً بشكل تلقائي و بدون تدخل المبرمج. توفر جافا منظف الذاكرة أو جامع القمامة (Garbage Collector) والذي يكون مسؤولاً عن هذه العملية.
ما هو جامع القمامة وكيف يعمل وكيف نستفيد منه لأقصى حد؟
جامع القمامة هو برنامج موجود ضمن آلة جافا الافتراضية (JVM) يقوم بتنظيف الكومة (Heap), و الكومة هي مساحة يتم تخزين الكائنات التي ينشئها البرنامج فيها.
صورة توضح أجزاء Hotspot JVM, ويوجد جامع القمامة في Execution Engine
عند تشغيل آلة جافا الإفتراضية يتم تحديد مساحة الكومة التي يستطيع البرنامج إستغلالها, لكن عندما يقوم البرنامج بالإقتراب من ملء الكومة ينشط جامع القمامة ليرى إن كان يستطيع مسح الكائنات التي لم تعد تستخدم, ويستطيع جامع القمامة تمييز هذه الكائنات عندما لا تعود أي Thread تمتلك مرجعاً Reference لذلك الكائن, وهكذا يستحيل على أي Thread إستخدام ذلك الكائن في المستقبل. عندما يقرر جامع القمامة أنه سيحذف كائناً من الكومة سيقوم بتنفيذ الدالة finalize()
على ذلك الكائن, و هذه الدالة موجودة في الكلاس Object مما يعني أنها مورثة لجميع الكائنات, ويفترض عند بناء أي صف أن تقوم بكتابة الأوامر التي يجب تنفيذها قبل حذف الكائن مثل إغلاق قنوات اتصال معينة IO Stream.
دعونا نجرب الكود التالي ونرى النتيجة.
public class test { public test() { System.out.println("constructing"); } public static void main(String[] args) { new Thread() { public void run() { while (true) { System.out.println(Runtime.getRuntime().totalMemory() / (1024.0f * 1024.0f) +"MB"); } } }.start(); while(true){ new test(); } } @Override protected void finalize() throws Throwable { System.out.println("finalizing"); } }
سنحصل على النتيجة التالية عند التشغيل.
constructing{73099} 61.5MB finalizing
كما رأينا في هذه النتيجة الملخصة بعد طباعة الكلمة constructing 37099
مرة رأينا أول كلمة finalizing
, وإستخدمنا الدالة ()Runtime.getRuntime().totalMemory
لمعرفة المساحة التي تستهلكها JRE من الذاكرة العشوائية, ومن خلال أداة VisualVM إستطعنا الحصول على الصورة التالية.
من تحليل الشكل للكومة يمكننا ملاحظة أن المساحة المستخدمة تزداد إلى مرحلة تقترب فيها من 25MB وبعدها يتحفز جامع القمامة تلقائياً (ويدل على ذلك إرتفاع الخط الأزرق في الرسم البياني في الأعلى على اليسار) ليمسح الكائنات التي لن تستخدم في المستقبل, مما يفسر الإنخفاضات بعد نقاط الذروة.
إذاً لنجرب الآن محاولة تحفيز جامع القمامة من كود الجافا عبر إستدعاء الدالة System.gc();
public class test { public test() { System.out.println("constructoring"); } public static void main(String[] args) { new Thread() { public void run() { while (true) { try { System.out.println(Runtime.getRuntime().totalMemory() / (1024.0f * 1024.0f) +"MB"); System.gc(); Thread.sleep(2000); } catch (InterruptedException ex) {break;} } } }.start(); while(true){ new test(); } } @Override protected void finalize() throws Throwable { System.out.println("finalizing"); } }
سنحصل على النتيجة التالية عند التشغيل.
constructoring{175} 61.5MB finalizing
يمكننا ملاحظة فرق شاسع بين هذا المثال والمثال السابق في أول ظهور للكلمة finalizing, أما الرسم البياني فكان كالتالي.
من جهة كانت أكبر مساحة مستهلكة من الذاكرة في هذا المثال 5MB أما في المثال السابق كانت 25MB, ومن جهة أخرى كان أقصى استهلاك للمعالج 15% أما في المثال السابق فكان 7%, وتفسير ذلك أن عملية تحفيز جامع القمامة للبدء بالبحث داخل الكومة عن كائنات لإزالتها هي عملية مكلفة وتنفيذها بشكل دوري هو أمر غير مجدٍ.
قمنا في المثالين السابقين بإستعراض كيف يتعامل جامع القمامة مع الكائنات التي ليس لها مرجع ويستحيل إستخدامها مرة أخرى في البرنامج, و الآن سنستعرض كيف يتعامل جامع القمامة مع الكائنات التي لها مرجع عبر هذا المثال.
import java.util.ArrayList; public class test { private static ArrayList list = new ArrayList(); public static void main(String[] args) { new Thread() { public void run() { while (true) { System.out.println(Runtime.getRuntime().totalMemory() / (1024.0f * 1024.0f) +"MB"); } } }.start(); while(true){ list.add("!@#$%^&*()1234567890qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOPASDFGHJKLZXCVBNM"); } } }
سنحصل على النتيجة التالية عند التشغيل.
61.5MB{50} 120.5MB{50} 173.5MB{50} 253.0MB{50} 372.0MB{50} 259.5MB{50} 438.0MB{50} 684.0MB{50} Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at org.rami.io.test.main(685.5MB test.java:36) 685.5MB{50}
نستطيع أن نرى من خلال هذا الكود أن جميع النصوص String التي تم توليدها قد تم حفظها في القائمة list
, ولأن هذه القائمة تمتلك مرجعاً لكل النصوص المخزنة بداخلها فلا يستطيع جامع القمامة إزالتها من الذاكرة العشوائية, ولذلك قامت JRE بزيادة مساحة الكومة حتى أطلقت في النهاية الخطأ OutOfMemoryError معلنة أنها لا تملك مساحة أكبر لتخزين الكائنات في الكومة وأنها لا تستطيع زيادة مساحة الكومة أكثر من ذلك, وكان الرسم البياني كالتالي.
تمتلك كل JRE قيم لأعلى وأقل مساحة ممكنة للكومة, وفي حال تطويرك لبرنامج يستهلك مساحة كبيرة من الذاكرة العشوائية فيجب عليك تمرير معامل Xmx يحوي على أكبر مساحة مسموح للبرنامج بإستهلاكها من الذاكرة العشوائية عند تشغيل ملف JAR, و تنفيذ الأمر التالي في موجه أوامر ويندوز (Command Prompt) سيقوم بتشغيل ملف test.jar
مع السماح له بإستهلاك 1GB من الذاكرة العشوائية.
java -Xmx1g -jar test.jar