جاوا اسکریپت چه جوری کار میکنه؟ مدیریت حافظه و ۴ روش برای مهار کردن مموری لیک
سری "جاوا اسکریپت چه جوری کار میکنه؟" بازگردان مقاله های Alexander Zlatkov به زبون خودم هست. هدف از این کار در مرحله ی اول یادگیری خودم و انتشار این مقاله به فارسیه.
قرارم بر این نیست که همه کلمات رو به فارسی بگم, به این دلیل که اگر خواستید بیشتر در مورد کلمات کلیدیش جستو جو داشته باشید, بتونید راحت تر این کارو انجام بدین.
اهمیت این نوشته برای من محتواش هست نه طرز بیان یا نگارشش. ممنون میشم اگر در محتوا یا در طرز بیان یا نگارش اشکالی هست بیان کنید تا بتونم بهترش کنم.
بازگردانی شده از: How JavaScript works: memory management + how to handle 4 common memory leaks
هفته ها پیش, ما یه سری مقالات رو در مورد درک عمقی تر از جاوا اسکریپت و چه جوری کار کردنش شروع کردیم: ما فکر میکردیم که با دونستن اجزای تشکیل دهنده جاوا اسکریپت و تعاملشون با هم شما میتونید کدها و برنامه های بهتری بنویسید.
اولین مطلب این سری تمرکزش روی مرور بر موتور جاوا اسکریپت, run time و call stack بوده. مطلب دوم این سری بررسی اجزای داخلی موتور V8 گوگل و چند تا نکته درباره ی بهتر جاوا اسکریپت کد زدن هست.
در سومین پست, در مورد موضوع حیاتی دیگه ای بحث میکنیم به نام memory leak که داره بیشتر و بیشتر توسط برنامه نویس ها فراموش میشه. دلیلش هم پیچیده شدن و بالغ شدن زبان های برنامه نویسی هست که داریم هر روز ازشون استفاده میکنیم و همین طور به توضیح چند تا نکته در مورد کنترل مموری لیک تو جاوا اسکریپت میپردازیم.
بررسی کوتاه
زبونی مثل C, به کمک تابع هایی مثل malloc و free امکان مدیریت سطح پایین حافظه رو به دولوپر سپرده. این تابع های سطح پایین و اولیه به توسعه دهنده ها کمک میکنه تا مقداری از حافظه رو از سیستم عامل در اختیار بگیرن یا به سیستم عامل برگردونن.
جاوا اسکریپت در زمان ساخت آبجکت ها و استرینگ ها حافظه رو اشغال میکنه و وقتی نیازی به اون ها نباشه حافظه رو آزاد میکنه که به این پروسه گاربج کالکشن میگیم. این فرایند به ظاهر اتوماتیک به دولوپر ها ( حتی زبان های سطح بالا ) این اطمینان خاطر اشتباه رو میده که نباید دیگه به مدیریت حافظه فکر بکنند. که این جور فکر کردن اشتباه بزرگی هست.
حتی موقع کار کردن با زبون های سطح بالا هم, دولوپر ها باید درکی از مدیریت حافظه داشته باشن ( یا حد اقل اصول پایه ای اون رو بدونن ). گاهی وقتا فرایند اوتوماتیک مدیریت حافظه, مشکلاتی رو به همراه خودش داره ( مثل باگ ها یا محدودیت ها در پیاده سازی گاربج کالکتور و ... ) که دولوپر ها برای کنترل کردن این مشکل ها باید اون هارو درک کرده باشن ( یا یه روشی برای دور زدن پیدا کنن که کمترین trade off و قانون شکنی و پا گذاشتن روی اصول رو داشته باشه ).
چرخه ی عمر حافظه - Memory life cycle
مهم نیست که چه زبونی رو دارید استفاده میکنید ولی عموما چرخه ی عمر حافظه بین اون ها یک شکل هست :
این یه توضیح مختصر از اتفاقاتی که تو هر قدم از چرخه رخ میده هست:
- اشغال حافظه: حافظه ی اشغال شده و در حقیقت رزور شده توسط سیستم عامل برای برنامه ی شما که این امکان رو میده تا از این حافظه در برنامه استفاده بشه. تو زبان های سطح پایین مثل C این یک فرایند مستقیم هست که دولوپر باید اون رو کنترل کنه. اما تو زبان های سطح بالا این فرایند برای شما به طور خودکار انجام میشه.
- استفاده از حافظه: قدمی هست که در واقع برنامه ی شما از حافظه از قبل اشغال شده استفاده میکنه. وقتی که شما از متغیر ها اشغال شده توی کدتون استفاده میبرید فرایند خوندن و نوشتن در همین قدم اتفاق میوفته.
- رها سازی حافظه: حالا وقت آزاد کردن و در دسترس قرار دادن مجدد همه ی حافظه ای هست که شما دیگه به اون احتیاج ندارید. مثل اشغال کردن حافظه این فرایند هم تو زبان های سطح پایین به طور مستقیم باید مدیریت بشه.
برای مرور سریع بر مفاهیم پشته فراخوانی و پشته ی حافظه میتونید پست اول این سری رو بخونید.
حافظه چیست؟
قبل از این که بپریم وسط حافظه تو جاوا اسکریپت, بهتره خیلی خلاصه وار در مورد ماهیت عمومی حافظه و چه جوری کار کردنش بحث کنیم.
تو سطح سخت افزاری, حافظه ی کامپویتر تشکیل شده از کلی flip flop. هر فلیپ فلاپ از تعدادی ترانزیستور ساخته شده و یک بیت جا داره. تعدادی فلیپ فلاپ منحصر به فرد با یه شناسه ی منحصر به فرد در دسترس هستن, که به کمک این شناسه بتونیم اون هارو بخونیم یا روشون مجددا بنویسیم. با این حال به طور مفهومی میتونیم کل حافظه ی کامپیوترو یه آرایه خیلی بزرگ ببینیم که میتونیم روش بنویسیم و ازش بخونیم.
و ازون جایی که آدمی زاد زیاد تو انجام محاصبات و فکر کردن هاش به سبک بیتی خوب نیست, سعی کرده اون هارو به گروه های بزرگتری دسته بندی کنه, که با هم دیگه میشه اعداد رو نشون داد.
خیلی چیزا تو حافظه نگه داری میشه.
- تمام متغیر ها و داده هایی که توسط برنامه ها استفاده میشن.
- کد برنامه نویس ها, شامل سیستم عامل هم میشه
کامپایلر و سیستم عامل اکثر کارهای مدیریت حافظه رو برای ما انجام میدن اما ما پیشنهاد می کنیم یه سرکی به اتفاق هایی که این دو برای مدیریت حافظه انجام میدن بکشید.
وقتی شما کدتون رو کامپایل میکنید, کامپایلر میتونه تایپ های متغیر های شمارو ببینه و قبل اجرا محاسبه کنه که چقدر حافظه نیاز دارن. مقدار مورد نیاز حافظه تو فضای پشته stack space برای برنامه اشغال میشه. به فضایی که این متغیر ها در اون نگهداری میشن فضای پشته گفته میشه بخاطر این که وقتی تابع ها فراخوانی میشن, حافظه ی اون ها بالای همه ی حافظه اضافه میشه. و وقتی هم که از بین میرن, به ترتیب LIFO last-in, last-out حذف میشن. برای مثال این معرف هارو ببینید:
int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes
کامپایلر به سرعت تشخیص میده که این کد نیاز به 4 + 4 × 4 + 8 = 28 bytes حافظه داره.
با سایز الان اینتجر و دابل کامپایلر یه همچین محاسبه ای رو داره. حدود ۲۰ سال پیش اینتجر ۲ بایت و دابل ۴ بایت فضا داشته. برای الان لازم نیست که کد شما بدونه بایت و اینتجر چقدر فضا اشغال میکنن.
کامپایلر کد هایی رو اضافه میکنه که با سیستم عامل در جهت درخواست بایت های مورد نیاز روی استک برای ذخیره شدن متغیر ها ما تعامل میکنه.
تو مثال بالا کامپایلر آدرس دقیق هر متغیر رو میدونه. در حقیقت وقتی ما چیزی رو در متغیر n قرار میدیم, n ترجمه میشه به یه چیزی مثل این: "آدرس حافظه 4127963".
به این توجه کنید که اگر بخوایم به خونه ی چهارم آرایه x دسترسی داشته باشیم, ممکنه به داده ای که در فضای متغیر m هست دسترسی پیدا کنیم. این به این خاطر هست که المان چهارم در آرایه x وجود نداره. این خونه چهارم چهار بایت دور تر از آخرین المان ذخیره شده در فضای آخرین المان ارایه x است که ممکنه تعامل ما باهاش باعث خونده شدن (اور رایت کردن) قسمتی از بیت های حافظه ی متغیر m بشه. خب این اتفاق اگر بیوفته مشخصا عواقب خوبی برای اجرای بقیه برنامه نداره.
وقتی تابعی یه تابع دیگه رو فراخوانی میکنه, هر کدوم از این تابع ها قسمت مورد نیاز خودشون رو از استک میگیرن. تمام متغیر های داخلی خودشون رو تو قسمتی که بهشون اختصاص داده شده میذارن, اما شمارنده ی برنامه هم جایی که برنامه در حال اجرا هست رو در خودش نگه داری میکنه. وقتی اجرا تابع به پایان رسید حافظه ای که برداشته برای استفاده های دیگه مجددا آزاد میشه.
اختصاص گیری پویا - Dynamic allocation
متاسافنه, وقتی نمیدونیم چقدر حافظه رو باید اختصاص بدیم به یه برنامه تو زمان کامپایل محاسبات سخت میشه و آسون نیست دیگه. فرض کنید میخوایم یه همچین کاری کنیم:
int n = readInput(); // reads input from the user
...
// create an array with "n" elements
این جا تو زمان کامپایل, کامپایلر نمیتونه تشخیص بده که آرایه n چقدر حافظه نیاز داره چرا که این اندازه رو مقداری که کاربر مشخص کرده تعیین میکنه.
به همین منظور کامپایلر نمیتونه جایی رو برای این متغیر توی استک در نظر بگیره. به جاش برنامه نیاز داره به طور مستقیم از سیستم عامل بخواد که تو زمان اجراش حافظه ی مورد نظر رو بهش اختصاص بده. این حافظه از heap space اختصاص داده میشه. تفاوت اختصاص حافظه ی استاتیک و داینامیک رو به طور خلاصه در جدول زیر میبینید:
اختصاص حافظه در جاوا اسکریپت
جاوا اسکریپت متغیر هارو از مدیریت کردن حافظه معاف کرده و این کار رو خودش در کنار تعریف کردن متغیر ها انجام میده.
var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and its contained values
var a = [1, null, 'str']; // (like object) allocates memory for the
// array and its contained values
function f(a) {
return a + 3;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
فراخوانی بعضی از تابع ها نتیجش اخصاص فضا به آبجکت هاست:
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element
تابع ها قابلیت اختصاص دادن حافظه متغیر ها و آبجکت هارو دارن.
var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// new array with 4 elements being
// the concatenation of a1 and a2 elements
استفاده از حافظه در جاوا اسکریپت
استفاده از حافظه در جاوا اسکریپت به معنی خوندن و نوشتن حافظه است. این کار هم با خوندن و نوشتن متغیر ها یا پاس دادن آرگیومنتی به تابع اتفاق بیوفته.
رها کردن حافظه در زمانی که دیگر نیازی بهش نیست
اکثر مشکلات مدیریت حافظه در این مرحله رخ میده. قسمت سخت قضیه اینه که متوجه بشیم که دقیقا کی حافظه ی اختصاص شده دیگه مورد نیاز نیست. گاهی هم به عهده ی دولوپر هست که کجای برنامه دیگه نیاز به اون تیکه از حافظه نیست و خودش اون رو آزاد کنه.
زبان های سطح بالا گاربج کالکتوری رو در خودشون دارن که کارش دنبال کردن حافظه ی اختصاص داده شده و استفاده از اون هست تا بتونه تشخیص بده کی دیگه اون حافظه مورد نیاز نیست و اون رو خالی کنه.
متاسفانه این فرایند تقریبی هست و فهمیدن این که کدوم حافظه دیگه مورد نیاز نیست توسط الگوریتم ها قابل انجام نیست.
اکثر گاربج کالکتور ها با حافظه هایی که دیگه در دسترس نیستن کار میکنن. برای مثلا تمام متغیر هایی که به اون قسمت از حافظه اشاره میکنن از اسکوپ اجرایی خارج شدن. این نوع از حافظه ها امکان آزاد سازی دارن هرچند که ممکنه درجای دیگه ای از برنامه متغیری به اون ها اشاره کنه ولی چون در ادامه برنامه نیازی به دسترسی پیدا کردن به اون حافظه و متغیر رو نداریم آزاد سازی اون مشکلی ایجاد نمیکنه.
گاربج کالکشن
بر اساس این که حقیقت که ما به طور قاتع نمیتونیم بگیم کدوم قسمت از حافظه دیگه مورد نیاز نیست. گاربج کالکتور ها قسمتی از یک راه حل عمومی رو پیاده سازی کردن. در این قسمت به مفاهیم لازم برای فهم اساس الگوریتم گاربج کالکتور ها و محدودیت هاشون میپردازیم.
مرجع های حافظه - Memory references
تو اینجا مفهوم آبجکت چیزی فراتر از آبجکت های معمولی جاوا اسکریپت و اسکوپ توابع و یا lexical scope هست.
لکسیکال اسکوپ تکلیف اسم متغیر هارو تو تابع ها تو در تو مشخص میکنه: تابع های داخلی به اسکوپ تابع بیرونی دست رسی دارن حتی اگر تابع بیرونی چیزی رو ریترن کرده باشه.
یکی از مفاهیم اصلی گاربج کالکتور ها بر اساس مرجع هست.
در حوزه ی مدیریت حافظه, درصورتی آبجکت a میتونه مرجعی از آبجکت b رو داشته باشه که بشه از a به b رسید. ( این دسترسی به طور مستقیم و غیر مستقیم میتونه وجود داشته باشه. ) برای مثال آبجکت جاوا اسکریپت به پروتو تایپ خودش به صورت غیر مستقیم ( یعنی اگه به پروتو تایپ یه فانکشن یا پراپرتی بدیم نیازی نیست که بنویس a.prototype.whatever فقط کافیه بنویسیم a.whatever) دسترسی داره و به طور مستقیم به پراپرتی های خودش.
شماره ارجاع ها گاربج کالکشن
این ساده ترین الگوریتم گاربج کالکشن هست. یه آبجکت زمانی قابلیت آزاد سازی داره که هیچ چیزی بهش ارجاع داده نشده باشه.
به این قطعه کد نگاه کنید:
var o1 = {
o2: {
x: 1
}
};
// 2 objects are created.
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected
var o3 = o1; // the 'o3' variable is the second thing that
// has a reference to the object pointed by 'o1'.
o1 = 1; // now, the object that was originally in 'o1' has a
// single reference, embodied by the 'o3' variable
var o4 = o3.o2; // reference to 'o2' property of the object.
// This object has now 2 references: one as
// a property.
// The other as the 'o4' variable
o3 = '374'; // The object that was originally in 'o1' has now zero
// references to it.
// It can be garbage-collected.
// However, what was its 'o2' property is still
// referenced by the 'o4' variable, so it cannot be
// freed.
o4 = null; // what was the 'o2' property of the object originally in
// 'o1' has zero references to it.
// It can be garbage collected.
چرخه ها باعث مشکلات هستند (ارجاع های تو در تو a به b و b به a)
تو این راه حل محدودیت هایی روی چرخه ها وجود داره. تو مثالی که داریم دو تا آبجکت ساخته شدن و به هم ارجاع داده شدن, به همین منظور یک چرخه رو به وجود آوردن. اون ها بعد از فراخوانی تابع از اسکوپ خارج و بی کاربرد میشن پس میشه حافظه اون ها رو آزاد کرد. اما الگوریتم شمارنده ارجاع ها اینجوری برداشت میکنه که حد اقل یک رفرنس به هردوی این آبجکت ها وجود داره پس نمیشه حافظه ی اون هارو آزاد کرد.
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}
f();
الگوریتم mark-and-sweep
سه قدم این الگوریتم
- ریشه ها: به طور عمومی ریشه های متغیر های گلوبال هستن که در کد بهشون ارجاع داده میشه. به طور مثال در جاوا اسکریپت متغیر گلوبالی که مثل یک ریشه رفتار میکنه آبجکت window هست. آبجکت مشابه در nodejs به اسم global هست. لیست کاملی از ریشه ها توسط گاربج کالکتور ساخته میشه.
- این الگوریتم تمام ریشه ها و فرزندان اون ریشه رو بررسی میکنه و اون هارو در حالت اکتیو نشانه گذاری میکنه ( یعنی اونهارو نباید دور بریزیم ).
- در نهایت گاربج کالکتور تمام حافظه ای که با اکتیو نشانه گذاری نشده رو به سیستم عامل برمیگردونه.
این الگوریتم از قبلیه بهتره چون آبجکتی که هیچ رفرنسی نداشته باشه به آبجکتی تبدیل شده که دیگه نیاز به دسترسی بهش نداریم اما برعکس این قضیه تو الگوریتم اول صادق نبود. یعنی آبجکتی بهش دسترسی نداریم ممکنه به کمک چرخه ها هنوز رفرنسی داشته باشه.
از ۲۰۱۲ تمام مرورگر های مدرن دارن از mark-and-sweep استفاده میکنن. تمام بهبود هایی که اتفاق افتاده (generational/incremental/concurrent/parallel garbage collection) اثر بهتر کردن پیاده سازی این الگوریتم هست. اما خود اصل الگوریتم و هدفش که که تشخیص دادن در دسترس بودن یه آبجکت هست بهتر نشده.
تو این مقاله میتونید بهینه سازی خود الگوریتم رو بخونید.
چرخه ها دیگه مشکل ساز نیستند
در اولین مثال بالا بعد از این که تابع چیزی رو ریترن میکنه دو آبجکت دیگه ارجاعی ندارن از آبجکت گلوبال. در نتیجه حافظه اونها توسط گاربج کالکتور آزاد میشه.
هرچند که این دو آبجکت به هم رفرنس دارن ولی توسط ریشه دیده نمیشن و حافظه ای که اشغال کردن باید آزاد بشه.
رفتار های غیر معمول گاربج کالکتور ها
هرچند که گاربج کالکتور ها راه حل عمومی و مناسبی هستند ولی مشکلات خودشون رو هم دارن.یکی از اونها قاطعیت در تصمیم گیری هست. به زبانی دیگر یعنی گاربج کالکتور ها غیر قابل پیش بینی هستند. دقیقا نمیشه گفت چه زمانی گاربج کالکتور شروع به جمع آوری حافظه ها میکنه و این به این معنا هست که بعضی وقت ها برنامه ها حافظه ی بیشتر از نیاز خودشون رو در اختیار میگیرن. تو حالت های مختلف این مورد باعث وقفه هایی در اجرا تو برنامه های حساس میشه. هر چند که قاطع نبودن گاربج کالکتور ها به این معنی هست که معلوم نیست کی شروع به اجرای جمع آوری بکنن, اما اکثر اون ها یک پترن مشابه رو جمع آوری حافظه بعد از اختصاص دادن حافظه به برنامه دارن. اگر اختصاص حافظه ای به برنماه صورت نگیره اکثر گاربج کالکتور ها به صورت آیدل و بی کار در میان و کاری نمیکنن. این سناریو رو در نظر بگیرید:
- مقدار قابل توجهی از حافظه به برنامه اختصاص داده میشه.
- اکثر این حافظه ها به دلیل غیر قابل دسترس بودن علامت گذاری میشن ( به فرض ما نال کردیم پوینتری رو که به اطلاعات کش ما دسترسی داشت و دیگه بهش نیاز نداریم )
- دیگه هیچ اختصاص حافظه ای رخ نده.
تو این سناریو اکثر گاربج کالکتور ها, شروع به جمع آوری حافظه نمیکنند. هر چند که این حافظه ها دیگر در دسترس نیستند ولی کالکتور ها اقدام به جمع آوری آن ها نمیکنند. به طور قطعی نمیتوان گفت همه این حافظه ها باید جمع آوری بشوند ولی دلیلی بر غیر معمول و زیاد بودن حجم حافظه ی در حال استفاده هستند.
مموری لیک ها چه هستند؟
همانطور که حافظه نشون میده، مموری لیک قطعاتی از حافظه ای است که برنامه در گذشته استفاده کرده اما دیگه نیازی به اون ها نیست اما هنوز به سیستم عامل و یا مجموعه حافظه آزاد برنگشتن.
زبان های برنامه نویسی برای مدیریت حافظه روش های متفاوتی نسبت به همدیگه پیاده سازی کردن. تصمیم گیری قاطع در مورد یک تکه از حافظه که استفاده شده یا نه یک مساله ی غیر قابل حله و بهتر بگم این توسعه دهند ها هستند که در نهایت تصمیم میگیرند که یک تکه از حافظه آزاد بهد و به سیستم عامل بازگرده یا نه.
بعضی از زبان های برنامه نویسی برای مدیریت حافظه ابزاری رو برای توسعه دهنده ها فراهم کردن. بعضی از اون ها هم از برنامه نویس انتظار دارن که زمان آزاد شدن حافظه رو مشخص کنه.
4 نوع متدوال از مموری لیک در جاوا اسکریپت
۱: متغیر های گلوبال
جاوا اسکریپت روش جالبی برای هندل کردن متغیر های تعریف نشده داره: وقتی به یک متغیر تعریف نشده ارجاع میدیم, یک متغیر در آبجکت گلوبال ساخته میشه. تو مرورگر متغیر گلوبال window هست. به این معنی که:
function foo(arg) {
bar = "some text";
}
برابر هست با
function foo(arg) {
window.bar = "some text";
}
بر فرض این که تنها استفاده از متغیر bar در تابع foo باشد بعد از اجرا تابع foo یک متغیر بی خودی گلوبال bar درست شده. اگر از کیورد var برای متغیر ها استفاده کنیم متغیر گلوبالی به وجود نمیاد. البته هنوز سناریو های بد تری هم میشه تصور کرد.
میشه حتی تصادفی به کمک this هم یه متغیر global ساخت:
function foo() {
this.var1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
با استفاده از use sctrict در اول اسکریپت, پارسر جاوا اسکریپت سخت گیرانه تر برخورد میکنه و از ساخته شدن همچین متغیر هایی جلوگیری میکنه.
متغیر گلوبال ناخواسته, قطعا یه مشکله هر چند که تو کد های خودمون هم از متغیر های گلوبال استفاده کردیم که قاعدتا گاربج کالکت نمیشن. توجه خاصی باید به متغیر های گلوبالی داشته باشیم که دیتای موقتی رو نگه میدارن یا دیتایی که پراسس بالایی روش هست. اگر مجبور هستید از متغیر های گلوبال استفاده کنید و هر جا که دیگه نیازی نیست به اون ها یا نال کنید یا مقدار دیگری رو درش ذخیره کنید.
- ۲: تایمر ها و کالبک هایی که فراموش شدن
بزارید به تابع setInterval اشاره کنیم که تو کدهامون استفادش میکنیم.
کتابخونه هایی که observers و یا ابزار هایی که کالبک قبول میکنن همیشه سعی میکنن این مورد هارو بعد از اتمام کارشون از دسترس خارج کنن هر چند که نمونه کد پایین رو هنوز میشه تو بعضی از کار ها پیدا کرد:
var serverData = loadData();
var interval = setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer = JSON.stringify(serverData);
clearInterval(interval);
}
}, 5000); //This will be executed every ~5 seconds.
در تکه کد بالا استفاده از اچ تی ام ال نود و دیتایی رو تو setInterval میبینیم که بعد از یک بار اجرا دیگه بهشون احتیاجی نیست. (مثلاش جالب نیست میدونم ولی حرفای بعدش خوبه).
خب اگر تو فرایند برنامه ابجکت renderer حذف یا با چیز دیگه ای جایگزین بشه هیچ وقت این ست اینتروال به سرانجام نمیرسه و چون در تابعی که داره از serverData داره استفاده میکنه گاربج کالکتور هیچوقت نمیتونه این داده رو که ممکنه کلی پراسس و رم رو مشغول کرده حذف کنه.
وقتی داریم از Observer ها استفاده میکنیم بهتره بعد از اتمام کارمون, اون هارو هم حذف کنیم. ( چه زمانی که دیگه به ابزرور نیازی نداریم و یا نودی که داریم ابزروش میکنیم دیگه در دسترس نیست )
خوشبختانه اکثر مرورگر های مدرن این کارو برای ما به طور خودکار انجام میدن. به طور اوتوماتیک وقتی یه آبجکتی دیگه در دسترس نباشه تمام آبزرور هاش رو کالکت میکنن حتی اگر ما یادمون رفته باشه که اون هارو حذف کنیم در کد.البته یه مرورگر مثل IE6 خیلی خسته تر از این حرفا هست که این کار هارو انجام بده.
اما جزو بست پرکتیس ها هست که وقتی آبجکتی از روند اجرایی برنامه خارج میشه, این آبزرور هارو تو لایه کد حذف کنیم. به مثال زیر دقت کنید:
var element = document.getElementById('launch-button');
var counter = 0;
function (event) {
counter++;
element = 'text ' + counter;
}
element.addEventListener('click', );
// Do stuff
element.removeEventListener('click', );
element.removeChild(element);
// Now when element goes out of scope,
// both element and will be collected even in old browsers // that don't handle cycles well.
البته باز اشاره کنیم که شما دیگه لازم نیست در لایه کد تابع removeEventListener رو قبل از این که نود اچ تی ام ال از دسترس خارج بشه فراخوانی کنید, چون مرورگر های مدرن این رفرنس سایکل هارو پیدا میکنه و اونهارو گاربج کالکت میکنه.
اگر هم که از کتابخونه ی jquery استفاده میکنید ( البته بقیه لایبرری ها هم به خوبی از این قضیه پشتیبانی میکنند ) میتونید قبل از از دسترس خارج شدن نود اچ تی ام ال لیسنرهایی که به اون بایند کردین رو حذف کنید و کتابخونه هم مطمان میشه که مموری لیکی اتفاق نیوفتاده باشه حتی تو مرورگر های قدیمی.
- 3: کلوژر ها
یکی از نقاط قوت جاوا اسکریپت کلوژر هاش هستن: تابع داخلی که به اسکوپ تابع بیرونی خودش دسترسی داره. با توجه به نحوه ی پیاده سازی رانتایم جاوا اسکریپت ممکن هست که به روش زیر مموری لیک داشته باشیم.
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // a reference to 'originalThing'
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
وتقی replaceThing فراخوانی میشه, theThing با یه آبجکت پر میشه که شامل یک آرایه بزرگ و یک کلوژر به نام someMethod هست. در عین حال متغیر originalThing توسط یک کلوژری که در unused هست استفاده شده ( که در حقیقت originalThing رفرنسش برمیگرده به فراخوانی قبلی تابع replaceThing به متغیر theThing ). چیزی که باید به یاد داشته باشیم هر چند تا کلوژری که ایجاد شده اسکوپ پرنت خودشون رو با هم در اشتراک دارن. در کد بالا someMethod و unused هر دو کلوژری هستند که اسکوپ replaceThing رو در اختیار دارن. unused اشاره ای به متغیر originalThing داره و هرچند که unused هیچ وقت صدا زده نمیشه اما someMethod میتونه هر جای برنامه از روی آبجکت theThing فراخوانی بشه. و با توجه به این که اسکوپ تابع replaceThing با دو کلوژر someMethod و unused در اشتراک هست صرف اینکه unused فقط اشاره ای به originalThing داره باعث میشه تمام اسکوپ replaceThing برای گاربج کالکتور اکتیو بمونه و هیچوقت کالکت نشه.
تمام این داستان ها باعث یه مموری لیک خفن بشه. اگر این تیکه کد رو اجرا کنید هر چند ثانیه یه پیک استفاده از مموری ایجاد میشه که هروقت هم گاربج کالکتور اجرا بشه این پیک کمتر نمیشه. یه لیستی از کلوژر های بهم متصل شده که ریشه اون ها theThing هست درست کردیم و علاوه بر این اسکوپ هر کلوژر به طور غیر مستقیم اشاره میکنه به یک آرایه خیلی بزرگ.
تیم میتیئور این مموری لیک رو پیدا کردن و در موردش تو این مقاله نوشتن.
- 3: در دسترس نبودن DOM
مواردی هست که دولوپر ها نود های DOM رو توی دیتا استراکچر هایی نگه داری میکنن. فرض کنید شما میخواید سل های یه تیبل رو خیلی سریع به روز کنید و یک رفرنسی از اون هارو داخل یه دیکشنری (ابجکت جاوا اسکریپتی ) یا آرایه نگه داری کنید. این طوری دو تا رفرنس از هر سل ایجاد شده. یکی تو دیتا استراکچر شما و دیگر تو درخت DOM مرورگر. اگر میخواید سلی رو پاک کنید حواستون باشه که هر دو رفرنس رو باید حذف کنید.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
}
باید خیلی مواظب باشید وقتی با برگ های درخت DOM یا همون نود های اچ تی ام ال داخلی کار میکنید. اگر توی کدتون رفرنسی از یه تگ td داخل یه تیبل داشته باشید و بعد از اون کل تیبل رو از DOM حذف کنید و از پس این حذف کردن انتظار داشته باشید که کل اون تیبل از مموری حذف میشه کور خوندید و سناریو ایجاد مموری لیک رو پیش گرفتید. چون تگ های اچ تی ام ال رفرنس به پرنت خودشون دارن و وجود این رفرنس در تگ td که در کد بهش دسترسی داریم باعث میشه هیچ وقت کل تیبل از مموری حذف نشه.
تمام.
مطلبی دیگر از این انتشارات
چگونه در سال ۱۳۹۷ یک برنامهنویس Front End عالی باشیم؟
مطلبی دیگر از این انتشارات
همه چیز درباره آرایه ها در زبان جاوا اسکریپت - قسمت 4
مطلبی دیگر از این انتشارات
فایل JSON و کاربرد آن