عنوان:

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


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۳/۱۱/۳۰ ۰۹:۳۵
آدرس: www.dntips.ir
تا به اینجا چهار نوع Garbage Collection را بررسی کردیم؛ چطوری می‌توانیم بفهمیم کدام روش بهتراست؟
به جای اینکه بگیم "بهترین روش چی هست؟"، باید ببینیم چه معیاری برای ما مهم‌تراست؟ مثلا:
  • اگر هدف ما بیشترین عملکرد (Throughput) باشد، کدام روش سریع‌تراست؟
  • اگر هدف کاهش توقف‌ها (Latency) باشد، کدام روش وقفه‌های کوتاه‌تری دارد؟
  • اگر محدودیت حافظه داشته باشیم، کدام روش بهتر فضا را مدیریت می‌کند؟

بررسی اولین معیار: Throughput (بازدهی کلی برنامه)
Throughput به این معنی است که در یک بازه‌ی زمانی مشخص، برنامه چقدر سریع اجرا می‌شود. Mark-Sweep کمترین بار پردازشی روی Mutator را دارد (چون اشاره‌گرها را تغییر نمی‌دهد) ولی ممکن است باعث تکه‌تکه شدن حافظه (Fragmentation) شود. Mark-Compact حافظه را فشرده‌سازی می‌کند، پس Fragmentation ندارد؛ ولی چندین عبور روی حافظه دارد که ممکن است زمان‌بر باشد. Copying (Semispace) کمترین هزینه‌ی تخصیص حافظه را دارد ولی نصف حافظه را مصرف می‌کند و برای حجم زیاد داده مشکل‌ساز می‌شود. Reference Counting زباله‌ها را بلافاصله حذف می‌کند ولی هزینه‌ی پردازشی زیادی روی Mutator دارد (به‌ویژه در خواندن و نوشتن اشاره‌گرها)
نتیجه:
  • اگر حجم داده‌ی زنده کم باشد، Mark-Sweep بهترین بازدهی را دارد.
  • اگر نیاز به مدیریت حافظه‌ی بهینه باشد، Mark-Compact گزینه‌ی خوبی محسوب می‌شود.
  • اگر تخصیص حافظه سریع باشد، Copying می‌تواند سریع‌تر اجرا شود، ولی مصرف حافظه بالاست.
  • Reference Counting بیشتر در محیط‌های خاص مثل پردازش تصویر یا سیستم‌های توزیع‌شده مفید است.

در نهایت، کدام روش را انتخاب کنیم؟
  • اگر بخواهیم حافظه را کم‌تر مصرف کنیم، Mark-Sweep یا Mark-Compact مناسب است.
  • اگر مهم‌ترین معیار، سرعت تخصیص حافظه باشد، Copying بهترین گزینه‌ست.
  • اگر بخواهیم حافظه‌ها را بلافاصله آزاد کنیم، ولی نگران چرخه‌های حافظه نیستیم، Reference Counting گزینه‌ی خوبی محسوب می‌شود.

بررسی تأخیر (Latency) و زمان توقف (Pause Time) در Garbage Collection
یکی از مهم‌ترین معیارها تأخیر (Latency) و زمان توقف (Pause Time) است. در سیستم‌های تعامل‌گرا (Interactive Systems) مثل رابط‌های کاربری، بازی‌ها و پردازش‌های تراکنشی، نباید توقف‌های طولانی داشته باشیم، چون باعث کندی تجربه‌ی کاربری و ایجاد بک‌لاگ (Backlog) از وظایف معلق می‌شود. Mark-Sweep زمان توقف متوسط تا بالا (بسته به حجم داده‌های زنده) و تاخیر ممکن است ولی قابل پیش‌بینی است؛ اما ممکن است تکه‌تکه شدن حافظه ایجاد کند. Mark-Compact زمان توقف بالا (چندین بار عبور روی حافظه) و تأخیر زیاد در زمان اجرا دارد ولی قابل پیش‌بینی است. اما تأخیر طولانی در فشرده‌سازی حافظه دارد. در Copying (Semispace) زمان توقف، کوتاه‌تر از Mark-Sweep است و تأخیر کمتر، ولی مصرف حافظه دو برابر است. اما نصف حافظه باید خالی بماند. Reference Counting بدون توقف ولی ممکن است تأخیر ناگهانی در حذف بازگشتی داشته باشد. Deferred Reference Counting باید در یک زمان خاص، اصلاحات انجام شود.نیاز به توقف مقطعی برای به‌روزرسانی جدول شمارنده‌های صفر دارد.
نتیجه:
  • اگر تأخیر کم و توقف‌های کوتاه می‌خواهیم: Copying یا Reference Counting (بهینه شده) مناسب است.
  • اگر هدف مدیریت مؤثر حافظه است، ولی توقف طولانی مهم نیست: Mark-Compact گزینه‌ی خوبی محسوب می‌شود.
  • اگر GC باید در پس‌زمینه اجرا شود و تأخیر مهم نیست: Mark-Sweep خوب کار می‌کند.

آیا Reference Counting کاملاً بدون توقف است؟
نه دقیقاً! روش‌هایی مثل Deferred Reference Counting و Coalesced Reference Counting سعی می‌کنند مشکلات همزمانی را کاهش دهند، ولی برای به‌روزرسانی جدول شمارنده‌های صفر (Zero Count Table) باید یک وقفه‌ی کوتاه ایجاد کنند. همچنین وقتی شمارنده‌ی مرجع یک شیء به صفر می‌رسد، باید ببینیم آیا این شیء هنوز از طریق چرخه‌ها در دسترس است یا خیر. برای حل این مشکل، GC ترکیبی (Hybrid GC) که از Tracing برای چرخه‌ها و Reference Counting برای بقیه‌ی موارد استفاده می‌کند، می‌تواند عملکرد بهتری ارائه بدهد.
پس در عمل، هیچ GC کاملاً بدون تأخیر نیست! اما می‌شود تأخیرها را کم کرد.

بررسی مصرف حافظه در انواع Garbage Collection
Copying GC (Semispace)
مشکل اصلی: این روش نیاز دارد که نصف فضای Heap همیشه خالی باشد، چون باید حافظه‌ی کافی برای کپی کردن اشیای زنده داشته باشیم.
نتیجه: حداقل دو برابر فضای مورد نیاز واقعی باید در دسترس باشد!

Mark-Sweep GC
مشکل اصلی: این روش باعث تکه‌تکه شدن حافظه (Fragmentation) می‌شود، چون اشیای جدید باید در بین فضای آزاد پراکنده‌شده قرار بگیرند.
راه‌حل: برای کاهش Fragmentation از Segregated Free-Lists استفاده می‌شود، ولی همین هم فضای اضافه برای مدیریت حافظه مصرف می‌کند.

Mark-Compact GC
مزیت: با فشرده‌سازی (Compaction)، مشکل Fragmentation را حل می‌کند.
مشکل: برای مرتب‌سازی اشیا، باید چند بار روی کل Heap پردازش انجام شود که این کار خودش نیازمند ساختارهای کمکی (Auxiliary Data Structures) مثل Mark Bitmap است.

Reference Counting GC
مزیت: این روش اشیا را بلافاصله بعد از بی‌استفاده شدن آزاد می‌کند، پس نیازی به فضای زیاد برای Garbage Collection نیست.
مشکل: به یک فیلد اضافه در هدر هر شیء نیاز دارد تا تعداد ارجاعات را ذخیره کند.
چالش دیگر: نمی‌تواند داده‌های چرخه‌ای (Cyclic Garbage) را تشخیص بدهد، مگر اینکه یک GC ترکیبی به آن اضافه شود.

بررسی چالش‌های پیاده‌سازی انواع Garbage Collection
خطاهای GC معمولاً بعد از چندین مرحله اجرا خودشان را نشان می‌دهند. یک مشکل رایج این است که یک شیء پاک شود، در حالیکه هنوز به آن ارجاعی وجود دارد! یا برعکس، یک شیء زامبی (Zombie Object) در حافظه بماند و پاک نشود، چون GC نتوانسته تشخیص بدهد که دیگر نیازی به آن نیست.

الگوریتم‌های Tracing GC (مثل Mark-Sweep و Copying GC)
مزیت: ارتباط ساده‌ای با برنامه‌ی اجرایی (Mutator) دارند، چون فقط وقتی حافظه کم میاید، اجرا می‌شوند.
چالش: باید بتوانند تمام ریشه‌های (Roots) برنامه را پیدا کنند، از جمله متغیرهای Global، استک CPU و رجیسترها.
مشکل برای Moving GC: چون اشیاء را جابه‌جا می‌کنند، باید تمام مراجع (References) به آن اشیا را پیدا و آپدیت کنند.

الگوریتم‌های Reference Counting
مزیت: می‌توانند به‌عنوان یک کتابخانه‌ی مستقل پیاده‌سازی شوند، پس برنامه‌نویس می‌تواند انتخاب کند کدام اشیا با Reference Counting مدیریت شوند.
چالش: این روش باید همزمان با اجرای برنامه (Mutator) اجرا گردد، پس هزینه‌ی پردازشی بالایی دارد.
مشکل برای حافظه‌های اشتراکی: در برنامه‌های چندنخی (Multithreading) باید اطمینان حاصل شود که تمام تغییرات روی Reference Counts ایمن هستند و تداخل داده (Race Condition) پیش نمیاید.

نکات مهم برای پیاده‌سازی GC سریع و کارآمد:
  1. کدهای GC باید تا حد ممکن درون‌خطی (Inlined) باشند تا سرعت اجرا بالا رود.
  2. مسیرهای اجرای متداول (Fast Path) باید کوتاه و بهینه باشند و بقیه‌ی کارها در مسیرهای کندتر (Slow Path) اجرا شوند.
  3. بررسی عملکرد (Performance Profiling) ضروری است! کدهای اسمبلی خروجی باید بررسی شوند که GC باعث کندی بیش از حد نشود.
  4. مدیریت کش پردازنده (CPU Cache Management) اهمیت زیادی دارد، چون اگر اندازه‌ی کدهای GC زیاد شود، پردازنده زمان بیشتری برای واکشی دستورالعمل‌ها از کش صرف می‌کند.