عنوان:

‫مدیریت حافظه و جمع‌آوری زباله (Garbage Collection) - قسمت دوم


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۳/۱۱/۱۲ ۱۴:۰۵
آدرس: www.dntips.ir
الگوریتم‌های مختلف Garbage Collection طراحی شده‌اند تا نیازهای متفاوتی مثل بارکاری (Workload)، سخت‌افزار و عملکرد را پوشش دهند. ولی نکته‌ی مهم این است که هیچ الگوریتمی وجود ندارد که برای تمام شرایط «بهترین» باشد. معیارهای مختلفی وجود دارند، مثل ایمنی (Safety)، بازدهی (Throughput)، کامل بودن (Completeness)، زمان توقف (Pause Time) و حتی مصرف انرژی (Energy Use).

ایمنی (Safety)
یعنی Collector (چیزی که مسئول جمع‌آوری و پاک‌سازی داده‌های غیرقابل استفاده از حافظه است) نباید هیچ وقت حافظه‌ای را که هنوز استفاده می‌شود، آزاد کند. ولی ایمنی به‌خصوص برای Concurrent Collectors ها هزینه دارد. بعضی Collector ها مثل Conservative Collectors ممکن است به‌خاطر بهینه‌سازی‌های کامپایلر نتوانند ایمنی کاملی را تضمین کنند.

بازدهی و مصرف CPU (Throughput and Cycles Consumed)
بازدهی یعنی زمان صرف‌شده‌ی برای جمع‌آوری زباله باید کم باشد. ولی باید تعادلی وجود داشته باشد، چون Collector و Mutator (بخشی از برنامه که اشیاء جدید را می‌سازد) هر دو از CPU استفاده می‌کنند. بعضی الگوریتم‌ها مثل Mark-Sweep ممکن است فازهای گران‌تری مثل فشرده‌سازی (Compacting) داشته باشند تا بهره‌وری Mutator، افزایش پیدا کند.

کامل بودن (Completeness)
کامل بودن یعنی همه‌ی زباله‌ها جمع شوند. ولی همیشه ممکن نیست یا حتی مطلوب نیست. مثلاً Collector های Reference Counting نمی‌توانند Cyclic Garbage را جمع کنند. همچنین Generational Collectors بیشتر بر روی اشیای جوان تمرکز می‌کنند و ممکن است بقیه‌ی هیپ را در هر چرخه، بررسی نکنند.

زمان توقف و تأخیر (Pause Time and Latency)
Collector هایی که همه Thread ها رو متوقف می‌کنند (Stop-the-World) ممکن است تأخیر زیادی ایجاد کنند. برای کاهش این تأخیر، Collector های Concurrent یا Incremental طراحی شده‌اند که در حین اجرای Mutator، بخشی از کار Collector را انجام می‌دهند.
در سیستم‌های معمولی، اگر GC بخواهد هیپ را مدیریت کند، می‌تواند همه‌ی Thread ها را متوقف کند تا از ثبات داده‌ها مطمئن شود. اما این باعث می‌شود برنامه برای مدتی طولانی، پاسخ‌گو نباشد که برای سیستم‌های بلادرنگ (Real-Time) اصلاً قابل قبول نیست.

برای همین، در حافظه‌های پایدار، دو تکنیک مهم برای کاهش تأخیر به‌کار گرفته می‌شود:
Concurrent Collectors
Incremental Collectors


این نمودار برای ارزیابی عملکرد GC و تأثیر آن بر اجرای برنامه بسیار مهم است. این نمودار نشان‌دهنده‌ی حداقل بهره‌وری Mutator (MMU) و بهره‌وری محدود شده‌ی Mutator (BMU) در بازه‌های زمانی مختلف است.
محور افقی (Time in ms):
بازه‌های زمانی مختلف از 1ms تا 10,000ms را نمایش می‌دهد. این بازه‌های زمانی نشان می‌دهند که در طول چه دوره‌ای، حداقل میزان استفاده‌ی Mutator از پردازنده چگونه بوده‌است. حداکثر Pause Time مستقیماً از محور x در جایی که MMU صفر است، قابل استخراج است.
محور عمودی (Utilisation %):
مقدار MMU و BMU را بر حسب درصد نشان می‌دهد. این مقدار نشان می‌دهد که چه درصدی از زمان، پردازنده در حال اجرای کدهای Mutator بوده است.
شیب BMU نشان‌دهنده‌ی چگونگی بازیابی Mutator پس از توقف‌های GC است. هرچه MMU مقدار بالاتری داشته باشد، به معنای بهره‌وری بهتر و کاهش تأخیرهای GC است.
هدف بهینه‌سازی GC این است که Pause Time را کاهش داده و MMU را در تمامی بازه‌های زمانی تا حد امکان بالا نگه دارد.

چگونه Pause Time را از نمودار استخراج کنیم؟
بزرگ‌ترین بازه‌ی زمانی که مقدار MMU صفر است، برابر با حداکثر Pause Time است. پس از عبور از این نقطه، مقدار MMU به سرعت افزایش پیدا می‌کند، که نشان‌دهنده‌ی بازگشت اجرای Mutator است. تعداد و شدت افت‌های بعدی در MMU نشان‌دهنده‌ی تأثیرگذاری GC بر روی کارآیی Mutator است.

پایین‌ترین مقدار BMU و MMU در محور افقی (زمان بسیار کم) برابر است با حداکثر Pause Time. این مقدار نشان‌دهنده‌ی بیشترین زمانی‌است که سیستم در آن متوقف شده و هیچگونه اجرای Mutator ای رخ نداده‌است. از نمودار مشخص است که در بازه‌ی کمتر از 10 میلی‌ثانیه، مقدار MMU تقریباً صفر است. یعنی در این مدت، Mutator متوقف شده‌است.
افزایش ناگهانی در مقدار BMU و MMU پس از ~100ms. در این نقطه، Mutator دوباره اجرا می‌شود و میزان استفاده‌ی آن از پردازنده افزایش می‌یابد. این نشان می‌دهد که توقف GC به پایان رسیده و Mutator دوباره شروع به اجرا کرده‌است.
وجود پیک‌های نوسانی در مقدار MMU (نقاط خط‌چین بین 100ms تا 1000ms). این نوسانات نشان‌دهنده‌ی وقفه‌های دوره‌ای GC است که باعث کاهش موقتی بهره‌وری Mutator می‌شوند. این رفتار معمولاً در GCهای Incremental یا Concurrent مشاهده می‌شود.
شیب ملایم BMU و افزایش پایدار MMU پس از 1000ms. این قسمت نشان می‌دهد که سیستم به تعادل رسیده و بهره‌وری Mutator در حال افزایش است. GC، دیگر توقف‌های بزرگی ایجاد نمی‌کند، اما همچنان مقدار MMU به تدریج بهبود می‌یابد.

Paul R. Wilson در مقاله‌ای که در سال 1992 با عنوان Uniprocessor garbage collection techniques منتشر کرد که یکی از منابع کلاسیک در بررسی الگوریتم‌های مختلف GC برای سیستم‌های تک‌پردازنده (Uniprocessor) است. روش‌های مختلف جمع‌آوری زباله مانند Mark-and-Sweep, Copying Collection, Reference Counting و Generational Collection را بررسی می‌کند. یکی از نتایج کلیدی این مقاله این است که Copying GC معمولاً سریع‌تر از Mark-and-Sweep است، اما به حافظه‌ی بیشتری نیاز دارد. همچنین به این موضوع اشاره دارد که Reference Counting برای مدیریت حلقه‌های ارجاع ضعیف است و به مکانیسم‌های کمکی نیاز دارد.