مدیریت اشیاء بزرگ در جمعآوری زباله
اشیاء بزرگ چالشهای خاصی دارند که باعث میشود استراتژی خاصی برای مدیریت آنها لازم باشد.
چالشهای اشیاء بزرگ:
- هزینه بالای تخصیص حافظه
- ایجاد تکهتکه شدن حافظه (Fragmentation)
- هزینه بالای جابجایی (Copying Cost)
روشهای تخصیص اشیاء بزرگ
سه معیار برای تعیین "بزرگ" بودن یک شیء وجود دارد:
- اندازهی مطلق (مثلاً بیش از 1024 بایت)
- اندازهی نسبی نسبت به بلاکهای تخصیصدهنده
- اندازهی نسبی نسبت به کل هیپ
روشهای معمول مدیریت:
Mark-Sweep: پاکسازی بدون جابجایی
استفاده از هدرهای جداگانه برای مدیریت اشیاء بزرگ
عدم انتقال اشیاء بزرگ به نسلهای مختلف در GC نسلگرا
جمعآوری زباله با Treadmill
یک روش جالب برای مدیریت اشیاء بزرگ، الگوریتم Treadmill هست. اشیاء درون یک لیست پیوندی دوطرفه چرخهای (Cyclic Doubly-Linked List) سازماندهی میشوند. بهجای جابجا کردن اشیاء، فقط موقعیت آنها در لیست تغییر میکند!
مزایا:
عدم نیاز به کپیبرداری (Copy-Free Collection)
عملیات تخصیص و آزادسازی سریع با هزینه ثابت (O(1))
امکان اجرای همزمان (Concurrent Execution)
چالشها:
هر شیء نیاز به دو اشارهگر اضافی دارد
مدیریت اشیاء با اندازههای مختلف پیچیده میشود
جابجایی مجازی اشیاء با کمک سیستمعامل
یک روش دیگر برای مدیریت اشیاء بزرگ این است که آنها را جابجا نکنیم، بلکه فقط آدرس آنها را تغییر بدهیم! بهجای کپی کردن بیتبهبیت یک شی بزرگ، صفحههای (Pages) حافظهی آن را به یک آدرس جدید مپ (Map) میکنیم. این کار با Remapping Virtual Memory Pages انجام میشود.
مزایا:
سرعت بالاتر نسبت به کپیبرداری سنتی
کاهش فشار روی حافظه کش (Cache-Friendly)
امکان مقداردهی تدریجی اشیاء بزرگ با کمک Memory Protection
بهینهسازی برای اشیاء بدون اشارهگر (Pointer-Free Objects)
یک روش دیگر برای بهینهسازی مدیریت حافظه، جداسازی اشیاء بدون اشارهگر است. این اشیاء نیازی به اسکن ندارند، پس میتوانیم آنها را در یک بخش جداگانه ذخیره کنیم.
روشها:
نشانهگذاری در آدرسها
مدیریت جداگانهی رشتهها (Strings) و آرایههای عددی
جمعآوری زباله بر اساس توپولوژی گراف اشیاء
توپولوژی یعنی اینکه چیدمان اشیاء در حافظه را بر اساس ارتباطات بین آنها(یعنی گراف اشارهگرها) تنظیم کنیم. بهجای اینکه فقط بر اساس سن اشیاء تصمیم بگیریم که چه چیزی را جمعآوری کنیم، این روش سعی میکند اشیایی را که به هم مرتبط هستند، کنار هم نگه دارد.
روشهای اصلی این نوع GC عبارتند از:
- الگوریتم Train Collector
- جمعآوری زباله بر اساس ارتباطات (Connectivity-Based Collection)
- جمعآوری زباله در سطح نخ (Thread-Local Garbage Collection)
- مدیریت حافظه مبتنی بر استک (Stack Allocation)
- مدیریت حافظه مبتنی بر نواحی (Region-Based Memory Management)
الگوریتم Train Collector
این الگوریتم توسط Hudson و Moss در 1992 معرفی شد و هدفش کاهش زمان توقف (Pause Time) برای دادههای قدیمیتر بود. در GC نسلگرا، جمعآوری نسلهای قدیمی خیلی هزینه بر است، چون کل حافظه را اسکن میکند. ولی این الگوریتم با تقسیمبندی نسلهای قدیمی به بخشهای کوچیکتر (Cars and Trains) این مشکل را حل میکند.
عملکرد:
حافظهی اشیاء قدیمی به چندین قطار (Train) و واگن (Car) تقسیم میشود. در هر مرحله، فقط اولین واگن قطار قدیمیتر بررسی میشود. اشیائی که هنوز زنده هستند، به واگنهای دیگر منتقل میشوند. اگر یک قطار بهطور کامل خالی شود، حذف میشود. اگر یک چرخهی زباله تشکیل گردد، بعد از چند مرحله خودش جدا شده و پاک میشود!
مزایا:
- مدیریت کارآمد حافظه : فقط بخشهایی که نیاز به جمعآوری دارند، بررسی میشود.
- کاهش سربار پردازشی : لازم نیست کل حافظه را اسکن کنیم.
- مدیریت چرخههای زباله : چرخهها بهطور خودکار از بین میروند.
مشکلات:
- اگر یک شیء محبوب (Popular Object) داشته باشیم که خیلیها به آن اشاره میکنند، در همهی واگنها کپی میشود و هزینهها زیاد میشوند.
- ممکن است "جمعآوری بینتیجه (Futile Collection)" رخ بدهد، یعنی GC نتواند تصمیم بگیرد که کدام بخش را پاک کند.
جمعآوری زباله بر اساس ارتباطات (Connectivity-Based Collection)
هدف این روش، کاهش سربار Write Barrier و بهینهسازی مدیریت حافظه بر اساس ارتباط بین اشیاء است.
عملکرد:
گراف حافظه را به پارتیشنهای پایدار (Stable Partitions) تقسیم میکند. فقط پارتیشنهایی که مستقل از بقیه هستند، جمعآوری میشوند. نیازی به Write Barrier نداریم!
مزایا:
- نیازی به مانع نوشتن (Write Barrier) نداریم : یعنی عملکرد بهتری داریم.
- مدیریت بهینهتر حافظه : چون فقط پارتیشنهای مستقل پردازش میشوند.
مشکلات:
- بهینهسازی پارتیشنبندی سخت و پیچیده است.
- نتایج عملکردی این روش هنوز از GC نسلگرا ضعیفتر است.
جمعآوری زباله در سطح نخ (Thread-Local Garbage Collection)
اگر بتوانیم اشیاء مربوط به هر نخ را جدا کنیم، میتوانیم هر نخ را مستقل از بقیه جمعآوری کنیم.
عملکرد:
هر نخ حافظهی اختصاصی خودش (Thread-Local Heap) را دارد. فقط وقتی که حافظهی خودش پر شود، GC روی همان بخش اجرا میشود. اشیائی که به اشتراک گذاشته میشوند، به یک حافظهی مشترک (Shared Heap) منتقل میشوند.
مزایا:
- کاهش زمان توقف (Pause Time) : چون GC نیازی به توقف کل برنامه ندارد.
- بهینهتر برای سیستمهای چندنخی مثل Java و Erlang.
مشکلات:
- اگر اشیاء زیادی به اشتراک گذاشته شوند، کارایی کاهش پیدا میکند.
- نیاز به همگامسازی پیچیده دارد.
مدیریت حافظه مبتنی بر استک (Stack Allocation)
اگر اشیاء کوتاهعمر مستقیماً روی استک ذخیره شوند، نیازی به جمعآوری زباله نخواهیم داشت!
عملکرد:
از تحلیل فرار (Escape Analysis) استفاده میکنیم تا ببینیم یک شی فقط در محدودهی یک تابع استفاده میشود یا خیر. اگه بله، بهجای هیپ، روی استک ذخیره میشود.
مزایا:
- کاهش نیاز به GC : اشیاء بهطور خودکار هنگام خروج از تابع پاک میشوند.
- افزایش کارایی : نیازی به پیگیری اشارهگرها نداریم.
مشکلات:
- فقط برای اشیاء کوتاهعمر کاربرد دارد.
- برای اشیاء پویا و پیچیده مناسب نیست.
مدیریت حافظه مبتنی بر نواحی (Region-Based Memory Management)
بهجای پاک کردن یک به یک اشیاء ، کل ناحیهی حافظهای که آنها داخلش هستند را با هم آزاد کنیم.
عملکرد:
حافظه به ناحیههای جداگانه (Regions) تقسیم میشود. وقتی که یک ناحیهی خاص به کار نمی آید، کل آن ناحیه آزاد میشود. سیستمهایی مثل Rust و Real-Time Java از این روش استفاده میکنند.
مزایا:
- مدیریت حافظهی کارآمد
- کاهش هزینهی GC برای دادههای بلندمدت
مشکلات:
- پیچیدگی در مدیریت دستی
- نیاز به دانش دقیق از چرخهی عمر دادهها
نتیجهگیری:
- اگر نیاز به کاهش Pause Time دارید : Train Collector یا Thread-Local GC
- اگر دنبال حذف Write Barrier هستید :Connectivity-Based GC
- اگر دادهها خیلی کوتاهمدت هستند : Stack Allocation
- اگر به مدیریت نواحی مشخص نیاز دارید : Region-Based Memory Management