إدارة الذاكرة التلقائية من خلال جمع القمامة هي آلية رئيسية لمنصة Android، تعتمد على الآلة الافتراضية ART. وفقًا لوثائق Google Android، 2026، يحرر جامع القمامة المطور من إدارة الذاكرة اليدوية، عن طريق إزالة الكائنات التي لم يعد لها مراجع تلقائيًا. بدون GC، كان كل تخصيص كائن يتطلب استدعاءً صريحًا free أو delete، وهو أمر مستحيل فعليًا في نظام Java البيئي مع ملايين الكائنات في الثانية.
الخلاصة
Garbage Collection (GC) هي عملية تلقائية لاكتشاف وتحرير الذاكرة المشغولة بكائنات لم يعد البرنامج يستخدمها. في سياق التطوير المحمول، يُستخدم GC في منصة Android عبر الآلة الافتراضية ART، وكذلك في Java Virtual Machine القياسية.
على عكس اللغات ذات إدارة الذاكرة اليدوية (C، C++)، حيث يجب على المبرمج استدعاء free أو delete صراحةً، يتولى GC بالكامل مهمة تتبع دورة حياة الكائنات. ينشئ المطور كائنات جديدة عبر عامل new، بينما يحدد الجامع متى يصبح الكائن غير قابل للوصول — أي عندما لا يتبقى أي مراجع نشطة له.
المقاييس الرئيسية لكفاءة GC هي وقت الإيقاف (pause time) والإنتاجية (throughput). الإيقاف هو الفترة التي يتوقف فيها تنفيذ التطبيق لإجراء الجمع. في البيئة المحمولة، الإيقافات الأطول من 8–16 مللي ثانية تكون ملحوظة كإطارات مفقودة (jank).
وفقًا Google I/O 2019، قلل ART في Android 10 الإيقافات النموذجية لـ GC إلى 2–4 مللي ثانية، وهو أقل بنسبة 70% مقارنة بـ Dalvik في Android 4.4. ومع ذلك، تظل إدارة الذاكرة غير السليمة — التخصيص المتكرر للكائنات في الحلقات، إنشاء مثيلات مؤقتة دون داع — السبب الرئيسي لمشاكل الأداء.
جميع تطبيقات GC في Java وAndroid تستند إلى عدة خوارزميات أساسية يتم دمجها لتحقيق التوازن بين وقت الإيقاف واكتمال التنظيف. فهم هذه الخوارزميات ضروري لكتابة كود متوافق مع GC.
Mark-and-Sweep — أبسط خوارزمية، تعمل على مرحلتين. في مرحلة Mark، يجتاز الجامع رسم بياني للكائنات بدءًا من المراجع الجذرية (root set) — المتغيرات المحلية، الحقول الثابتة، أكوام الخيوط. يتم وضع علامة حي على كل كائن يمكن الوصول إليه. في مرحلة Sweep، يجتاز الجامع الكومة بأكملها ويحرر ذاكرة الكائنات غير الموسومة.
العيب هو تجزئة الذاكرة: بعد Sweep، تتناوب المناطق الحرة مع المشغولة، مما يصعب تخصيص الكائنات الكبيرة. في السيناريوهات المحمولة، هذا أمر بالغ الأهمية لأن الكومة عادة ما تكون صغيرة (64–512 ميجابايت على Android).
Copying Collection تقسم الكومة إلى نصفين (semi-spaces). تُنسخ الكائنات النشطة بشكل مضغوط من نصف مساحة إلى آخر، بدون فجوات. بعد النسخ، يُعلن أن المساحة النصفية القديمة حرة بالكامل. تلغي الخوارزمية التجزئة تمامًا، لكنها تتطلب ضعف الذاكرة.
في البيئات المحمولة، تُستخدم Copying Collection من قبل المجمعات الجيلية للتنظيف السريع للكائنات الشابة، التي تموت إحصائيًا مبكرًا (فرضية الجيل الضعيف).
Generational Collection تقسم الكومة إلى أجيال: Young Generation (كائنات شابة) وOld Generation (كائنات قديمة نجت من عدة عمليات جمع). يتم جمع الجيل الشاب (Minor GC) بشكل متكرر وسريع، لأن معظم الكائنات تموت شابة. يحدث جمع الجيل القديم (Major GC أو Full GC) بشكل أقل لكنه يستغرق وقتًا أطول.
// توضيح GC الجيلي: الكائنات الشابة تموت بسرعة
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // يعيش طوال الطريقة
for (Item item : items) {
Result r = new Result(item.getValue()); // يموت فورًا
if (r.isValid()) {
process(r); // r يصبح قمامة
}
}
saveResults(results); // results ينتقل إلى Old Gen
}
في هذا المثال، يتم إنشاء كائنات Result داخل حلقة وتصبح قمامة فورًا — وهي مرشحة مثالية لـ Young GC. الكائن results يعيش لفترة أطول ويهاجر إلى Old Generation. فصل الأجيال يسمح Minor GC بتنظيف الكائنات الشابة في ميلي ثوانٍ دون لمس الكومة القديمة.
تطور Android من Dalvik VM إلى ART (Android Runtime)، وتطبيق GC هو أحد الاختلافات الرئيسية بينهما. فهم بنية GC في Android يساعد في كتابة كود يقلل الإيقافات على الأجهزة الحقيقية.
| الخاصية | Dalvik (حتى 4.4) | ART (5.0+) |
|---|---|---|
| نوع GC | Mark-and-Sweep مع Concurrent Mark | Generational + Concurrent |
| الإيقاف النموذجي | 10–30 مللي ثانية | 2–4 مللي ثانية |
| الضغط | لا (فقط التجزئة تزداد) | نعم (في الخلفية، دون إيقاف التطبيق) |
| التجميع AOT | JIT (Just-In-Time) | AOT + JIT (هجين) |
Dalvik استخدم مزيجًا من Mark-and-Sweep مع مرحلة متزامنة. سمح Concurrent Mark للتطبيق بمواصلة العمل أثناء اجتياز الرسم البياني للكائنات، لكن مرحلة Sweep تطلبت إيقاف جميع الخيوط (Stop-The-World). على الأجهزة ذات ذاكرة الوصول العشوائي الصغيرة (512 ميجابايت — 1 جيجابايت)، وصلت الإيقافات إلى 30 مللي ثانية، مما تسبب في تأخير ملحوظ للواجهة. بالإضافة إلى ذلك، لم يقم Dalvik بضغط الكومة، لذلك بعد التشغيل الطويل، زاد التجزؤ، وكان تخصيص الكائنات الكبيرة (مثل Bitmap) قد يرمي OutOfMemoryError حتى مع وجود ذاكرة حرة كافية إجمالاً.
ART (Android Runtime) قدم جامعًا جيليًا مع ضغط متزامن. تنقسم الكومة إلى ثلاث مناطق: Young وMature (مماثلة لـ Old Generation) وLarge Object Space (للكائنات الأكبر من 12 كيلوبايت). يتم جمع منطقة Young بالتوازي دون إيقاف الخيوط في معظم الحالات. في Android 10+، تم تقديم Concurrent Copying — يتم الضغط في خيط خلفية دون Stop-The-World.
بفضل بنية ART، انخفضت إيقافات GC النموذجية إلى 2–4 مللي ثانية، وفي السيناريوهات التي تسودها الكائنات الشابة — إلى 0.5–1 مللي ثانية. سمح ذلك لأجهزة Android بالحفاظ على 60 إطارًا في الثانية مستقرًا حتى أثناء عمليات الذاكرة النشطة.
في نظام Java البيئي، توجد عدة تطبيقات لـ GC، لكل منها ملف أداء خاص به. لتطوير Android، الخيار محدود بـ ART، لكن معرفة Java GC مفيدة عند كتابة كود الخادم لتطبيقات المحمول وعند التطوير باستخدام Kotlin Multiplatform.
Serial GC — جامع أحادي الخيط مع إيقاف كامل للتطبيق (Stop-The-World). يتم تنفيذ كل عملية Mark وSweep وCompact بواسطة خيط واحد. الأداء منخفض — لا يُستخدم لخوادم المحمول. مناسب فقط للتطبيقات الصغيرة ذات كومة تصل إلى 100 ميجابايت.
Parallel GC (يُعرف أيضًا بـ Throughput Collector) يستخدم عدة خيوط لجميع مراحل الجمع. موجه نحو أقصى إنتاجية (throughput) — تقليل الوقت المستغرق في GC بالنسبة لوقت تشغيل التطبيق. يتم تفعيله عبر العلم -XX:+UseParallelGC في JVM.
G1 (Garbage-First) GC — الجامع الافتراضي في Java 9+. تنقسم الكومة إلى مناطق بحجم 1–32 ميجابايت. يتنبأ G1 بوقت الإيقاف ويسعى للبقاء ضمن الحد المحدد (افتراضيًا 200 مللي ثانية). الأولوية: تنظف المناطق ذات أكبر كمية من القمامة أولاً (ومن هنا الاسم). G1 فعال للخوادم ذات الكومة الكبيرة (4–64 جيجابايت) مع إيقافات قابلة للتنبؤ.
// تفعيل G1 GC مع إيقاف هدف 100 مللي ثانية
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("Heap usage exceeded threshold: " + used);
System.out.println("Consider reducing allocations");
}
}
}
مراقبة الكومة عبر Runtime تسمح باكتشاف تسربات الذاكرة في مرحلة مبكرة. إذا تجاوز used 80% من الحد الأقصى للكومة في التشغيل المستقر — فهذه إشارة إلى تسرب محتمل أو استهلاك مفرط للذاكرة من التطبيق.
حتى ART GC الحديث لا يحل جميع المشاكل — يظل الاستخدام غير السليم للذاكرة السبب الرئيسي للـ jank وANR (Application Not Responding). دعنا نلقي نظرة على السيناريوهات الرئيسية وطرق التحسين.
إيقافات GC — توقف خيوط التطبيق أثناء الجمع. على الشاشة، يظهر هذا كإطارات مفقودة، عندما يتجاوز الوقت بين إطارين 16.6 مللي ثانية (60 إطارًا في الثانية). إذا استمر GC 30 مللي ثانية، يُرسم إطار واحد فقط بدلاً من اثنين — يرى المستخدم تقطّع في الواجهة.
الأسباب الرئيسية للإيقافات الطويلة: عدد كبير من الكائنات الحية في Old Generation، تجزؤ الكومة، Full GC المتكرر. للتشخيص، يُستخدم Android Studio Profiler وsystrace.
القاعدة الرئيسية للكود المتوافق مع GC هي تقليل عدد الكائنات المخصصة. كل كائن جديد يتطلب ليس فقط تخصيص ذاكرة ولكن أيضًا جمعًا لاحقًا. حتى إذا كان GC سريعًا، فإن 1000 تخصيص إضافي في الثانية ينتج عنه 1000 فحص للجامع.
تسرب الذاكرة يحدث عندما يبقى الكائن قابلاً للوصول رغم أنه لم يعد مطلوبًا. لا يمكن لـ GC حذف هذا الكائن، وتنفد الذاكرة تدريجيًا. الأسباب النموذجية: مستمعات غير مسجلة، مراجع ثابتة لـ Activity، فئات مجهولة تلتقط السياق الخارجي، وCursor/InputStream غير مغلقة.
// تسرب ذاكرة: فئة مجهولة تحتفظ بمرجع لـ Activity
public void startTask() {
new Thread(new Runnable() { // يحتفظ ضمنيًا بـ this (Activity)
@Override
public void run() {
// عملية طويلة...
System.out.println("Done");
}
}).start();
}
// التصحيح: فئة متداخلة ثابتة + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// عمل آمن مع Activity
}
}
}
في هذا المثال، يلتقط Runnable المجهول مرجعًا ضمنيًا لـ Activity. ما دام الخيط حيًا — لا يمكن جمع Activity بواسطة GC، حتى إذا أغلق المستخدم الشاشة بالفعل. التصحيح باستخدام WeakReference + فئة ثابتة يكسر هذه السلسلة ويسمح بتحرير Activity.
الأسئلة المتكررة
GC في Android (ART) هو جامع جيلي مع ضغط متزامن، محسّن للأجهزة المحمولة ذات الذاكرة المحدودة. Java GC (G1, ZGC) هم جامعو خادم مع كومة كبيرة وإيقافات قابلة للتنبؤ. ART GC لا يستخدم أعلام JVM — يتم كل الضبط تلقائيًا على مستوى نظام التشغيل.
Stop-The-World — اللحظة التي يوقف فيها الجامع جميع خيوط التطبيق لاجتياز الرسم البياني للكائنات أو تحرير الذاكرة بأمان. كلما طال STW، زاد وضوح jank. قلل ART وقت STW النموذجي إلى 2–4 مللي ثانية بفضل بنيته الجيلية.
استخدم Android Studio Memory Profiler — يظهر نمو الكومة وعدد التخصيصات ويسمح بعمل Heap Dump. للتحليل العميق، استخدم LeakCanary — المكتبة تكتشف التسربات تلقائيًا وتظهر سلسلة المراجع التي تمنع جمع GC.
Full GC — جمع كامل لجميع أجيال الكومة، بما في ذلك Old Generation. في التطبيقات المحمولة، يمكن أن يستمر Full GC 50–200 مللي ثانية، مسببًا jank أو ANR ملحوظًا. الأسباب الرئيسية: تجزؤ الكومة، تسربات الذاكرة، تجاوز حد Old Generation.
Kotlin يوفر coroutines مع التزامن المنظم — إلغاء النطاق يلغي تلقائيًا جميع coroutines التابعة، مما يمنع التسربات. يحتوي Kotlin أيضًا على المفوض lazy للتهيئة البطيئة ودوال النطاق التي تقلل عدد الكائنات المؤقتة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا