حافظه در جاواسکریپت؛ جلوی Memory Leak رو بگیریم

یکی از تفاوت هایی که دنیای برنامه نویسی موبایل و گیم با وب داره توی مدیریت حافظه هست. همچنین توی زبون های سطح پایین تر مثل C باید دقیقا بدونیم که مموری چطور تخصیص پیدا میکنه و چطور آزاد میشه. توی دنیای وب و مخصوصا برنامه نویسی سمت کاربر به خاطر ذات زبون جاواسکریپت، برنامه نویس درگیر مدیریت حافظه نمیشه و این هم خوبه، هم خوب نیست. خوبه چون راحته! خوب نیست چون ممکنه گند بزنید.تو این مطلب، اول توضیح میدم که در JS چطور حافظه تخصیص پیدا میکنه (Memory Allocation) و چطور آزاد میشه. با موجودی به نام Garbage Collector آشنا میشیم و با چند تا از Memory Leakage ها یا نشتی حافظه آشنا میشیم و یاد میگیریم که از این چیزی که اینجا میگم چطور استفاده کنیم که اپلیکیشن های بهتری بنویسیم.پیش زمینهتو زبون هایی مثل C اختیار کاملا دست شماست! اینکه چطور از مموری استفاده کنید، کی داخلش چیزی بریزید و کی بخواید از روش چیزی رو پاک کنید. یا حتی از اینم سطح پایین تر، توی زبون هایی مثل اسمبلی شما عملا روی ثبات های پردازنده دارید یه مقدار رو ذخیره میکنید و از یه ثبات به یه ثبات دیگه منتقل میکنید و ... تصور کنید توی این حالت ازتون بخوان یه اپلیکیشن خیلی پیچیده بنویسید. خیلی سخت میشه دیگهالبته قطعا از شما چنین چیزی رو نمیخوان ولی این زبون ها به دلایل خاصی این شکلی طراحی شدن، که به شما امکان این رو بدن که با پرفورمنس خیلی بالاتر بعضی کار ها رو انجام بدید. مثلا مدیریت منابع که توسط سیستم عامل انجام میشه، منابع هم منظورم همون Memory و CPU هست. یا پیاده سازی خیلی از چیز های سطح پایین که شما روزمره توی JS ازشون استفاده می‌کنید!ولی در مقابل، جاواسکریپت مدیریت حافظه رو خودش انجام میده پس شما درگیر مسائل مدیریت حافظه به طور دستی و Manual نمیشید ولی نباید این باعث بشه که کلا بیخیال موضوع بشیم و همه چیز رو بسپریم به خودش و اگه یاد بگیریم که جاواسکریپت چطور اینکارو انجام میده میتونیم توی بعضی جاها بهتر عمل کنیم.سایکلچرخه حافظه، فارغ از اینکه تو چه زبونی دارید کد میزنید از این حالت خارج نیست:اختصاص حافظه یا Memory Allocationخوندن و نوشتن توی حافظه Read & Writeآزاد کردن حافظه که دیگه بهشون نیازی نیست یا Releaseاز این ۳ مرحله که گفتم، مرحله دوم بستگی به زبون برنامه نویسی نداره و بین همه مشترکه ولی قسمت اول و آخر ممکنه تو زبون های مختلف باهم فرق داشته باشه.تخصیص حافظه توی جاواسکریپت خیلی ساده اتفاق میوفته:منبع مثال از MDNهمینطور که مشخصه یه متغیر با جنس عدد داخل مموری تعریف میشه و یکی دیگه برای یه رشته. مقادیری که Primitive محسوب میشن با مقادیری که جلو تر بهشون اشاره کردم یکم فرق میکنن.منبع مثال از MDNهمینطور که میبینیم، Object ها Array ها و Function ها و حتی EventHanlder ها (که البته همه این ها توی جاواسکریپت همون Object هستند) داخل مموری ذخیره میشن. این دومی ها رو بهشون میگیم Non-Primitive. مدل ذخیره شدن Primitive ها و Non-Primitive ها با هم یکم فرق میکنه. الان دقیق میگم منظورم چیه.پشته یا (Stack)ببینید یه stack یا پشته (همون گونی خودمون) وجود داره که وقتی کد داره اجرا میشه از بالا میاد و همه Assignment ها رو داخل مموری میریزه. از اونجا که این یه stack هست، آیتم های اول میرن زیر و هرچی جلو تر میریم بقیه آیتم ها میاد بالای قبلی ها (که البته مدل های مختلفی از این پشته داریم). میشه بهش مثل یه قطار و واگن هاش هم نگاه کرد، از اولین واگن مسافرا (متغیر ها) شروع میکنن به سوار شدن تا برسن اون تَه (حالا البته تَه نداره ولی مثلا دیگه). یه چیزی شبیه این:Felix Gerschau منبع عکس از بلاگخب این stack همون مموری هست. همون RAM، لزوما هم همه این اطلاعات اینطوری شیک و تر و تمیز پشت سر هم نیستن ولی برای شفاف سازی اینطوری نشونش میدیم. وقتی میگیم تخصیص حافظه یا Allocation منظورمون همینه. تمام متغیر ها میرن داخل این stack. ولی آبجکت ها داخل این stack نمیرن، آبجکت ها وارد چیز دیگه ای میشن به نام Heap و رفرنس شون میره داخل stack. جلو تر میبینیمش.هر جا که شما اسم متغیر رو استفاده می‌کنید یا یه تابع رو صدا میزنید در واقع از همین stack مقدارشون رو میخونه. بر اساس type متغیر ممکنه توی مموری اندازه مشخصی رو براشون در نظر بگیره. مثلا ممکنه برای رشته ها n بیت رو در نظر بگیره ولی برای boolean مقدار ۱ بیت کفایت میکنه. من دقیق نمیدونم برای هر کدوم از این دیتا تایپ ها چقدر فضا توی مموری در نظر میگیره.هیپ (Heap)هیپ هم مثل stack میمونه کاملا ولی یه data structure خاص تری محسوب میشه. اینجا میتونید در موردش بیشتر بخونید. وقتی که توی جاواسکریپت یه آبجکت تعریف می‌کنیم، توی مموری اون آبجکت وارد heap میشه و یه reference ازش (یا آدرسش داخل heap) داخل stack نگهداری میشه:Felix Gerschau منبع عکس از بلاگهمینطور که میبینید، مقدار name داخل stack ذخیره شده و آبجکت های dog و person و تابع getOwner داخل heap نگهداری میشن، و آدرس رفرنس شون داخل stack نگهداری میشه.همینطور هم که از روی این تصویر قشنگ مشخصه، newPerson و person هر دو داخل heap به یک مقدار اشاره دارن پس از تعریف مقادیر تکراری هم جلوگیری میشه.دیدیم که فرایند تخصیص حافظه در جاواسکریپت به چه شکلی انجام میشه. نکته اینه که ما اینجا فقط یاد گرفتیم که چطور این اتفاق میوفته و وارد جزئیات خیلی ریز اش که دقیق برای هر دیتاتایپ چقدر فضا در نظر گرفته میشه و با چه الگوریتمی این اتفاق میوفته نشدیم. این ها سوالات خیلی خوبی هستند که میشه بهشون فکر کرد و براشون دنبال جواب بود.آشغال جمع کن یا Garbage Collectorواقعا اسم دیگه ای نتونستم براش پیدا کنم ? اسمش همینه ولی. کارش خیلی جالب تر از اسمش هست.خب ببینید، بعد اینکه این مقادیر داخل مموری ذخیره میشن و خونده میشن و تغییر میکنن و ... در یک زمانی بالاخره باید از حافظه خالی بشن و چیز هایی که دیگه به درد نمیخوره حذف بشه که فضا برای مقادیر جدید باز باشه. این همونجاییه موضوع چالش بر انگیز میشه. سوال اینه: چه زمانی باید این مقادیر از حافظه حذف بشن؟ بر چه اساسی این اتفاق باید بیوفته؟دقیقا Garbage Collector برای همین بوجود اومده و کارش همینه. خالی کردن مموری دقیقا باید زمانی اتفاق بیوفته که دیگه به اون نیازی نباشه، اما تصمیم گرفتن این موضوع برای Garbage Collector تقریبا بر اساس احتماله (ببین احتمال که نه، ولی از اونجا که مدل کار کردنش خیلی شاید دقت کافی رو نداشته باشه من بهش میگم احتمال، جلوتر که باهاش بیشتر آشنا شدیم متوجه میشید چرا میگم اینو).دو تا الگوریتم داره برای آزاد کردن مموری:الگوریتم Reference Countingالگوریتم Sweapالگوریتم Reference Countingاین الگوریتم کار نسبتا ساده ای انجام میده، آبجکت هایی که هیچ رفرنسی براشون وجود نداره رو از توی مموری حذف میکنه.نکته: برای از بین بردن یه متغیر از مموری مقدارش رو null میکنیم.این مثال رو در نظر بگیرید:ایده مثال از بلاگ دوستمونببینیم چه اتفاقی میوفته:آبجکت person تعریف میشه و همونطور که دیدیم داخل heap تعریف میشه آبجکت newPerson یه رفرنس میزنه به همون آبجکت personمتغیر hobbies تعریف میشه که اشاره میکنه به مقدار hobbies از آبجکت person و اون هم میره داخل heapبعد person مقدارش null میشه و رفرنسش از بین میرهبعد newPerson هم همین اتفاق براش میوفتهالان دو تا رفرنسی که به person بود ازبین رفته و داخل heap این آبجکت دیگه رفرنسی به جایی نداره بنابراین دیگه نیازی نیست توی heap بمونه و حافظه اش آزاد میشهامیدوارم ایده رو گرفته باشید.یه مثال جالب دیگه، یه وقتایی هم هست که آبجکت ها داخل heap به همدیگه رفرنس دارن مثل این:اینجا son به dad و برعکس دارن رفرنس میزنندر این حالت داخل heap چنین چیزی رو خواهیم داشت:اینجا وقتی هر دو آبجکت رو null کردیم انتظار داریم که از مموری هم حذف بشن ولی الگوریتم Reference counting نمیتونه تشخیص بده که باید اون ها رو از heap حذف کنه. اینجاست که نیاز پیدا میشه به الگوریتم بعدی یعنی Sweapالگوریتم Sweapمیدونین که در جاواسرکیپت همیشه یه آبجکت گلوبال وجود داره و این آبجکت گلوبال بر اساس enviroment ممکنه متفاوت باشه، مثلا توی browser گلوبال میشه window یا توی node میشه global.این الگوریتم بر خلاف الگوریتم قبلی که تعداد رفرنس ها رو بشمره (که قبلا دیدیم مشکلش چی بود) میاد و نگاه میکنه که آیا این آبجکت ها که توی مموری هستن از طریق این global آبجکت در دسترس هستن یا نه. و اگر نبودن مموری رو آزاد میکنه.باز هم مثال از بلاگ دوستمونهمینطور که توی تصویر هم مشخصه آبجکت هایی که به خودشون reference زدن یا به هم دیگه رفرنس دارن ولی داخل stack نیستند توسط Garbage Collector علامت گذاری میشن و حذف میشن.این کار طی سایکل هایی انجام میشه، ولی مشخص نیست دقیقا این سایکل ها طبق چه روتینی اجرا میشن.خب به کمک این الگوریتم مشکلی که قبل تر اشاره شد تا حدود زیادی حل میشه، ولی همچنان جای خالی قابلیتی که به شما اجازه بده به صورت دستی مموری رو خالی کنید حس میشه.نشت مموری (Memory Leakage)تا الان دیدیم که مموری چطور Allocate میشه و چطور و با چه فرایندی آزاد میشه. اما بر اساس چیز هایی که تا الان یاد گرفتیم چطور میتونیم کارمون رو بهبود بدیم؟درسته که خیلی از فرآیند ها توسط جاواسکریپت به صورت خودکار انجام میشه ولی همچنان ممکنه بعضی اشتباهات از جانب برنامه نویس سر بزنه که کاری از خود JS هم بر نیاد. در ادامه چندتا از این اشتباهات رو میگم و چند نمونه میبینیم که حواسمون جمع باشه.در حالت کلی چند حالت ممکنه پیش بیاد که اپلیکیشن ما دچار Memory Leak بشه:تعریف متغیر های Globalخالی نکردن تایمر هاپاک نکردن event handler هامسأله باز تعریف توابع یکسانمتغیر های Global البته شاید تصادفیهمونطور که میدونید تعریف متغیر های global تو جاواسکریپت به سادگی انجام میشه:هر دو این ها متغیر های global هستنداگر بعد از تعریف این متغیر ها یه نگاه به window بندازیم (اینجا env مون همون مرورگره و اگر node بود باید داخل global رو نگاه می‌کردیم) میبینیم که هر دو این متغیر ها داخل window تعریف شدن و وجود دارن. البته همیشه به همین سادگی نیست، خیلی اوقات این اتفاق به صورت سهوی میوفته. این مثال رو ببینید:وقتی داخل بدنه تابع مقدار bar تعریف شده، انگار که مستقیم نوشتیم که بره توی window و این اصلا جالب نیست. برای همین حتما همه جا از let و const برای تعریف متغیر ها استفاده کنید. حتی بهتره که تعریف توابع تون هم تا جای ممکن به شکل function expression باشه:توی بوجود آوردن توابع هم میشه بهینه تر عمل کردخالی نکردن تایمر ها و interval هاشما شیر آب رو باز می‌کنید حتما باید ببندینش دیگه! این از اون مشکلاته که حتما حتما باید خیلی روش دقت کنید. وقتی که شما یه تایمر ست می‌کنید باید حتما وقتی کارتون باهاش تموم میشه پاکش کنید:مدل غلطحتما تایمر رو توی یک متغیر ذخیره کنید که رفرنسش رو داخل stack داشته باشید و بعد از اینکه کار این تابع تموم شد به کمک clearTimeout یا clearInterval حذفش کنید:این موضوع برای setTimeout هم صادقه. هر دو این بزرگواران از چیزهایی هستند که قطعا ممکنه یکی دوبار توی کار بهش برخورد کنید.یه مثال هم از دنیای React ببینیم خالی از لطف نیست. احتمالا با useEffect آشنایی دارید، میتونید فرایند حذف تایمر ها رو توی useEffect این شکلی هندل کنید:همینطور که میبینید جایی که کامپوننت unmount میشه تایمر رو خالی کردیمپاک نکردن Event Handler هامثل مثال قبلی ولی اینبار برای event ها. هر بار که یک eventListener جدید تعریف می‌کنید بهتره یه جا که دیگه بهش نیاز نیست اون eventHandler رو unsubscibe کنید. حتی اگه امکانش براتون وجود داشته باشه که کلا اون المان رو از روی صفحه حذف کنید هم که چه بهتر:خط اول المان button رو از روی صفحه خوندیم، یه تابع تعریف کردیم که وقتی روش کلیک شد صدا زده بشه، بعد اینکه کارمون تموم شد به کمک removeEventListener اونو unsubscribe کردیم. در نهایت هم اون المان رو به کل از صفحه حذف کردیم.این اتفاق البته داخل React به صورت خودکار انجام میشه و نیاز نیست نگرانش باشید.باز تعریف توابع یکساناین مثالش رو توی React خیلی زیاد میبینیم. البته در حالت عادی هم (خارج از React) بعید نیست به این مشکل بخوریم ولی توی React این موضوع خیلی پر رنگ میشه. البته بیشتر از اینکه یه مشکل مربوط به مموری باشه مربوط به performance میشه ولی اشاره بهش خالی از لطف نیست.این مطلب به اندازه کافی طولانی شده، برای توضیح و فهم بهتر این موضوع آخر قبلا یه مطلب نوشتم که پیشنهاد میکنم اونو بخونید، اگه براتون سوالی پیش اومد کامنت بذارید. https://virgool.io/@hesanam/%D8%AE%D9%84%D8%A7%D8%B5%D9%87-%D9%81%DB%8C%DA%86%D8%B1-%D8%AC%D8%AF%DB%8C%D8%AF-%D8%B1%DB%8C-%D8%A7%DA%A9%D8%AA-useevent-ifegiziyjbuq معرفی ابزار های کروم برای debugخیلی جالبه که گوگل کروم این قابلیت رو به شما میده که بتونید مموری رو توی کدی نوشتید (یا اپلیکیشن تون) ببینید، اسنپشات بگیرید و دقیقا متوجه بشید که چه جاهایی داره به صورت غیر طبیعی از منابع تون استفاده میشه و جلوش رو بگیرید.برای اینکار dev tools رو به کمک دکمه F12 یا کلیک راست و گزینه inspect میتونید باز کنید و وارد تب memory بشید و اونجا روند اجرای اپلیکیشن رو مانیتور کنید:اینجا به شما قابلیت های زیادی میده که واقعا از حوصله این مطلب خارجه!‌ برای دستگرمی شروع کنید همون مثال های بالا رو نوشتن و اجرا کردن و ببینید اینجا چه اتفاق هایی میوفته. توی یوتوب هم پره از ویدئو هایی که روش استفاده ازش رو توضیح میدن.اگه این مطلب تو از اول تا آخر خوندید واقعا دمتون گرم!‌ میدونم که موضوع، موضوع پیش پا افتاده ای نیست، خوندنش حوصله میخواد ولی به نظرم یاد گرفتنش خیلی خیلی ارزشمنده.میتونید اگه دوست داشتید باهام در ارتباط باشید از طریق لینکدین اینکارو انجام بدید.موفق باشید رفقا.