حسان امینی لو
حسان امینی لو
خواندن ۱۱ دقیقه·۲ سال پیش

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

یکی از تفاوت هایی که دنیای برنامه نویسی موبایل و گیم با وب داره توی مدیریت حافظه هست. همچنین توی زبون های سطح پایین تر مثل C باید دقیقا بدونیم که مموری چطور تخصیص پیدا میکنه و چطور آزاد میشه. توی دنیای وب و مخصوصا برنامه نویسی سمت کاربر به خاطر ذات زبون جاواسکریپت، برنامه نویس درگیر مدیریت حافظه نمیشه و این هم خوبه، هم خوب نیست. خوبه چون راحته! خوب نیست چون ممکنه گند بزنید.

تو این مطلب، اول توضیح میدم که در JS چطور حافظه تخصیص پیدا میکنه (Memory Allocation) و چطور آزاد میشه. با موجودی به نام Garbage Collector آشنا میشیم و با چند تا از Memory Leakage ها یا نشتی حافظه آشنا میشیم و یاد میگیریم که از این چیزی که اینجا میگم چطور استفاده کنیم که اپلیکیشن های بهتری بنویسیم.


پیش زمینه

تو زبون هایی مثل C اختیار کاملا دست شماست! اینکه چطور از مموری استفاده کنید، کی داخلش چیزی بریزید و کی بخواید از روش چیزی رو پاک کنید. یا حتی از اینم سطح پایین تر، توی زبون هایی مثل اسمبلی شما عملا روی ثبات های پردازنده دارید یه مقدار رو ذخیره میکنید و از یه ثبات به یه ثبات دیگه منتقل میکنید و ... تصور کنید توی این حالت ازتون بخوان یه اپلیکیشن خیلی پیچیده بنویسید. خیلی سخت میشه دیگه

البته قطعا از شما چنین چیزی رو نمیخوان ولی این زبون ها به دلایل خاصی این شکلی طراحی شدن، که به شما امکان این رو بدن که با پرفورمنس خیلی بالاتر بعضی کار ها رو انجام بدید. مثلا مدیریت منابع که توسط سیستم عامل انجام میشه، منابع هم منظورم همون Memory و CPU هست. یا پیاده سازی خیلی از چیز های سطح پایین که شما روزمره توی JS ازشون استفاده می‌کنید!

ولی در مقابل، جاواسکریپت مدیریت حافظه رو خودش انجام میده پس شما درگیر مسائل مدیریت حافظه به طور دستی و Manual نمیشید ولی نباید این باعث بشه که کلا بیخیال موضوع بشیم و همه چیز رو بسپریم به خودش و اگه یاد بگیریم که جاواسکریپت چطور اینکارو انجام میده میتونیم توی بعضی جاها بهتر عمل کنیم.


سایکل

چرخه حافظه، فارغ از اینکه تو چه زبونی دارید کد میزنید از این حالت خارج نیست:

  1. اختصاص حافظه یا Memory Allocation
  2. خوندن و نوشتن توی حافظه Read & Write
  3. آزاد کردن حافظه که دیگه بهشون نیازی نیست یا Release

از این ۳ مرحله که گفتم، مرحله دوم بستگی به زبون برنامه نویسی نداره و بین همه مشترکه ولی قسمت اول و آخر ممکنه تو زبون های مختلف باهم فرق داشته باشه.


تخصیص حافظه توی جاواسکریپت خیلی ساده اتفاق میوفته:

منبع مثال از MDN
منبع مثال از MDN


همینطور که مشخصه یه متغیر با جنس عدد داخل مموری تعریف میشه و یکی دیگه برای یه رشته. مقادیری که Primitive محسوب میشن با مقادیری که جلو تر بهشون اشاره کردم یکم فرق میکنن.

منبع مثال از MDN
منبع مثال از MDN


همینطور که میبینیم، Object ها Array ها و Function ها و حتی EventHanlder ها (که البته همه این ها توی جاواسکریپت همون Object هستند) داخل مموری ذخیره میشن. این دومی ها رو بهشون میگیم Non-Primitive. مدل ذخیره شدن Primitive ها و Non-Primitive ها با هم یکم فرق میکنه. الان دقیق میگم منظورم چیه.


پشته یا (Stack)

ببینید یه stack یا پشته (همون گونی خودمون) وجود داره که وقتی کد داره اجرا میشه از بالا میاد و همه Assignment ها رو داخل مموری میریزه. از اونجا که این یه stack هست، آیتم های اول میرن زیر و هرچی جلو تر میریم بقیه آیتم ها میاد بالای قبلی ها (که البته مدل های مختلفی از این پشته داریم). میشه بهش مثل یه قطار و واگن هاش هم نگاه کرد، از اولین واگن مسافرا (متغیر ها) شروع میکنن به سوار شدن تا برسن اون تَه (حالا البته تَه نداره ولی مثلا دیگه). یه چیزی شبیه این:

Felix Gerschau منبع عکس از بلاگ
Felix Gerschau منبع عکس از بلاگ


خب این stack همون مموری هست. همون RAM، لزوما هم همه این اطلاعات اینطوری شیک و تر و تمیز پشت سر هم نیستن ولی برای شفاف سازی اینطوری نشونش میدیم. وقتی میگیم تخصیص حافظه یا Allocation منظورمون همینه. تمام متغیر ها میرن داخل این stack. ولی آبجکت ها داخل این stack نمیرن، آبجکت ها وارد چیز دیگه ای میشن به نام Heap و  رفرنس شون میره داخل stack. جلو تر میبینیمش.

هر جا که شما اسم متغیر رو استفاده می‌کنید یا یه تابع رو صدا میزنید در واقع از همین stack مقدارشون رو میخونه. بر اساس type متغیر ممکنه توی مموری اندازه مشخصی رو براشون در نظر بگیره. مثلا ممکنه برای رشته ها n بیت رو در نظر بگیره ولی برای boolean مقدار ۱ بیت کفایت میکنه. من دقیق نمیدونم برای هر کدوم از این دیتا تایپ ها چقدر فضا توی مموری در نظر میگیره.


هیپ (Heap)

هیپ هم مثل stack میمونه کاملا ولی یه data structure خاص تری محسوب میشه. اینجا میتونید در موردش بیشتر بخونید. وقتی که توی جاواسکریپت یه آبجکت تعریف می‌کنیم، توی مموری اون آبجکت وارد heap میشه و یه reference ازش (یا آدرسش داخل heap) داخل stack نگهداری میشه:

Felix Gerschau منبع عکس از بلاگ
Felix Gerschau منبع عکس از بلاگ


همینطور که میبینید، مقدار name داخل stack ذخیره شده و آبجکت های dog و person و تابع getOwner داخل heap نگهداری میشن، و آدرس رفرنس شون داخل stack نگهداری میشه.

همینطور هم که از روی این تصویر قشنگ مشخصه، newPerson و person هر دو داخل heap به یک مقدار اشاره دارن پس از تعریف مقادیر تکراری هم جلوگیری میشه.


دیدیم که فرایند تخصیص حافظه در جاواسکریپت به چه شکلی انجام میشه. نکته اینه که ما اینجا فقط یاد گرفتیم که چطور این اتفاق میوفته و وارد جزئیات خیلی ریز اش که دقیق برای هر دیتاتایپ چقدر فضا در نظر گرفته میشه و با چه الگوریتمی این اتفاق میوفته نشدیم. این ها سوالات خیلی خوبی هستند که میشه بهشون فکر کرد و براشون دنبال جواب بود.


آشغال جمع کن یا Garbage Collector

واقعا اسم دیگه ای نتونستم براش پیدا کنم ? اسمش همینه ولی. کارش خیلی جالب تر از اسمش هست.

خب ببینید، بعد اینکه این مقادیر داخل مموری ذخیره میشن و خونده میشن و تغییر میکنن و ... در یک زمانی بالاخره باید از حافظه خالی بشن و چیز هایی که دیگه به درد نمیخوره حذف بشه که فضا برای مقادیر جدید باز باشه. این همونجاییه موضوع چالش بر انگیز میشه. سوال اینه: چه زمانی باید این مقادیر از حافظه حذف بشن؟ بر چه اساسی این اتفاق باید بیوفته؟

دقیقا Garbage Collector برای همین بوجود اومده و کارش همینه. خالی کردن مموری دقیقا باید زمانی اتفاق بیوفته که دیگه به اون نیازی نباشه، اما تصمیم گرفتن این موضوع برای Garbage Collector تقریبا بر اساس احتماله (ببین احتمال که نه، ولی از اونجا که مدل کار کردنش خیلی شاید دقت کافی رو نداشته باشه من بهش میگم احتمال، جلوتر که باهاش بیشتر آشنا شدیم متوجه میشید چرا میگم اینو).

دو تا الگوریتم داره برای آزاد کردن مموری:

  1. الگوریتم Reference Counting
  2. الگوریتم Sweap


الگوریتم Reference Counting

این الگوریتم کار نسبتا ساده ای انجام میده، آبجکت هایی که هیچ رفرنسی براشون وجود نداره رو از توی مموری حذف میکنه.

نکته: برای از بین بردن یه متغیر از مموری مقدارش رو null میکنیم.

این مثال رو در نظر بگیرید:

ایده مثال از بلاگ دوستمون
ایده مثال از بلاگ دوستمون


ببینیم چه اتفاقی میوفته:

  1. آبجکت person تعریف میشه و همونطور که دیدیم داخل heap تعریف میشه
  2. آبجکت newPerson یه رفرنس میزنه به همون آبجکت person
  3. متغیر hobbies تعریف میشه که اشاره میکنه به مقدار hobbies از آبجکت person و اون هم میره داخل heap
  4. بعد person مقدارش null میشه و رفرنسش از بین میره
  5. بعد newPerson هم همین اتفاق براش میوفته
  6. الان دو تا رفرنسی که به person بود ازبین رفته و داخل heap این آبجکت دیگه رفرنسی به جایی نداره بنابراین دیگه نیازی نیست توی heap بمونه و حافظه اش آزاد میشه

امیدوارم ایده رو گرفته باشید.

یه مثال جالب دیگه، یه وقتایی هم هست که آبجکت ها داخل heap به همدیگه رفرنس دارن مثل این:

اینجا son به dad و برعکس دارن رفرنس میزنن
اینجا 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 بشه:

  1. تعریف متغیر های Global
  2. خالی نکردن تایمر ها
  3. پاک نکردن event handler ها
  4. مسأله باز تعریف توابع یکسان



متغیر های Global البته شاید تصادفی

همونطور که میدونید تعریف متغیر های global تو جاواسکریپت به سادگی انجام میشه:

هر دو این ها متغیر های global هستند
هر دو این ها متغیر های global هستند


اگر بعد از تعریف این متغیر ها یه نگاه به window بندازیم (اینجا env مون همون مرورگره و اگر node بود باید داخل global رو نگاه می‌کردیم) میبینیم که هر دو این متغیر ها داخل window تعریف شدن و وجود دارن. البته همیشه به همین سادگی نیست، خیلی اوقات این اتفاق به صورت سهوی میوفته. این مثال رو ببینید:

وقتی داخل بدنه تابع مقدار bar تعریف شده، انگار که مستقیم نوشتیم که بره توی window و این اصلا جالب نیست. برای همین حتما همه جا از let و const برای تعریف متغیر ها استفاده کنید. حتی بهتره که تعریف توابع تون هم تا جای ممکن به شکل function expression باشه:

توی بوجود آوردن توابع هم میشه بهینه تر عمل کرد
توی بوجود آوردن توابع هم میشه بهینه تر عمل کرد


خالی نکردن تایمر ها و interval ها

شما شیر آب رو باز می‌کنید حتما باید ببندینش دیگه! این از اون مشکلاته که حتما حتما باید خیلی روش دقت کنید. وقتی که شما یه تایمر ست می‌کنید باید حتما وقتی کارتون باهاش تموم میشه پاکش کنید:

مدل غلط
مدل غلط


حتما تایمر رو توی یک متغیر ذخیره کنید که رفرنسش رو داخل stack داشته باشید و بعد از اینکه کار این تابع تموم شد به کمک clearTimeout یا clearInterval حذفش کنید:

این موضوع برای setTimeout هم صادقه. هر دو این بزرگواران از چیزهایی هستند که قطعا ممکنه یکی دوبار توی کار بهش برخورد کنید.

یه مثال هم از دنیای React ببینیم خالی از لطف نیست. احتمالا با useEffect آشنایی دارید، میتونید فرایند حذف تایمر ها رو توی useEffect این شکلی هندل کنید:

همینطور که میبینید جایی که کامپوننت unmount میشه تایمر رو خالی کردیم
همینطور که میبینید جایی که کامپوننت 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 بشید و اونجا روند اجرای اپلیکیشن رو مانیتور کنید:

اینجا به شما قابلیت های زیادی میده که واقعا از حوصله این مطلب خارجه!‌ برای دستگرمی شروع کنید همون مثال های بالا رو نوشتن و اجرا کردن و ببینید اینجا چه اتفاق هایی میوفته. توی یوتوب هم پره از ویدئو هایی که روش استفاده ازش رو توضیح میدن.



اگه این مطلب تو از اول تا آخر خوندید واقعا دمتون گرم!‌ میدونم که موضوع، موضوع پیش پا افتاده ای نیست، خوندنش حوصله میخواد ولی به نظرم یاد گرفتنش خیلی خیلی ارزشمنده.

میتونید اگه دوست داشتید باهام در ارتباط باشید از طریق لینکدین اینکارو انجام بدید.



موفق باشید رفقا.

برنامه نویسیجاواسکریپتmemory leakreact
برنامه نویس از جلو
شاید از این پست‌ها خوشتان بیاید