مقدمه
در حالی که جاوا اسکریپت از جمع آوری زباله برای مدیریت خودکار حافظه استفاده می کند، جایگزینی برای مدیریت موثر حافظه در برنامه ها نیست. برنامههای جاوا اسکریپت از همان مشکلات مربوط به حافظه رنج میبرند که برنامههای بومی مانند نشت حافظه و نفخ میکنند، اما باید با مکثهای جمعآوری زباله نیز مقابله کنند. برنامه های کاربردی در مقیاس بزرگ مانند Gmail با مشکلات مشابهی روبرو می شوند که برنامه های کوچکتر شما با آن روبرو هستند. برای آشنایی با نحوه استفاده تیم Gmail از Chrome DevTools برای شناسایی، جداسازی و رفع مشکلات حافظه خود، ادامه دهید.
جلسه Google I/O 2013
ما این مطالب را در Google I/O 2013 ارائه کردیم. ویدیوی زیر را ببینید:
جیمیل ما مشکل داریم…
تیم جیمیل با مشکل جدی مواجه بود. حکایتهایی از برگههای جیمیل که چندین گیگابایت حافظه مصرف میکنند در لپتاپها و رایانههای رومیزی با محدودیت منابع، به طور فزایندهای شنیده میشدند، اغلب با نتیجهگیری که کل مرورگر را از کار میاندازد. داستانهایی از سیپییوهایی که به 100 درصد پین شدهاند، برنامههایی که پاسخگو نیستند، و تبهای غمگین Chrome («او مرده، جیم»). تیم در مورد چگونگی شروع به تشخیص مشکل، چه رسد به رفع آن، دچار مشکل شد. آنها نمیدانستند که مشکل چقدر گسترده است و ابزارهای موجود به برنامههای بزرگ نمیرسند. این تیم با تیمهای کروم متحد شدند و با هم تکنیکهای جدیدی را برای تریاژ مشکلات حافظه، بهبود ابزارهای موجود و امکان جمعآوری دادههای حافظه از میدان ایجاد کردند. اما، قبل از پرداختن به ابزارها، اجازه دهید اصول مدیریت حافظه جاوا اسکریپت را پوشش دهیم.
مبانی مدیریت حافظه
قبل از اینکه بتوانید به طور موثر حافظه را در جاوا اسکریپت مدیریت کنید، باید اصول اولیه را بدانید. این بخش انواع اولیه، نمودار شیء را پوشش می دهد و تعاریفی را برای bloat حافظه به طور کلی و نشت حافظه در جاوا اسکریپت ارائه می دهد. حافظه در جاوا اسکریپت را می توان به عنوان یک گراف در نظر گرفت و به همین دلیل نظریه گراف نقشی در مدیریت حافظه جاوا اسکریپت و Heap Profiler ایفا می کند.
انواع اولیه
جاوا اسکریپت سه نوع اولیه دارد:
- شماره (به عنوان مثال 4، 3.14159)
- بولی (درست یا نادرست)
- رشته ("سلام جهان")
این انواع اولیه نمی توانند به هیچ مقدار دیگری ارجاع دهند. در نمودار شی، این مقادیر همیشه گره های برگ یا پایانی هستند، به این معنی که هرگز یک یال خروجی ندارند.
فقط یک نوع ظرف وجود دارد: شی. در جاوا اسکریپت Object یک آرایه انجمنی است. یک شی غیر خالی یک گره داخلی با لبه های خروجی به مقادیر دیگر (گره ها) است.
آرایه ها چطور؟
آرایه در جاوا اسکریپت در واقع یک شی است که دارای کلیدهای عددی است. این یک سادهسازی است، زیرا زمانهای اجرا جاوا اسکریپت اشیاء آرایهمانند را بهینهسازی میکند و آنها را در زیر هود بهعنوان آرایه نشان میدهد.
اصطلاحات
- مقدار - نمونه ای از نوع اولیه، شی، آرایه و غیره.
- متغیر - نامی که به یک مقدار ارجاع می دهد.
- Property - نامی در یک شی که به یک مقدار ارجاع می دهد.
نمودار شی
تمام مقادیر در جاوا اسکریپت بخشی از گراف شی هستند. نمودار با ریشه ها شروع می شود، به عنوان مثال، شی پنجره . مدیریت طول عمر ریشههای GC در کنترل شما نیست، زیرا توسط مرورگر ایجاد میشوند و در هنگام بارگیری صفحه از بین میروند. متغیرهای سراسری در واقع ویژگی های روی پنجره هستند.
چه زمانی یک ارزش تبدیل به زباله می شود؟
یک مقدار زمانی به زباله تبدیل می شود که هیچ مسیری از ریشه به ارزش وجود نداشته باشد. به عبارت دیگر، با شروع از ریشه ها و جستجوی جامع همه ویژگی ها و متغیرهای Object که در قاب پشته زنده هستند، به یک مقدار نمی توان رسید، تبدیل به زباله شده است.
نشت حافظه در جاوا اسکریپت چیست؟
نشت حافظه در جاوا اسکریپت معمولاً زمانی اتفاق میافتد که گرههای DOM وجود داشته باشند که از درخت DOM صفحه قابل دسترسی نیستند، اما همچنان توسط یک شی جاوا اسکریپت ارجاع داده میشوند. در حالی که مرورگرهای مدرن ایجاد ناخواسته نشت را به طور فزاینده ای دشوار می کنند، هنوز هم آسان تر از آن چیزی است که تصور می شود. فرض کنید یک عنصر را به درخت DOM اضافه می کنید به این صورت:
email.message = document.createElement("div");
displayList.appendChild(email.message);
و بعداً، عنصر را از لیست نمایش حذف می کنید:
displayList.removeAllChildren();
تا زمانی که email
وجود دارد، عنصر DOM که توسط پیام ارجاع داده شده است، حذف نخواهد شد، حتی اگر اکنون از درخت DOM صفحه جدا شده باشد.
Bloat چیست؟
هنگامی که از حافظه بیش از حد لازم برای سرعت مطلوب صفحه استفاده می کنید، صفحه شما پف می کند. به طور غیرمستقیم، نشت حافظه نیز باعث نفخ می شود، اما این به دلیل طراحی نیست. حافظه نهان برنامهای که هیچ محدودیتی برای اندازه ندارد، منبع متداول نفخ حافظه است. همچنین، صفحه شما می تواند توسط داده های میزبان، به عنوان مثال، داده های پیکسل بارگیری شده از تصاویر، پر شود.
زباله جمع آوری چیست؟
جمع آوری زباله نحوه بازیابی حافظه در جاوا اسکریپت است. مرورگر تصمیم می گیرد که چه زمانی این اتفاق بیفتد. در طول یک مجموعه، تمام اجرای اسکریپت در صفحه شما به حالت تعلیق در می آید در حالی که مقادیر زنده با پیمایش گراف شی که از ریشه های GC شروع می شود، کشف می شوند. تمام مقادیری که قابل دسترسی نیستند به عنوان زباله طبقه بندی می شوند. حافظه برای مقادیر زباله توسط مدیر حافظه بازیابی می شود.
V8 زباله جمع کن با جزئیات
برای کمک به درک بیشتر نحوه جمع آوری زباله، بیایید نگاهی به جمع آوری زباله V8 با جزئیات بیاندازیم. V8 از یک کلکتور نسلی استفاده می کند. حافظه به دو نسل تقسیم می شود: جوان و پیر. تخصیص و جمع آوری در نسل جوان سریع و مکرر است. تخصیص و جمع آوری در نسل قدیم کندتر و کمتر است.
کلکسیونر نسلی
V8 از یک کلکتور دو نسلی استفاده می کند. سن یک مقدار به عنوان تعداد بایت های تخصیص یافته از زمان تخصیص آن تعریف می شود. در عمل، سن یک ارزش اغلب با تعداد مجموعههای نسل جوانی که باقی مانده است، تقریب مییابد. پس از اینکه یک ارزش به اندازه کافی قدیمی شد، در نسل قدیمی نگهداری می شود.
در عمل، ارزشهای تازه تخصیص داده شده عمر طولانی ندارند. مطالعه برنامه های اسمال تاک نشان داد که تنها 7 درصد از ارزش ها پس از مجموعه نسل جوان باقی می مانند. مطالعات مشابه در طول زمان اجرا نشان داد که به طور متوسط بین 90٪ تا 70٪ از مقادیر تازه تخصیص داده شده هرگز در نسل قدیمی استفاده نمی شود.
نسل جوان
پشته نسل جوان در V8 به دو فضای به نامهای از و به تقسیم میشود. حافظه از فضا به فضا اختصاص می یابد. تخصیص بسیار سریع است، تا زمانی که فضای به پر شود و در آن نقطه یک مجموعه نسل جوان فعال شود. مجموعه نسل جوان ابتدا از و به فضا مبادله میکند، قدیمی به فضا (اکنون از فضا) اسکن میشود و همه مقادیر زنده در فضا کپی میشوند یا در نسل قدیمی نگهداری میشوند. یک مجموعه نسل جوان معمولی حدود 10 میلی ثانیه (ms) طول می کشد.
به طور شهودی، باید درک کنید که هر تخصیصی که برنامه شما ایجاد میکند، شما را به فضا نزدیکتر میکند و یک مکث GC را متحمل میشود. توسعه دهندگان بازی، توجه داشته باشید: برای اطمینان از زمان فریم 16 میلیثانیه (که برای دستیابی به 60 فریم در ثانیه لازم است)، برنامه شما باید تخصیص صفر داشته باشد، زیرا یک مجموعه نسل جوان بیشتر زمان فریم را میخورد.
نسل قدیم
هیپ نسل قدیمی در V8 از یک الگوریتم علامت فشرده برای جمع آوری استفاده می کند. تخصیص نسل قدیم زمانی اتفاق می افتد که ارزشی از نسل جوان به نسل قدیم منتقل شود. هر زمان که یک مجموعه نسل قدیم اتفاق می افتد، مجموعه نسل جوان نیز انجام می شود. برنامه شما به ترتیب چند ثانیه مکث خواهد شد. در عمل این قابل قبول است زیرا مجموعه های نسل قدیم نادر هستند.
V8 GC خلاصه
مدیریت خودکار حافظه با جمعآوری زباله برای بهرهوری توسعهدهندگان عالی است، اما هر بار که مقداری را تخصیص میدهید، به توقف جمعآوری زباله نزدیکتر میشوید. مکث های جمع آوری زباله با معرفی jank می تواند حس برنامه شما را خراب کند. اکنون که متوجه شدید که جاوا اسکریپت چگونه حافظه را مدیریت می کند، می توانید انتخاب های مناسبی برای برنامه خود داشته باشید.
تعمیر جیمیل
در طول سال گذشته، ویژگیها و رفع اشکالهای متعددی به ابزارهای توسعهدهنده کروم راه پیدا کردهاند و آنها را قدرتمندتر از همیشه کردهاند. علاوه بر این، خود مرورگر یک تغییر کلیدی در API performance.memory ایجاد کرد که این امکان را برای Gmail و هر برنامه دیگری فراهم کرد تا آمار حافظه را از این زمینه جمعآوری کند. مسلح به این ابزارهای عالی، چیزی که زمانی غیرممکن به نظر می رسید، به زودی به یک بازی هیجان انگیز برای ردیابی مجرمان تبدیل شد.
ابزار و تکنیک ها
Field Data and performance.memory API
از Chrome 22، API performance.memory به طور پیشفرض فعال است. برای برنامه های طولانی مدت مانند Gmail، داده های کاربران واقعی بسیار ارزشمند است. این اطلاعات به ما امکان می دهد بین کاربران قدرتمند - کسانی که 8 تا 16 ساعت در روز را در جیمیل سپری می کنند و صدها پیام در روز دریافت می کنند - از کاربران معمولی تر که چند دقیقه در روز را در جیمیل می گذرانند و ده ها یا بیشتر از آن دریافت می کنند، تمایز قائل شویم. پیام در هفته
این API سه قطعه داده را برمی گرداند:
- jsHeapSizeLimit - مقدار حافظه (بر حسب بایت) که پشته جاوا اسکریپت به آن محدود شده است.
- totalJSHeapSize - مقدار حافظه (بر حسب بایت) که پشته جاوا اسکریپت شامل فضای خالی اختصاص داده است.
- usedJSHeapSize - مقدار حافظه (بر حسب بایت) که در حال حاضر استفاده می شود.
نکته ای که باید در نظر داشت این است که API مقادیر حافظه را برای کل فرآیند کروم برمی گرداند. اگرچه این حالت پیشفرض نیست، اما تحت شرایط خاص، Chrome ممکن است چندین برگه را در یک فرآیند رندر باز کند. این بدان معنی است که مقادیر بازگردانده شده توسط performance.memory ممکن است علاوه بر برگههای حاوی برنامه شما، ردپای حافظه سایر برگههای مرورگر را نیز داشته باشد.
اندازه گیری حافظه در مقیاس
جیمیل جاوا اسکریپت خود را برای استفاده از API performance.memory برای جمعآوری اطلاعات حافظه تقریباً هر 30 دقیقه یک بار استفاده کرد. از آنجایی که بسیاری از کاربران جیمیل برنامه را برای روزها فعال میکنند، تیم قادر به ردیابی رشد حافظه در طول زمان و همچنین آمار کلی ردپای حافظه بود. در عرض چند روز از ابزارسازی جیمیل برای جمعآوری اطلاعات حافظه از نمونهگیری تصادفی از کاربران، این تیم دادههای کافی برای درک میزان گسترده مشکلات حافظه در میان کاربران معمولی داشت. آنها یک خط مبنا تعیین کردند و از جریان داده های دریافتی برای پیگیری پیشرفت به سمت هدف کاهش مصرف حافظه استفاده کردند. در نهایت از این داده ها برای گرفتن هرگونه رگرسیون حافظه نیز استفاده می شود.
فراتر از اهداف ردیابی، اندازهگیریهای میدانی همچنین بینش دقیقی از همبستگی بین ردپای حافظه و عملکرد برنامه ارائه میدهد. برخلاف تصور رایج که "حافظه بیشتر منجر به عملکرد بهتر می شود"، تیم Gmail دریافتند که هر چه ردپای حافظه بزرگتر باشد، تاخیرهای طولانی تری برای اقدامات رایج Gmail وجود دارد. با مسلح شدن به این مکاشفه، آنها بیش از همیشه انگیزه داشتند تا مصرف حافظه خود را مهار کنند.
شناسایی مشکل حافظه با خط زمانی DevTools
اولین گام در حل هر مشکل عملکرد، اثبات وجود مشکل، ایجاد یک آزمون قابل تکرار و اندازهگیری پایه مسئله است. بدون برنامه قابل تکرار، نمی توانید مشکل را به طور قابل اعتماد اندازه گیری کنید. بدون اندازهگیری پایه، نمیدانید چقدر عملکرد را بهبود دادهاید.
پانل DevTools Timeline یک کاندید ایده آل برای اثبات وجود مشکل است. این یک نمای کلی از زمان صرف شده هنگام بارگیری و تعامل با برنامه یا صفحه وب شما می دهد. همه رویدادها، از بارگیری منابع گرفته تا تجزیه جاوا اسکریپت، محاسبه سبکها، توقفهای جمعآوری زباله و رنگآمیزی مجدد در یک جدول زمانی ترسیم میشوند. به منظور بررسی مسائل مربوط به حافظه، پانل Timeline همچنین دارای یک حالت حافظه است که کل حافظه اختصاص داده شده، تعداد گره های DOM، تعداد اشیاء پنجره و تعداد شنوندگان رویداد اختصاص داده شده را ردیابی می کند.
اثبات وجود مشکل
با شناسایی دنباله ای از اقداماتی که گمان می کنید حافظه را درز می کند، شروع کنید. شروع به ضبط جدول زمانی کنید و دنباله ای از اقدامات را انجام دهید. برای جمع آوری کامل زباله از دکمه سطل زباله در پایین استفاده کنید. اگر پس از چند بار تکرار، نموداری به شکل دندانه اره مشاهده کردید، تعداد زیادی از اشیاء کوتاه مدت را به خود اختصاص می دهید. اما اگر انتظار نمی رود دنباله ای از اقدامات منجر به حافظه باقی مانده شود و تعداد گره های DOM به خط پایه ای که شروع کرده اید کاهش پیدا نکند، دلیل خوبی برای مشکوک شدن به نشت وجود دارد.
هنگامی که تأیید کردید که مشکل وجود دارد، می توانید برای شناسایی منبع مشکل از DevTools Heap Profiler کمک بگیرید.
یافتن نشت حافظه با DevTools Heap Profiler
پنل Profiler هم یک پروفایلر CPU و هم یک پروفایل Heap را ارائه می دهد. پروفایل Heap با گرفتن یک عکس فوری از نمودار شی کار می کند. قبل از گرفتن یک عکس فوری، هم نسل جوان و هم نسل قدیمی زباله جمع آوری می شوند. به عبارت دیگر، شما فقط مقادیری را خواهید دید که در هنگام گرفتن عکس فوری زنده بودند.
عملکردهای زیادی در نمایه ساز Heap وجود دارد که به اندازه کافی در این مقاله پوشش داده نمی شود، اما مستندات دقیق را می توانید در سایت توسعه دهندگان Chrome پیدا کنید. ما در اینجا بر روی نمایه ساز Heap Allocation تمرکز می کنیم.
با استفاده از Heap Allocation Profiler
نمایه ساز Heap Allocation اطلاعات لحظه ای دقیق از Heap Profiler را با به روز رسانی و ردیابی تدریجی پانل Timeline ترکیب می کند. پانل پروفایل ها را باز کنید، نمایه تخصیص پشته های ضبط را شروع کنید، دنباله ای از اقدامات را انجام دهید، سپس ضبط را برای تجزیه و تحلیل متوقف کنید. نمایه ساز تخصیص، عکس های فوری پشته ای را به صورت دوره ای در طول ضبط (به دفعات هر 50 میلی ثانیه!) و یک عکس فوری نهایی در پایان ضبط می گیرد.
میله های بالا نشان می دهد که چه زمانی اشیاء جدید در پشته پیدا می شوند. ارتفاع هر نوار مطابق با اندازه اشیاء اخیراً تخصیص داده شده است، و رنگ میلهها نشان میدهد که آیا آن اشیاء هنوز زنده هستند یا خیر: نوارهای آبی اشیایی را نشان میدهند که هنوز در انتهای جدول زمانی زنده هستند. ، نوارهای خاکستری اشیایی را نشان می دهد که در طول جدول زمانی اختصاص داده شده اند، اما پس از آن زباله جمع آوری شده اند.
در مثال بالا، یک عمل 10 بار انجام شد. برنامه نمونه پنج شی را در حافظه پنهان نگه می دارد، بنابراین پنج نوار آبی آخر انتظار می رود. اما نوار آبی سمت چپ نشان دهنده یک مشکل بالقوه است. سپس میتوانید از لغزندههای موجود در جدول زمانی بالا برای بزرگنمایی آن عکس فوری خاص و دیدن اشیایی که اخیراً در آن نقطه اختصاص داده شدهاند استفاده کنید. با کلیک بر روی یک شی خاص در پشته درخت نگهدارنده آن در قسمت پایین عکس فوری پشته نشان داده می شود. بررسی مسیر نگهدارنده به شی باید به شما اطلاعات کافی بدهد تا متوجه شوید که چرا شی جمعآوری نشده است و میتوانید تغییرات کد لازم را برای حذف ارجاع غیر ضروری انجام دهید.
حل بحران حافظه جیمیل
با استفاده از ابزارها و تکنیکهای مورد بحث در بالا، تیم جیمیل توانست چند دسته از اشکالها را شناسایی کند: حافظههای پنهان نامحدود، آرایههای بینهایت در حال رشد از تماسهای برگشتی که منتظر اتفاقی هستند که در واقع هرگز اتفاق نمیافتد، و شنوندگان رویداد ناخواسته اهداف خود را حفظ میکنند. با رفع این مشکلات، استفاده کلی از حافظه Gmail به طور چشمگیری کاهش یافت. کاربران در 99 درصد 80 درصد کمتر از قبل از حافظه استفاده می کردند و مصرف حافظه کاربران متوسط نزدیک به 50 درصد کاهش یافت.
از آنجایی که Gmail از حافظه کمتری استفاده میکرد، تأخیر مکث GC کاهش یافت و تجربه کلی کاربر را افزایش داد.
همچنین قابل توجه است که با جمعآوری آمار مربوط به استفاده از حافظه توسط تیم Gmail، آنها توانستند رگرسیونهای جمعآوری زباله در کروم را کشف کنند. به طور خاص، زمانی که دادههای حافظه Gmail افزایش چشمگیری را در شکاف بین کل حافظه اختصاصیافته و حافظه زنده نشان میداد، دو باگ تقسیمبندی کشف شد.
فراخوان برای اقدام
این سوالات را از خود بپرسید:
- برنامه من چقدر از حافظه استفاده می کند؟ این امکان وجود دارد که شما از حافظه بیش از حد استفاده می کنید که برخلاف تصور رایج، بر عملکرد کلی برنامه منفی است. سخت است که دقیقاً بدانید عدد مناسب چیست، اما مطمئن شوید که هر گونه ذخیره اضافی که صفحه شما از آن استفاده می کند تأثیر قابل اندازه گیری بر عملکرد دارد.
- آیا نشت صفحه من رایگان است؟ اگر صفحه شما دارای نشت حافظه باشد، نه تنها می تواند بر عملکرد صفحه شما تأثیر بگذارد، بلکه بر سایر برگه ها نیز تأثیر می گذارد. از ردیاب آبجکت برای کمک به محدود کردن هرگونه نشتی استفاده کنید.
- چند وقت یکبار صفحه من GCing می شود؟ میتوانید هر مکث GC را با استفاده از پانل Timeline در Chrome Developer Tools ببینید. اگر صفحه شما به طور مکرر GCing می شود، به احتمال زیاد به دفعات زیاد آن را تخصیص می دهید و در حافظه نسل جوان شما نقش می بندد.
نتیجه گیری
ما در یک بحران شروع کردیم. اصول اصلی مدیریت حافظه در جاوا اسکریپت و به ویژه V8 را پوشش داد. شما یاد گرفتید که چگونه از ابزارها استفاده کنید، از جمله ویژگی جدید ردیاب اشیا موجود در جدیدترین نسخه های Chrome. تیم جیمیل که به این دانش مجهز شده بود، مشکل استفاده از حافظه خود را حل کرد و شاهد بهبود عملکرد بود. شما می توانید همین کار را با برنامه های وب خود انجام دهید!