دیباگ نشت حافظه در جاوا اسکریپت — راهنمای کاربردی
اگر تاکنون فرصت نوشتن یک اپلیکیشن تکصفحهای یا دیگر انواع وباپلیکیشنهای جدی را داشته باشید، بیشک دستکم یک بار از خود پرسیدهاید آیا در اپلیکیشنتان نشت حافظه در جاوا اسکریپت رخ میدهد؟ در اغلب مواقع با اطمینان میتوان گفت که بله این اتفاق میافتد. ما در موارد زیادی فراموش میکنیم که یک اشتراک رویداد را حذف کرده و اشیای باقیمانده را خاتمه ببخشیم. در اغلب موارد، این اشتباههای کوچک مشکل بزرگتری ایجاد نمیکنند، چون چند کیلوبایت کمتر یا بیشتر استفاده از RAM این روزها چندان به چشم نمیآید.
اما باید توجه داشته باشید که همین چند کیلوبایت ناچیز اینجا و آنجا وقتی روی هم جمع میشوند، میتوانند به چند مگابایت یا شاید چند صد مگابایت نشت حافظه در اپلیکیشن منجر شوند. چاره کار در دیباگ نشت حافظه است، اما قبل از آن که شروع به اقدام عملی در این مسیر بکنیم، ابتدا باید با مفهوم نشت حافظه آشنا شویم.
طرز کار حافظه به طور کلی چگونه است؟
در زمان برنامهنویسی وقتی یک حافظه را برای ذخیرهسازی دادهها در هیپ اختصاص میدهید، آن حافظه در انتهای استفاده باید آزاد شود تا سیستم عامل بتواند آن را برای کار دیگری مورد استفاده قرار دهد. زمانی که حافظه در حالت تخصیص قرار دارد، برای اپلیکیشنی که به آن اختصاص یافته است رزرو میشود.
در زبانهایی مانند جاوا اسکریپت که از مفهوم garbage collection استفاده میکنند، تخصیص حافظه و آزادسازی آن عموماً در پشت صحنه انجام میگیرد. هر زمان که شیء جدیدی ایجاد میشود حافظه تخصیص مییابد و زمانی که garbage collector تشخیص دهد هم ارجاعها به آن شیء پاک شدهاند، حافظه مربوطه را آزاد میکند. یک ارجاع شیء متغیری (از نوع اشارهگر) است که امکان دسترسی به شیء را فراهم میسازد.
طرز کار حافظه در جاوا اسکریپت چگونه است؟
حافظه در جاوا اسکریپت به صورت شبکهای از اشیا مدیریت میشود که در آن هر گره یک شیء است. با این حال این اشیا صرفاً اشیای جاوا اسکریپت نیستند، بلکه اشیای ایجاد شده از سوی جاوا اسکریپت در زمان اجرا را نیز شامل میشوند که شامل دامنههایی هستند.
برای این که garbage collector یک شیء را جمعآوری کند، باید هیچ مسیری از یک دامنه فعال به آن شیء وجود نداشته باشد. این وضعیت از طریق null کردن ارجاعها (انتساب مقدار دیگری به آن ارجاع) یا در مورد متغیرهای محلی زمانی که از دامنه آن خارج میشویم، اتفاق میافتد.
نشتِ حافظه زمانی رخ میدهد که یک شیء که گمان میرود از سوی garbage collector جمعآوری شده است، در عمل جمع نشده است، زیرا هنوز دستکم یک مسیر در شبکه به آن وجود دارد که امکان دستیابی به شیء را فراهم میسازد و ممکن است شما از آن اطلاع نداشته باشید.
توجه کنید که تفاوت بین نشت حافظه و رفتار نرمال در نیت است. همه الگوهای نشت حافظه تنها زمانی رخ میدهند که نمیخواهید یک شیء در حافظه باقی بماند.
مثالهایی از نشت حافظه
در این بخش برخی وضعیتهای نمونه را که ممکن است در آنها نشت حافظه رخ بدهد، بررسی میکنیم.
متغیرهای سراسری
از آنجا که بنای تصمیمگیری garbage collector برای جمعآوری یک شیء امکان دستیابی یا عدم امکان دسترسی به آن شیء است، اشیای عمومی بر اساس تعریف خود هرگز جمعآوری نمیشوند. این وضعیت اختصاصی به پنجره ندارد و همه متغیرهایی که از سوی ماژولهای جاوا اسکریپت اکسپورت میشوند نیز همین حالت را دارند. در ادامه مثالی از ماژول جاوا اسکریپت را مشاهده میکنید:
1class SomeClass { … }
2export const permanentInstance = new SomeClass();
وهله دائمی هرگز جمعآوری نمیشود، زیرا شیء سراسری نیست و این از آن واقعیت ناشی میشود که میتواند هر جایی ایمپورت شود. لازم به ذکر است که در اغلب موارد هنگامی که نشت حافظه مشکل ایجاد میکند، ناشی از وهله سراسری نیست، بلکه از عواقب موارد جانبی وهلههای سراسری ناشی میشود، چون هر چیزی که از یک وهله سراسری ارجاع یافته باشد، هرگز جمعآوری نخواهد شد.
الگوی Observer (رویدادها)
الگوی رویدادها در جاوا اسکریپت بسیار رایج، اما مستعد نشت حافظه است. هر اشتراک رویداد (event subscription) یک نشت حافظه بالقوه است. به همین دلیل است که هر API اشتراک رویداد دارای یک تابع لغو اشتراک است، زیرا وقتی که مشترک میشوید، یک callback از سوی ارجاع به منبع رویداد ارسال میشود و آنجا ذخیره میشود تا زمانی که لغو اشتراک فراخوانی شود و یا این که منبع رویداد خودش جمعآوری شود.
معنی گفته فوق آن است که هر زمان در رویدادی مشترک شوید اگر لغو اشتراک متناظری وجود نداشته باشد، کلوژر callback تا زمانی که منبع رویداد وجود داشته باشد، همچنان به حضور خود ادامه میدهد. این کلوژر همه متغیرهایی که تابع callback به صورت درونی استفاده میکند و خارج از تابع میآید را نیز نشت میدهد. به مثال زیر توجه کنید:
1const someData = “hello world”;
2someButton = document.getElementById(“button”)
3someButton.addEventListener(‘click’, () => console.log(someData))
تا زمانی که دکمه در صفحه وجود داشته باشد و شنونده رویداد حذف نشده باشد، رشته hello world در حافظه باقی میماند، زیرا کلوژر تولید شده از سوی تابع arrow به رویداد کلیک ارسال شده است. اینک برایتان روشن شده است که دلیل این وضعیت آن است که someData قابل دسترسی است و دلیلی برای جمعآوری آن وجود ندارد، زیرا بدیهی است که مورد نیاز است.
نشت حافظه را چگونه دیباگ کنیم؟
دیباگ کردن موارد نشتهای حافظه بسیار دشوار است، چون در اغلب موارد حتی نمیدانیم باید به دنبال چه چیزی بگردیم و برخی اوقات، موارد نشت حافظه به تصمیمهای استاتیک وابستهاند که بسیار دشوار هستند زیرا بهترین کمک در مسیر دیباگ کردن نشت حافظه، تغییرهای رخ داده در حافظه در طی زمان محسوب میشوند.
یافتن نشت
گاهی اوقات با کلیک کردن در بخشهای مختلف وب اپلیکیشن و د برخی موارد حتی با منتظر ماندن میتوان وضعیت حافظه را در بازههای منظم بررسی کرد و روند آن را مورد مشاهده قرار داد. در همه اپلیکیشنهایی که مقدار زیادی شیء ایجاد میکنند، مشاهده میشود که مصرف حافظه در طی زمان افزایش مییابد. این وضعیت لزوماً نشت محسوب نمیشود. Garbage collection از نظر CPU پرهزینه است و از این رو garbage collector همواره همه حافظه را جاروب نمیکند. با مراجعه به زبانه performance در بخش inspector مرورگر کروم میتوانید شروع به رصد حافظه کرده و پس از انجام برخی اقدامات، ضبط را متوقف کنید.
تصویر فوق مانند یک گراف معمولی به نظر میرسد، مصرف حافظه بالا میرود، garbage collector حافظه را جاروب میکند و مصرف حافظه تقریباً تا ابتدای شروع ضبط پایین میآید. مشکل این جا است که مصرف حافظه پس از عمل garbage collector به همان سطح قبلی بازنمیگردد و با تداوم اجرای اعمال مختلف روی صفحه، رفتهرفته افزایش مییابد.
خواندن snapshot حافظه
این بخش از مقاله اختصاص به مرورگر کروم دارد. snapshot-های حافظه در دیباگرهای دیگر ممکن است مختصات متفاوتی داشته باشند. در زبانه حافظه مرورگر کروم گزینهای برای گرفتن یک snapshot از حافظه دارید. این گزینه یک گزارش JSON بزرگ از کل شبکه حافظه ایجاد کرده و آن را به صورت لیستی از گرهها نمایش میدهد.
این لیست بر اساس نوع گرهها، گروهبندی شده است. هنگامی که آن را باز کنید، میبینید که هر گره در شبکه حافظه دارای نوع مفروضی است. «مسافت» (Distance) به معنی این است که برای رسیدن به دامنه فعال یک شیء در شبکه حافظه باید از روی چه تعداد گره عبور کنید. اندازه سطح، مقدار حافظهای را که شیء خودش استفاده میکند، مقدار حافظهای که یک شیء پس از اجرای garbage collecton نگه میدارد را نمایش میدهد.
برخی گرهها را خواهید دید که به سازوکار درونی موتور جاوا اسکریپت مرتبط هستند، مانند کد کامپایل شده، سیستم و غیره. لازم نیست در مورد این موارد نگران باشید، زیرا فرض ما بر این است که نشتهای بالقوه از کد ناشی میشوند و ربطی به مرورگر ندارند.
بررسی گرهها
زمانی که روی یک گره کلیک میکنید، لیستی از retainers میبینید که یک مسیر در شبکه نمایش میدهند که شیء را از جمعآوری حفظ میکند. اما این وضعیت مشکلزا است، زیرا اگر یک ساختار داده شدیداً به هم متصل، مثلاً یک درخت با ارجاعهای والد داشته باشید که مسیرهای زیادی دارد، کروم تصمیم میگیرد تنها یک مسیر را به شما نمایش دهد و ممکن است مسیری را نمایش دهد که یک رویه بازگشت نامتناهی ارجاع از والد به فرزند و از فرزند به والد دارد و هرگز در عمل ریشهای که در شیء میماند را نمایش ندهد.
بنابراین وقتی که یک شیء مشکوک را میبینیم، هدفمان این است که ببینیم چه چیزی باقی میماند و بر اساس آن تصمیمگیری کنیم که آیا ارجاعی که وجود دارد مورد نظر ما است یا نه. برای کمک به این فرایند میتوانید ماوس را روی گره برده و روی آن کلیک کنید و عبارت $0 را در کنسول وارد کنید تا ارجاعی به آن گره در حافظه به دست آورید. این وضعیت به شناسایی شیئی که موجب باقی ماندن شیء مورد نظر میشود کمک میکند.
دیباگ کردن نشتهای حافظه
ما معمولاً نمیخواهیم همه موارد نشت حافظه را پیدا کنیم، چون اغلب آنها مقدار کوچکی دارند. در زمان بهینهسازی اولویت ما ابتدا با موارد حادتر است و این کار را تا زمانی ادامه میدهیم که از نتیجه کار یعنی وضعیت مصرف حافظه اپلیکیشن راضی شویم.
اگر سرنخی داشته باشیم که کدام اشیا ممکن است نشت یابند و اگر آن اشیا وهلههای کلاسی باشند، شانس با ما یار بوده است. به زبانه حافظه ابزار inspector کروم بروید و روی heap snapshot کلیک کنید و زمانی که کار آن به پایان رسید، نام کلاس را در نوار جستجو وارد کنید. اگر مورد مطابقی یافتید، میتوانید وضعیت آن را در حافظه مورد بررسی قرار دهید. برای نمونه اگر 10 وهله از Widget داشته باشید، اما تنها انتظار 2 ویجت را داشته باشید، در این صورت یک متهم بالقوه را شناسایی کردهاید.
اگر نمیدانید باید به دنبال چه چیزی بگردید، یک راهبرد این است که به وهلههای کلاسی عمده در اپلیکیشن خود فکر کنید که در طی زمان ایجاد و تخریب میشوند. این موارد میتواند ویجت، ادیتور، کامپوننتهای UI و مواردی از این دست باشند. سپس وارد حالتی شوید که آن اشیا به طور مکرر ایجاد و تخریب میشوند و سپس با گرفتن یک اسنپشات از هیپ و مقایسه تعداد وهلههای موجود در حافظه میتوانید وضعیت نشت حافظه را مورد بررسی قرار دهید.
تکنیک 3 اسنپشات
اگر از راهبرد فوق هم نتیجه خوبی به دست نیاوردید، راهبرد دیگر که شامل وهلههای کلاسی نیست تکنیک 3 اسنپشات است:
- صفحه را در حالت مطلوب قرار دهید و با زدن دکمه F5 یک حالت تمیز به دست آورید. زمانی که صفحه بارگذاری شد، اسنپشات هیپ بگیرید.
- برخی اقدامات (به جز رفرش) انجام دهید و صفحه را دوباره به همان حالت بازگردانید که در انتهای گام قبلی قرار داشت و یک اسنپشات دیگر بگیرید.
- حالت صفحه را تا بیشترین حد ممکن تغییر دهید و مطمئن شوید که هیچ رفرشی رخ نمیدهد، زیرا رفرش باعث پاک شدن کامل حافظه میشود و همه زحمتهای ما را به هدر میدهد. زمانی که صفحه کاملاً در حالت متفاوتی قرار گرفت، اسنپشات سوم را هم از هیپ را بگیرید.
اینک میتوانید روی اسنپشات سوم کلیک کنید و در کنار گزینه class filter روی Objects تخصیصیافته بین اسنپشات اول و دوم را ببینید.