ادارة الذاكرة وجمع القمامة
فهم الذاكرة في جافاسكريبت
ادارة الذاكرة هي واحدة من اكثر الجوانب اهمية والتي غالبا ما يتم تجاهلها في تطوير جافاسكريبت. على عكس لغات مثل C او C++ حيث يقوم المطورون بتخصيص الذاكرة وتحريرها يدويا، تستخدم جافاسكريبت ادارة الذاكرة التلقائية من خلال عملية تسمى جمع القمامة. ومع ذلك، فان فهم كيفية عمل الذاكرة من وراء الكواليس امر ضروري لكتابة تطبيقات عالية الاداء لا تسرب الذاكرة بمرور الوقت. في هذا الدرس، ستتعلم دورة حياة الذاكرة الكاملة وكيف يعمل جامع القمامة وانماط تسرب الذاكرة الشائعة وكيفية استخدام الادوات والواجهات البرمجية الحديثة لتشخيص مشاكل الذاكرة ومنعها في تطبيقاتك.
في كل مرة تنشئ فيها متغيرا او كائنا او دالة او اي بنية بيانات في جافاسكريبت، يجب على المحرك ايجاد مساحة في الذاكرة لتخزينها. عندما لا تكون هذه البيانات مطلوبة بعد الان، يجب على المحرك استعادة تلك المساحة حتى يمكن اعادة استخدامها. هذه العملية تبدو بسيطة نظريا، لكن تفاصيل كيفية عملها لها تاثيرات عميقة على اداء التطبيق واستقراره وتجربة المستخدم.
دورة حياة الذاكرة
بغض النظر عن لغة البرمجة، تتبع ادارة الذاكرة دائما ثلاث خطوات اساسية. فهم هذه الخطوات هو الاساس لكل شيء اخر في هذا الدرس.
الخطوة الاولى: التخصيص. عندما تعلن عن متغير او تنشئ كائنا، يقوم محرك جافاسكريبت بتخصيص ذاكرة لتخزين القيمة. بالنسبة للقيم الاولية مثل الارقام والقيم المنطقية، يخصص المحرك كمية ثابتة من الذاكرة. بالنسبة للكائنات والمصفوفات والدوال، يخصص المحرك ذاكرة كافية لحمل جميع خصائصها وبياناتها بالاضافة الى البيانات الوصفية الداخلية التي يحتاجها المحرك لادارتها.
الخطوة الثانية: الاستخدام. هذا عندما يقرا برنامجك من الذاكرة المخصصة ويكتب اليها. في كل مرة تصل فيها الى متغير او تستدعي دالة او تعدل خاصية كائن، فانت تستخدم الذاكرة المخصصة. هذه الخطوة صريحة في كودك -- انها ما يفعله منطق تطبيقك.
الخطوة الثالثة: التحرير. عندما لا تكون الذاكرة المخصصة مطلوبة بعد الان، يجب تحريرها حتى يمكن اعادة استخدامها. في جافاسكريبت، يتم التعامل مع هذه الخطوة تلقائيا بواسطة جامع القمامة. التحدي هو ان تحديد متى تكون الذاكرة "غير مطلوبة فعلا" هي مشكلة غير قابلة للحسم في الحالة العامة، لذلك يستخدم جامعو القمامة تقريبات تعمل بشكل جيد في الممارسة لكنها ليست مثالية.
مثال: دورة حياة الذاكرة في العمل
// الخطوة 1: التخصيص
// يخصص المحرك ذاكرة لرقم
let age = 30;
// يخصص المحرك ذاكرة لنص
let name = 'Edrees';
// يخصص المحرك ذاكرة لكائن وخصائصه
let user = {
firstName: 'Edrees',
lastName: 'Salih',
skills: ['JavaScript', 'PHP', 'Laravel']
};
// الخطوة 2: الاستخدام
// القراءة من الذاكرة المخصصة
console.log(user.firstName);
// الكتابة الى الذاكرة المخصصة
user.age = age;
// انشاء مراجع جديدة للذاكرة الموجودة
let skills = user.skills;
// الخطوة 3: التحرير
// عندما تخرج المتغيرات عن النطاق او يتم تعيينها الى null
// يمكن لجامع القمامة استعادة ذاكرتها
user = null;
// ملاحظة: مصفوفة skills لم يتم جمعها بعد لان
// المتغير 'skills' لا يزال يشير اليها
ذاكرة المكدس مقابل الكومة
تستخدم محركات جافاسكريبت منطقتين رئيسيتين في الذاكرة لتخزين البيانات: المكدس والكومة. فهم الفرق بينهما يساعدك على التفكير في خصائص الاداء وسلوك الذاكرة في تطبيقاتك.
المكدس هو منطقة ذاكرة منظمة وسريعة الوصول تعمل بطريقة الاخير يدخل اولا يخرج (LIFO). يخزن القيم الاولية (الارقام والنصوص والقيم المنطقية و null و undefined والرموز و BigInt) ومراجع الكائنات. يخزن المكدس ايضا اطارات استدعاء الدوال -- في كل مرة يتم فيها استدعاء دالة، يتم دفع اطار جديد على المكدس يحتوي على المتغيرات المحلية والمعاملات. عندما ترجع الدالة، يتم ازالة ذلك الاطار ويتم استعادة الذاكرة فورا. تخصيص وتحرير ذاكرة المكدس سريع للغاية لانه يتطلب فقط تحريك مؤشر.
الكومة هي منطقة ذاكرة كبيرة وغير منظمة تستخدم لتخزين الكائنات والمصفوفات والدوال وانواع البيانات المعقدة الاخرى. على عكس المكدس، لا يتم استعادة ذاكرة الكومة تلقائيا عند عودة الدالة. بدلا من ذلك، يديرها جامع القمامة الذي يفحص الكومة دوريا للعثور على كائنات لم تعد قابلة للوصول من المراجع الجذرية للبرنامج. تخصيص الكومة ابطا من تخصيص المكدس لان المحرك يجب ان يبحث عن كتلة مناسبة من الذاكرة الحرة ويدير التجزئة.
مثال: تخزين المكدس مقابل الكومة
function createUser(firstName, lastName) {
// 'firstName' و 'lastName' مخزنان على المكدس
// كجزء من اطار استدعاء هذه الدالة
// القيمة الاولية 25 مخزنة على المكدس
let age = 25;
// الكائن مخزن على الكومة
// المتغير 'user' على المكدس يحمل مرجعا
// (عنوان ذاكرة) يشير الى موقع الكومة
let user = {
firstName: firstName,
lastName: lastName,
age: age
};
// المصفوفة ايضا مخزنة على الكومة
let hobbies = ['coding', 'reading', 'hiking'];
// عندما ترجع هذه الدالة يتم ازالة اطار المكدس
// لكن الكائن المرجع يبقى على الكومة
return user;
}
// 'result' يحمل مرجعا لكائن الكومة
let result = createUser('Edrees', 'Salih');
// مصفوفة 'hobbies' من داخل الدالة الان غير قابلة للوصول
// ومؤهلة لجمع القمامة
جمع القمامة: المفهوم
جمع القمامة هو العملية التلقائية لتحديد الذاكرة التي لم يعد البرنامج يستخدمها واستعادتها. المفهوم الاساسي هو قابلية الوصول: يعتبر الكائن "حيا" ويجب الاحتفاظ به في الذاكرة اذا كان يمكن الوصول اليه مباشرة او غير مباشرة من مجموعة المراجع الجذرية. تشمل المراجع الجذرية المتغيرات العامة والمتغيرات المحلية ومعاملات الدالة المنفذة حاليا ومراجع المحرك الداخلية الاخرى.
اذا لم يكن الكائن قابلا للوصول من اي جذر عبر اي سلسلة مراجع، فانه يعتبر قمامة ويمكن جمعه بامان. يعمل جامع القمامة دوريا وتلقائيا -- لا يمكنك بشكل عام التحكم في وقت تشغيله، رغم انه يمكنك التاثير على سلوكه من خلال طريقة هيكلة الكود الخاص بك.
خوارزمية عد المراجع
عد المراجع هو واحد من ابسط استراتيجيات جمع القمامة. الفكرة مباشرة: احتفظ بعد لعدد المراجع التي تشير الى كل كائن. عندما ينخفض العد الى صفر، يمكن تحرير الكائن. بينما لا تستخدم المحركات الحديثة عد المراجع النقي كاستراتيجية GC الاساسية، فان فهمه يساعدك على استيعاب لماذا تسبب انماط معينة تسربات ذاكرة.
مثال: كيف يعمل عد المراجع
// عد المراجع للكائن يبدا عند 1
let obj = { name: 'Edrees' }; // refcount: 1
// انشاء مرجع اخر لنفس الكائن
let anotherRef = obj; // refcount: 2
// ازالة المرجع الاول
obj = null; // refcount: 1
// ازالة المرجع الثاني
anotherRef = null; // refcount: 0
// يمكن الان جمع الكائن
// العيب القاتل: المراجع الدائرية
function createCircularLeak() {
let objA = {};
let objB = {};
// انشاء مرجع دائري
objA.partner = objB; // objB refcount: 2
objB.partner = objA; // objA refcount: 2
// حتى بعد تصفير كلا المتغيرين:
objA = null; // objA refcount: 1 (objB لا يزال يشير اليه)
objB = null; // objB refcount: 1 (objA لا يزال يشير اليه)
// لا واحد يصل الى 0 لذلك لا يتم جمع اي منهما!
// هذا تسرب ذاكرة مع عد المراجع النقي
}
خوارزمية التمييز والمسح
التمييز والمسح هو اساس جمع القمامة الحديث في جافاسكريبت. تعمل الخوارزمية في مرحلتين. في مرحلة التمييز، يبدا الجامع من المراجع الجذرية ويجتاز جميع الكائنات القابلة للوصول مميزا كل واحد بانه "حي". في مرحلة المسح، يفحص الجامع جميع الكائنات في الكومة ويحرر اي كائنات لم يتم تمييزها خلال مرحلة التمييز. بعد المسح، يتم مسح جميع التمييزات استعدادا للدورة التالية.
الميزة الرئيسية للتمييز والمسح على عد المراجع هي انه يتعامل بشكل صحيح مع المراجع الدائرية. اذا اشار كائنان الى بعضهما البعض لكن لم يكن اي منهما قابلا للوصول من اي جذر، فلن يتم تمييزهما خلال مرحلة التمييز وسيتم مسحهما. هذا يحل المشكلة الاساسية لعد المراجع.
مثال: التمييز والمسح يتعامل مع المراجع الدائرية
function demonstrateMarkAndSweep() {
let objA = { name: 'Object A' };
let objB = { name: 'Object B' };
// انشاء مرجع دائري
objA.ref = objB;
objB.ref = objA;
// في هذه النقطة كلا الكائنين قابلان للوصول من
// المتغيرات المحلية للدالة (الجذور)
objA = null;
objB = null;
// الان لا يمكن الوصول الى اي كائن من اي جذر
// التمييز والمسح سيقوم بـ:
// 1. البدء من الجذور (الكائن العام، مكدس الاستدعاء)
// 2. لا يمكن الوصول الى اي كائن عبر اي سلسلة
// 3. كلاهما يبقى بدون تمييز
// 4. كلاهما يتم مسحه وتحريره
// لا تسرب!
}
demonstrateMarkAndSweep();
// بعد عودة الدالة اطار المكدس يختفي
// ولا توجد جذور تشير الى الكائنات الدائرية
جمع القمامة الجيلي
تستخدم محركات جافاسكريبت الحديثة نهجا متطورا يسمى جمع القمامة الجيلي، بناء على الملاحظة التجريبية المعروفة بـ "الفرضية الجيلية": معظم الكائنات تموت صغيرة. بمعنى اخر، غالبية الكائنات تصبح غير قابلة للوصول بعد وقت قصير من انشائها.
يقسم محرك V8 (المستخدم في كروم و Node.js) الكومة الى جيلين رئيسيين:
الجيل الشاب (الحضانة): يتم تخصيص الكائنات المنشاة حديثا هنا. هذه المساحة صغيرة (عادة 1-8 ميغابايت) ويتم جمعها بشكل متكرر باستخدام خوارزمية سريعة تسمى Scavenge (جامع نسخ شبه فضائي). ينقسم الجيل الشاب الى شبه فضائين متساويين. يتم تخصيص الكائنات في شبه فضاء واحد وعندما يمتلئ يتم نسخ الكائنات الباقية الى الشبه فضاء الاخر. الكائنات التي تنجو من دورتي Scavenge يتم ترقيتها الى الجيل القديم.
الجيل القديم: يتم ترقية الكائنات التي نجت من عدة عمليات جمع للجيل الشاب الى هنا. هذه المساحة اكبر بكثير ويتم جمعها بشكل اقل تكرارا باستخدام نوع من التمييز والمسح يسمى Mark-Compact. لان عمليات جمع الجيل القديم اكثر تكلفة، يحاول المحرك تقليلها. خوارزمية Mark-Compact لا تحرر الكائنات غير القابلة للوصول فحسب بل تضغط الكائنات المتبقية ايضا لتقليل تجزئة الذاكرة.
مثال: انماط عمر الكائنات
// كائنات قصيرة العمر (يتم جمعها بسرعة في الجيل الشاب)
function processData(items) {
// هذه الكائنات المؤقتة تموت عند عودة الدالة
let temp = items.map(item => ({
id: item.id,
processed: true
}));
let result = temp.filter(item => item.processed);
return result.length;
// 'temp' والمصفوفات الوسيطة يتم جمعها بسرعة
}
// كائنات طويلة العمر (يتم ترقيتها الى الجيل القديم)
const appCache = new Map();
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
});
// هذه تستمر طوال عمر التطبيق
// ويتم ترقيتها الى الجيل القديم بعد النجاة
// من عدة دورات GC للجيل الشاب
تسربات الذاكرة الشائعة
يحدث تسرب الذاكرة عندما لا يتم تحرير الذاكرة التي لم تعد مطلوبة لان البرنامج يحتفظ بمرجع اليها مما يمنع جامع القمامة من استعادتها. بمرور الوقت، تسبب تسربات الذاكرة تطبيقك باستهلاك المزيد والمزيد من الذاكرة مما يؤدي الى بطء وعدم استجابة وفي النهاية انهيارات. اليك الانماط الاكثر شيوعا التي تسبب تسربات الذاكرة في جافاسكريبت.
1. المتغيرات العامة العرضية
عندما تعين قيمة لمتغير غير معلن، تنشئ جافاسكريبت هذا المتغير كخاصية للكائن العام. هذه المتغيرات تستمر طوال عمر التطبيق ولا يتم جمعها ابدا الا اذا تمت ازالتها صراحة.
مثال: المتغيرات العامة العرضية
// سيئ: متغير عام عرضي
function processRequest(data) {
// مفقود 'let' او 'const' او 'var'
// هذا ينشئ متغيرا عاما!
results = JSON.parse(data);
processedCount = results.length;
}
// سيئ: 'this' يشير الى الكائن العام في الوضع غير الصارم
function setName() {
this.username = 'admin'; // window.username في المتصفحات
}
setName();
// جيد: استخدم الوضع الصارم لمنع المتغيرات العامة العرضية
'use strict';
function safeProcess(data) {
// هذا الان يرمي ReferenceError بدلا من
// انشاء متغير عام
// results = JSON.parse(data); // ReferenceError!
const results = JSON.parse(data); // صحيح
return results;
}
2. المؤقتات والفواصل المنسية
المؤقتات التي تنشئها setInterval تستمر في العمل الى اجل غير مسمى ما لم يتم ايقافها صراحة. اذا كانت دالة رد الاتصال الخاصة بالمؤقت تشير الى كائنات يجب ان تجمع كقمامة، فان هذه الكائنات ستبقى حية طالما يعمل المؤقت.
مثال: تسربات المؤقتات واصلاحها
// سيئ: فاصل منسي يبقي البيانات حية
function startPolling() {
let hugeData = new Array(1000000).fill('data');
setInterval(() => {
// هذا الاغلاق يبقي 'hugeData' حية للابد
// لان الفاصل لا يتوقف ابدا
console.log(hugeData.length);
}, 1000);
}
// جيد: خزن معرف الفاصل وامسحه عند الانتهاء
function startPollingFixed() {
let hugeData = new Array(1000000).fill('data');
const intervalId = setInterval(() => {
console.log(hugeData.length);
}, 1000);
// مسح بعد 10 ثواني
setTimeout(() => {
clearInterval(intervalId);
hugeData = null; // السماح لـ GC بجمع البيانات
}, 10000);
return intervalId; // ارجاع المعرف حتى يمكن للمستدعي مسحه
}
// جيد: في دورة حياة المكون نظف دائما
class DataPoller {
start() {
this.data = fetchLargeDataset();
this.intervalId = setInterval(() => {
this.process();
}, 5000);
}
stop() {
clearInterval(this.intervalId);
this.intervalId = null;
this.data = null;
}
}
3. عقد DOM المنفصلة
عقدة DOM المنفصلة هي عنصر تمت ازالته من شجرة المستند لكنه لا يزال مشارا اليه بواسطة كود جافاسكريبت. بما ان كود جافاسكريبت يحمل مرجعا، لا يمكن لجامع القمامة تحرير الذاكرة التي يشغلها العنصر وجميع ابنائه.
مثال: تسربات عقد DOM المنفصلة
// سيئ: الاحتفاظ بمراجع لعقد DOM المزالة
let detachedNodes = [];
function addAndRemoveElement() {
let div = document.createElement('div');
div.innerHTML = '<p>محتوى كبير هنا...</p>'.repeat(1000);
document.body.appendChild(div);
// تخزين المرجع قبل الازالة
detachedNodes.push(div);
// ازالة من DOM لكن المرجع في المصفوفة
// يمنع جمع القمامة
document.body.removeChild(div);
}
// جيد: تنظيف المراجع عند ازالة العقد
function addAndRemoveElementFixed() {
let div = document.createElement('div');
div.innerHTML = '<p>محتوى</p>';
document.body.appendChild(div);
document.body.removeChild(div);
div = null; // ازالة المرجع والسماح بجمع القمامة
}
// جيد: استخدم WeakRef للمراجع الاختيارية لـ DOM
let weakNodeRef = null;
function trackElement() {
let div = document.createElement('div');
document.body.appendChild(div);
weakNodeRef = new WeakRef(div);
// لاحقا تحقق اذا كانت العقدة لا تزال موجودة:
// const node = weakNodeRef.deref();
// if (node) { /* لا تزال حية */ }
}
4. الاغلاقات التي تحتفظ بالمراجع
الاغلاقات هي ميزة قوية في جافاسكريبت، لكنها يمكن ان تبقي كائنات كبيرة حية عن غير قصد اذا التقط الاغلاق متغيرات من نطاقه الخارجي تشير الى بنى بيانات كبيرة.
مثال: تسربات ذاكرة الاغلاقات
// سيئ: الاغلاق يبقي النطاق الخارجي بالكامل حيا
function createHandler() {
let largeArray = new Array(1000000).fill('x');
let importantValue = 42;
// هذا الاغلاق يستخدم فقط 'importantValue' لكن
// المحرك قد يبقي 'largeArray' حية ايضا
return function handler() {
return importantValue;
};
}
// جيد: اجعل البيانات الكبيرة null قبل ارجاع الاغلاق
function createHandlerFixed() {
let largeArray = new Array(1000000).fill('x');
let importantValue = computeFrom(largeArray);
// تحرير المصفوفة الكبيرة قبل انشاء الاغلاق
largeArray = null;
return function handler() {
return importantValue;
};
}
function computeFrom(arr) {
return arr.length;
}
5. مستمعو الاحداث غير المزالين
مستمعو الاحداث الذين يضافون ولا يزالون ابدا يمنعون جامع القمامة من تحرير دالة المستمع واي متغيرات التقطتها من خلال الاغلاقات. هذا مشكلة خاصة في تطبيقات الصفحة الواحدة حيث يتم انشاء العناصر وتدميرها ديناميكيا.
مثال: تسربات مستمعي الاحداث
// سيئ: اضافة مستمعين بدون ازالتهم
function setupHandlers() {
let data = loadHugeDataset();
// في كل مرة يتم فيها استدعاء setupHandlers يضاف مستمع جديد
// بدون ازالة القديم
document.getElementById('btn').addEventListener('click', () => {
processData(data);
});
}
// جيد: استخدم دوال مسماة وازل المستمعين
function setupHandlersFixed() {
let data = loadHugeDataset();
function handleClick() {
processData(data);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', handleClick);
// ارجاع دالة تنظيف
return function cleanup() {
btn.removeEventListener('click', handleClick);
data = null;
};
}
// جيد: استخدم AbortController للتنظيف الجماعي
function setupMultipleHandlers() {
const controller = new AbortController();
document.getElementById('btn1').addEventListener('click', handler1, {
signal: controller.signal
});
document.getElementById('btn2').addEventListener('click', handler2, {
signal: controller.signal
});
document.getElementById('btn3').addEventListener('keydown', handler3, {
signal: controller.signal
});
// ازالة جميع المستمعين مرة واحدة
return function cleanup() {
controller.abort();
};
}
// جيد: استخدم { once: true } للمعالجات لمرة واحدة
document.getElementById('submitBtn').addEventListener('click', () => {
submitForm();
}, { once: true }); // يزال تلقائيا بعد اول تفعيل
تحديد تسربات الذاكرة باستخدام Chrome DevTools
توفر Chrome DevTools ادوات قوية لتحديد وتشخيص تسربات الذاكرة. تقدم علامة تبويب Memory ثلاثة انواع رئيسية من التنميط تساعدك على فهم كيفية استخدام تطبيقك للذاكرة.
لقطات الكومة
تلتقط لقطة الكومة الحالة الكاملة لكومة جافاسكريبت في نقطة زمنية محددة. تظهر كل كائن في الذاكرة وحجمه وما يشير اليه ومسافته من جذر GC. التقنية الاكثر فعالية هي طريقة "اللقطات الثلاث": التقط لقطة قبل تنفيذ اجراء، نفذ الاجراء، التقط لقطة اخرى، تراجع عن الاجراء، والتقط لقطة ثالثة. الكائنات التي تظهر في اللقطة 2 ولكن ليس في اللقطتين 1 او 3 من المحتمل ان تكون تسرب.
الجدول الزمني للتخصيص
يسجل الجدول الزمني للتخصيص تخصيصات الذاكرة على مدى فترة زمنية، ويظهر لك متى واين يتم انشاء الكائنات. الاشرطة الزرقاء تمثل التخصيصات التي لا تزال حية في نهاية التسجيل، بينما الاشرطة الرمادية تمثل التخصيصات التي تم جمعها كقمامة. نمط متزايد باستمرار من الاشرطة الزرقاء يشير الى تسرب ذاكرة.
اخذ عينات التخصيص
اخذ عينات التخصيص هو وضع تنميط خفيف يقوم دوريا باخذ عينات من مكدس الاستدعاء اثناء التخصيصات. ينتج حملا اقل من الجدول الزمني للتخصيص مما يجعله مناسبا للتنميط في بيئات شبيهة بالانتاج. يظهر اي الدوال مسؤولة عن اكبر عدد من تخصيصات الذاكرة.
مثال: كشف التسربات باستخدام واجهة الاداء البرمجية
// استخدم performance.memory لمراقبة استخدام الذاكرة
// (كروم/ايدج فقط يتطلب علامة --enable-precise-memory-info)
function checkMemory() {
if (performance.memory) {
console.log('حد حجم كومة JS:',
formatBytes(performance.memory.jsHeapSizeLimit));
console.log('اجمالي حجم كومة JS:',
formatBytes(performance.memory.totalJSHeapSize));
console.log('حجم كومة JS المستخدم:',
formatBytes(performance.memory.usedJSHeapSize));
}
}
function formatBytes(bytes) {
if (bytes === 0) return '0 بايت';
const k = 1024;
const sizes = ['بايت', 'كيلوبايت', 'ميغابايت', 'غيغابايت'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// مراقبة الذاكرة بمرور الوقت لكشف التسربات
function monitorForLeaks(intervalMs, durationMs) {
const readings = [];
const id = setInterval(() => {
if (performance.memory) {
readings.push({
time: Date.now(),
used: performance.memory.usedJSHeapSize
});
}
}, intervalMs);
setTimeout(() => {
clearInterval(id);
analyzeReadings(readings);
}, durationMs);
}
function analyzeReadings(readings) {
if (readings.length < 2) return;
const first = readings[0].used;
const last = readings[readings.length - 1].used;
const growth = last - first;
console.log('نمو الذاكرة:', formatBytes(growth));
if (growth > 10 * 1024 * 1024) { // اكثر من 10 ميغابايت
console.warn('تم اكتشاف تسرب ذاكرة محتمل!');
}
}
WeakRef و FinalizationRegistry
قدم ES2021 ميزتين WeakRef و FinalizationRegistry تمنحان المطورين مزيدا من التحكم في كيفية تفاعل الكائنات مع جمع القمامة. هذه ادوات متقدمة يجب استخدامها بشكل مقتصد وفقط عندما تكون الانماط الابسط غير كافية.
WeakRef ينشئ مرجعا ضعيفا لكائن. على عكس المرجع العادي (القوي)، المرجع الضعيف لا يمنع جامع القمامة من جمع الكائن المشار اليه. يمكنك محاولة الغاء مرجع WeakRef باستخدام طريقة deref() التي ترجع الكائن اذا كان لا يزال موجودا او undefined اذا تم جمعه.
FinalizationRegistry يسمح لك بتسجيل دالة رد اتصال يتم استدعاؤها بعد جمع كائن كقمامة. هذا يمكن ان يكون مفيدا لتنظيف الموارد الخارجية المرتبطة بكائن مثل مقابض الملفات واتصالات الشبكة او مدخلات في ذاكرة التخزين المؤقت.
مثال: WeakRef و FinalizationRegistry
// WeakRef لذاكرة تخزين مؤقت فعالة
class WeakCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry((key) => {
// يتم استدعاؤها بعد جمع الكائن المخزن كقمامة
// تنظيف مدخل Map
const ref = this.cache.get(key);
if (ref && !ref.deref()) {
this.cache.delete(key);
console.log('تم تنظيف مدخل التخزين المؤقت للمفتاح:', key);
}
});
}
set(key, value) {
// تخزين مرجع ضعيف بدلا من الكائن الفعلي
const ref = new WeakRef(value);
this.cache.set(key, ref);
// التسجيل لاشعار التنظيف
this.registry.register(value, key);
}
get(key) {
const ref = this.cache.get(key);
if (ref) {
const value = ref.deref();
if (value !== undefined) {
return value; // الكائن لا يزال حيا
}
// تم جمع الكائن -- تنظيف
this.cache.delete(key);
}
return undefined; // خطا في التخزين المؤقت
}
get size() {
return this.cache.size;
}
}
// الاستخدام
const cache = new WeakCache();
function loadUserProfile(userId) {
let cached = cache.get(userId);
if (cached) {
return cached; // استخدام النسخة المخزنة
}
// جلب وتخزين الملف الشخصي
const profile = { id: userId, name: 'User ' + userId, data: '...' };
cache.set(userId, profile);
return profile;
}
FinalizationRegistry فورا بعد جمع الكائن او بعد وقت طويل او ربما لا يتم استدعاؤه ابدا (على سبيل المثال اذا خرج البرنامج قبل تشغيل دورة GC). لا تعتمد ابدا على FinalizationRegistry لمهام التنظيف الحرجة. استخدم طرق التنظيف الصريحة مثل انماط close() او dispose() بدلا من ذلك، وعامل FinalizationRegistry كشبكة امان عندما يتم تفويت التنظيف الصريح.انماط الترميز الفعالة للذاكرة
بالاضافة الى تجنب التسربات، هناك العديد من الانماط التي تساعدك على استخدام الذاكرة بشكل اكثر كفاءة مما يؤدي الى اداء افضل واستهلاك اقل للموارد.
تجميع الكائنات
بدلا من انشاء وتدمير العديد من الكائنات قصيرة العمر، اعد استخدامها من مجموعة مخصصة مسبقا. هذا يقلل ضغط GC وحمل التخصيص وهو مهم بشكل خاص في الكود الحرج للاداء مثل حلقات الالعاب واطارات الرسوم المتحركة.
مثال: نمط تجميع الكائنات
class ObjectPool {
constructor(factory, reset, initialSize = 10) {
this.factory = factory;
this.reset = reset;
this.pool = [];
// تخصيص الكائنات مسبقا
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.factory());
}
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
// المجموعة نفدت -- انشئ كائنا جديدا
return this.factory();
}
release(obj) {
this.reset(obj); // اعادة تعيين الحالة لاعادة الاستخدام
this.pool.push(obj);
}
}
// الاستخدام: نظام جسيمات للرسوم المتحركة
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, vx: 0, vy: 0, life: 0, active: false }),
(p) => { p.x = 0; p.y = 0; p.vx = 0; p.vy = 0; p.life = 0; p.active = false; },
100
);
function spawnParticle(x, y) {
const particle = particlePool.acquire();
particle.x = x;
particle.y = y;
particle.vx = Math.random() * 2 - 1;
particle.vy = Math.random() * -3;
particle.life = 60;
particle.active = true;
return particle;
}
function retireParticle(particle) {
particle.active = false;
particlePool.release(particle);
}
استخدم المصفوفات المحددة النوع للبيانات الرقمية الكبيرة
المصفوفات المحددة النوع مثل Float64Array و Int32Array و Uint8Array تخزن البيانات في مخازن ذاكرة متجاورة وهي اكثر كفاءة بكثير في الذاكرة من المصفوفات العادية للبيانات الرقمية.
مثال: المصفوفات المحددة النوع مقابل المصفوفات العادية
// سيئ: مصفوفة عادية من الارقام (كل عنصر كائن مغلف)
const regularArray = new Array(1000000);
for (let i = 0; i < 1000000; i++) {
regularArray[i] = Math.random();
}
// ذاكرة تقريبية: ~8 ميغابايت+ (كل رقم كائن كومة)
// جيد: مصفوفة محددة النوع (مخزن ذاكرة متجاور)
const typedArray = new Float64Array(1000000);
for (let i = 0; i < 1000000; i++) {
typedArray[i] = Math.random();
}
// ذاكرة تقريبية: ~8 ميغابايت (8 بايت بالضبط لكل رقم بدون حمل)
// استخدم ArrayBuffer للبيانات الثنائية
const buffer = new ArrayBuffer(1024); // مخزن 1 كيلوبايت
const view = new DataView(buffer);
view.setInt32(0, 42); // كتابة عدد صحيح 32 بت عند البايت 0
view.setFloat64(4, 3.14); // كتابة عدد عشري 64 بت عند البايت 4
WeakMap و WeakSet للبيانات الوصفية
عندما تحتاج الى ربط بيانات وصفية بكائنات دون منع جمع القمامة، استخدم WeakMap و WeakSet. على عكس Map و Set العاديين، تسمح المجموعات الضعيفة لمفاتيحها بان يتم جمعها كقمامة عندما لا توجد مراجع اخرى اليها.
مثال: WeakMap للبيانات الخاصة
// تخزين بيانات خاصة مرتبطة بعناصر DOM
const elementData = new WeakMap();
function trackElement(element) {
elementData.set(element, {
clickCount: 0,
firstSeen: Date.now(),
interactions: []
});
}
function recordClick(element) {
const data = elementData.get(element);
if (data) {
data.clickCount++;
data.interactions.push({ type: 'click', time: Date.now() });
}
}
// عندما يتم ازالة عنصر DOM ولا تبقى مراجع JS
// يتم جمع كل من العنصر وبياناته المرتبطة في WeakMap
// تلقائيا كقمامة. لا حاجة للتنظيف!
واجهة performance.memory البرمجية
توفر واجهة performance.memory البرمجية طريقة برمجية لمراقبة استخدام ذاكرة كومة جافاسكريبت في وقت التشغيل. بينما هي متاحة حاليا فقط في متصفحات كروميوم، فهي لا تقدر بثمن لبناء لوحات معلومات مراقبة الذاكرة وانظمة كشف التسرب الالية.
مثال: بناء مراقب ذاكرة
class MemoryMonitor {
constructor(options = {}) {
this.sampleInterval = options.sampleInterval || 5000;
this.maxSamples = options.maxSamples || 100;
this.warningThresholdMB = options.warningThresholdMB || 100;
this.samples = [];
this.intervalId = null;
}
start() {
if (!performance.memory) {
console.warn('performance.memory غير متاحة');
return;
}
this.intervalId = setInterval(() => {
this.takeSample();
}, this.sampleInterval);
this.takeSample(); // اخذ عينة اولية
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
takeSample() {
const sample = {
timestamp: Date.now(),
usedHeap: performance.memory.usedJSHeapSize,
totalHeap: performance.memory.totalJSHeapSize,
heapLimit: performance.memory.jsHeapSizeLimit
};
this.samples.push(sample);
if (this.samples.length > this.maxSamples) {
this.samples.shift();
}
const usedMB = sample.usedHeap / (1024 * 1024);
if (usedMB > this.warningThresholdMB) {
console.warn('استخدام ذاكرة عالي: ' + usedMB.toFixed(2) + 'MB');
}
}
getGrowthRate() {
if (this.samples.length < 2) return 0;
const first = this.samples[0];
const last = this.samples[this.samples.length - 1];
const timeDiff = (last.timestamp - first.timestamp) / 1000;
const memDiff = last.usedHeap - first.usedHeap;
return memDiff / timeDiff; // بايت في الثانية
}
report() {
const rate = this.getGrowthRate();
const current = this.samples[this.samples.length - 1];
return {
currentUsedMB: (current.usedHeap / (1024 * 1024)).toFixed(2),
growthRateKBPerSec: (rate / 1024).toFixed(2),
isLeaking: rate > 1024 // اكثر من 1 كيلوبايت/ثانية نمو مستمر
};
}
}
// الاستخدام
const monitor = new MemoryMonitor({ warningThresholdMB: 50 });
monitor.start();
// تحقق دوريا
setTimeout(() => {
console.log(monitor.report());
monitor.stop();
}, 60000);
performance.memory البرمجية ترجع قيما محدثة فقط بعد دورة جمع قمامة. للقياسات الادق، توفر Chrome DevTools زر "Collect garbage" (ايقونة سلة المهملات) في علامتي تبويب Performance و Memory الذي يفرض دورة GC قبل اخذ القياس.ملخص افضل الممارسات
ادارة الذاكرة في جافاسكريبت تتطلب الوعي بدلا من التحكم اليدوي. اليك النقاط الرئيسية من هذا الدرس التي يجب تطبيقها في كل مشروع:
- استخدم دائما
'use strict'او وحدات ES لمنع المتغيرات العامة العرضية. - امسح المؤقتات والفواصل باستخدام
clearTimeoutوclearIntervalعندما لا تكون مطلوبة. - ازل مستمعي الاحداث عند تدمير العناصر او استخدم
AbortControllerللتنظيف الجماعي. - اجعل المراجع للكائنات الكبيرة null عندما لا تحتاجها.
- تجنب تخزين مراجع لعقد DOM التي تمت ازالتها من المستند.
- استخدم
WeakMapوWeakSetوWeakRefعندما تحتاج ارتباطات لا يجب ان تمنع جمع القمامة. - انمذج تطبيقك بانتظام باستخدام علامة تبويب Memory في Chrome DevTools ولقطات الكومة.
- فكر في تجميع الكائنات لسيناريوهات انشاء وتدمير الكائنات عالية التكرار.
- استخدم المصفوفات المحددة النوع لمجموعات كبيرة من البيانات الرقمية.
- راقب استخدام الذاكرة في الانتاج باستخدام واجهة
performance.memoryالبرمجية حيثما كانت متاحة.
تمرين عملي
ابنِ محاكيا صغيرا لتطبيق صفحة واحدة يوضح تسربات الذاكرة ويصلحها. انشئ صفحة بها اربعة ازرار: "اضافة مكون" يجب ان ينشئ عنصر DOM ديناميكيا مع مستمع احداث ومؤقت setInterval يحدث محتواه كل ثانية. "ازالة مكون" يجب ان يزيل اخر عنصر مضاف من DOM. "فحص الذاكرة" يجب ان يعرض عدد المؤقتات النشطة ومستمعي الاحداث. "اصلاح التسربات" يجب ان ينظف بشكل صحيح جميع العقد المنفصلة ويمسح جميع المؤقتات اليتيمة ويزيل جميع مستمعي الاحداث اليتيمة. اولا نفذ النسخة "المسربة" حيث تقوم ازالة المكون فقط باستدعاء removeChild بدون تنظيف المؤقتات او المستمعين. افتح علامة تبويب Memory في Chrome DevTools والتقط لقطة كومة بعد اضافة وازالة 20 مكونا -- لاحظ نمو الذاكرة. ثم نفذ النسخة "المصلحة" حيث تقوم ازالة المكون باستدعاء clearInterval و removeEventListener بشكل صحيح وتجعل المراجع null قبل ازالة العقدة. التقط لقطة كومة اخرى بعد نفس التسلسل وقارن اللقطتين لرؤية الفرق الذي يحدثه التنظيف الصحيح.