اذهب إلى المحتوى

تُدَار الذاكرة في JavaScript تلقائيًا في الخفاء. نحن ننشئ المتغيرات الأولية، والكائنات، والدوال وجميعها تأخذ مكانًا في الذاكرة ولكن هل سألت نفسك ماذا يحدث عندما يصبح أحد هذه الأشياء مهملًا وغير مهم؟ كيف يكتشف ذلك مُحرِّك JavaScript ويتخلص منه؟ هذا ما سنعرفه في هذا الفصل.

قابلية الوصول

المبدأ الرئيسي لإدارة الذاكرة في JavaScript هو قابلية الوصول (reachability). ببساطة، القيم "القابلة للوصول" هي القيم التي يمكن الوصول إليها واستخدامها بطريقة ما وهذه القيم مخولة لتُخزَّن في الذاكرة.

1- يوجد مجموعة من القيم القابلة للوصول بطبيعة الحال والتي لا يمكن التخلص منها لأسباب وجيهة مثل:

  • المتغيرات المحلية والمُعاملات للدالة الحاليِّة.
  • المتغيرات والمُعاملات لدوال أخرى ضمن السلسلة الحالية في الاستدعاء المُتداخل.
  • المتغيرات العامة.
  • (بالإضافة إلى بعض المتغيرات الأخرى الداخلية)

تُدعى هذه القيم «جذورًا» (roots).

2- تكون أي قيمة أخرى قابلة للوصول إن كان بالإمكان الوصول إليها من جذر باستخدام مرجع أو سلسلة من المراجع. مثلًا، إن كان هناك كائن في متغير محلي، ولدى هذا الكائن خاصية تشير لكائن آخر، فإنَّ هذا الكائن قابل للوصول. وهذه الكائنات التي يُشار إليها قابلة للوصول أيضًا. ستجد أمثلة توضيحية لاحقًا.

يوجد عملية خلفيَّة في محرك JavaScript تُدعَى «كانس المهملات» (garbage collector) تعمل على مراقبة الكائنات وحذف التي لم تَعُد قابلة للوصول.

مثال بسيط

أبسط مثال هو:

// user يرجِع لكائن
let user = {
  name: "John"
};

 

1.png

يُصوِّر السهم في الصورة مرجعًا لكائن. المتغير العام "user" يشير للكائن {name: "John"‎} (سنُسميه John اختصارًا). الخاصية "name" للكائن John تُخزن قيمة أولية، لذ رُسمَت بداخل الكائن. إن استُبدلَت قيمة المتغير user، فَسَنفقد المرجع الذي يشير إلى الكائن John:

user = null;

 

2.png

أصبح الكائن John غير قابل للوصول الآن أي لا يوجد طريقة للوصول إليه. سيعمل كانس المهملات على حذف البيانات المتمثلة في الكائن John وتحرير الذاكرة التي يحتلها.

مرجعَان لكائن

لنفترض أننا نسخنا المرجع من المتغير user إلى متغير آخر باسم admin:

// user يرجِع لكائن
let user = {
  name: "John"
};

let admin = user;

 

3.png

إن قمنا بالأمر السابق ذاته الآن:

user = null;

فسيكون الكائن قابلًا للوصول من خلال المتغير العام admin، لذا يبقى في الذاكرة. إن استبدلنا محتوى المتغير admin أيضًا فسيُحذَف الكائن.

الكائنات المتداخلة

الآن ننتقل لمثال أكثر تعقيدًا. ألق نظرة على الشيفرة التالية:

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

تربط الدالة marry كائنين بجعل كلاهما يشير إلى الآخر ثم ترجِع كائنًا جديدًا يحوي كلاهما. هيكل الذاكرة الناتج يكون كالتالي:

4.png

تكون جميع البيانات قابلة للوصول حتى الآن. دعنا نجرب حذف مرجعين الآن:

delete family.father;
delete family.mother.husband;

 

5.png

ليس من الكافِ حذف أحد المرجِعين فقط، لأنَّ جميع الكائنات ستظل قابلة للوصول لكن إن حذفنا كلا المرجعين، فسنرى عدم وجود أي مرجع إلى الكائن John:

6.png

لا يهم وجود مرجع من الكائن، إذ ما يجعله قابلًا للوصول هو المراجع التي تشير إليه، لذا فإنَّ الكائن John أصبح غير قابل للوصول وسيُحذَف من الذاكرة مع جميع بياناته التي أصبحت غير قابلة للوصول أيضًا.

بعد تجميع البيانات الغير مرغوب بها، يبقى لدينا:

7.png

جزيرة غير قابلة للوصول

يمكن أن تصبح جزيرة من الكائنات المترابطة غير قابلة للوصول وتُحذَف من الذاكرة. الكائن الرئيسي هو الكائن أعلاه ذاته:

family = null;

تُصبح الصورة في الذاكرة كما يلي:

8.png

يوضح هذا المثال أهمية مبدأ قابلية الوصول. من الواضح أن الكائنين John و Ann ما زالا مرتبطين ولكل منهما مراجع لبعضهما، لكن ذلك غير كافٍ.

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

الخوارزميات الداخلية

تُدعى الخوارزمية الأساسية لتجميع البيانات المهملة «الاستهداف والتمشيط» (mark-and-sweep)؛ تُنفَّذ خطوات جمع البيانات المهملة دوريًا وفق الخطوات التالية:

  • يأخذ كانس المهملات الجذور ويحفظها (يُحدِّدها هدفًا له).
  • ثم يُمشِّط جميع الإشارات الخارجة منها (مراجع لكائنات أخرى) ويحفظها لاستهدافها أيضًا.
  • ثم يُمشِّط جميع الكائنات التي استهدفها مسبقًا ويحفظ مراجعها لاستهدفها لاحقًا. يُمشِّط جميع الكائنات بتلك الطريقة ويتذكرها لكي لا يُمشِّط أي كائن مرةً ثانية مستقبلًا.
  • تستمر العملية مرارًا وتكرارًا حتى يصبح هناك مراجع لم تُمشَّط (غير قابلة للوصول من أي جذر).
  • تُحذَف جميع الكائنات باستثناء تلك استُهدفَت وعُلِّمَت بأنَّها غير مهملة.

مثلًا، ليكن هيكل الكائنات لدينا كما يلي:

9.png

يمكن رؤية جزيرة غير قابلة للوصول في اليمين. الآن لنرى كيف يتعامل معها كانس المهملات وفق خوارزمية الاستهداف والتمشيط.

الخطوة الأولى هي تحديد الجذور:

10.png

ثم تُحدَّد المراجع التي تشير إليها:

11.png

تُحدَّد المراجع التي تشير لها هذه الكائنات أيضًا:

12.png

تُعدُّ الكائنات التي لم تُمشَّط أثناء العملية غير قابلة للوصول ويجري حذفها:

13.png

هذه الآلية التي يعمل بها كانس المهملات.

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

بعض التحسينات:

  • التجميع وفق الجيل: تُقَسَّم الكائنات إلى مجموعتين: "كائنات جديدة" و "كائنات قديمة". تُنشَأ العديد من الكائنات ثمَّ تؤدي عملها ثمَّ تموت بسرعة، ويمكن تنظيفها بقوة. وتلك التي تنجو تصبح قديمة وتُفحَص بوتيرة أقل.
  • التجميع التدريجي: إن وُجِد العديد من الكائنات وأردنا المرور خلال جميع الكائنات وتحديدها دفعة واحدة، فقد يأخذ ذلك وقتًا ويظهِِر آنذاك تأخيرٌ ملحوظٌ في التنفيذ، لذا يحاول المُحرِّك تقسيم عملية كنس البيانات المهملة إلى أجزاء ثم تنفيذ الأجزاء واحدًا تلو الآخر بشكل منفصل. قد يتطلب ذلك إجراء حسابات إضافية لتتبع التغييرات، لكن يصبح لدينا الكثير من التأخيرات الغير ملحوظة بدلًا من تأخير واحد كبير.
  • التجميع وقت الخمول: يحاول كانس المهملات العمل عندما يكون المعالج غير مشغول لتقليل أي تأثيرات محتملة على التنفيذ.

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

الخلاصة

الأشياء التي يجب معرفتها:

  • تُكنس البيانات المهملة تلقائيًا، وهو أمر لا يمكن فرضه أو تجنبه.
  • تبقى الكائنات في الذاكرة طالما يمكن الوصول إليها من أي جذر.
  • وجود مرجِع للكائن ليس مثل أن يكون قابلًا للوصول من جذر، ويمكن لمجموعة من الكائنات المترابطة أن تصبح غير قابلة للوصول.

تستخدم المحركات الحديثة خوارزميات متطورة لكنس البيانات المهملة.

يغطي كتاب "The Garbage Collection Handbook: The Art of Automatic Memory Management" (لمؤلفه R. Jones وغيره) بعضًا منها.

إن كنت معتادًا على البرمجة بلغات ذات مستوى منخفض، يوجد معلومات مفصَّلة عن كانس المهملات في المُحرِّك V8 في المقال رحلة إلى V8: كنس البيانات المهملة.

تنشر مدونة V8 أيضًا مقالات عن التغييرات في إدارة الذاكرة من وقت لآخر. لتتعلم عن كنس البيانات المهملة، يجب أن تتجهز بتعلم أمور V8 الداخلية كما يُفضَّل أن تقرأ مدونة Vyacheslav Egorov الذي عمل كأحد مهندسي V8. أنا أقول V8 لوجود الكثير من المقالات عنه على الإنترنت. العديد من الجوانب متشابهة بالنسبة لباقي المحركات، لكن يختلف كنس البيانات المهملة من عدة نواحي بينها.

المعرفة العميقة بالمحركات جيدة عندما تحتاج إلى إجراء تحسين منخفض المستوى. فكِّر في ذلك وليكن خطوتك التالية بعد أن تعتاد على اللغة.

ترجمة -وبتصرف- للفصل Garbage collection من كتاب The JavaScript Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...