عنوان:

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


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۳/۱۱/۲۱ ۱۲:۱۰
آدرس: www.dntips.ir
Lazy Sweeping
Lazy Sweeping یکی از بهبودهای هوشمندانه بر روی الگوریتم Mark-Sweep است که تلاش می‌کند سربار زمانی مرحله Sweep را کاهش بدهد و عملکرد کلی را بهتر کند.

چرا مرحله Sweep این‌قدر مهم است؟
در Mark-Sweep، مرحله Mark، پیچیدگی زمانی O(L) دارد؛ جائیکه L، اندازه داده‌های زنده در حافظه‌است. مرحله Sweep پیچیدگی زمانی O(H) دارد؛ جائیکه H، کل اندازه حافظه‌است. چون معمولاً H>L، به‌نظر می‌رسد مرحله Sweep باید بیشتر هزینه‌بر باشد؛ اما در عمل، این‌طور نیست؛ چون:
  • دسترسی‌های مرحله Mark تصادفی هستند: Collector باید به صورت نامنظم در گراف اشیاء حرکت کند.
  • مرحله Sweep الگوی دسترسی بهتری دارد: معمولاً اشیاء به صورت متوالی اسکن می‌شوند که باعث می‌شود کش و مکانیزم‌های سخت‌افزاری مثل Prefetching عملکرد بهتری داشته باشد.

ایده Lazy Sweeping
در روش سنتی، مرحله Sweep، کل حافظه را به صورت یکجا اسکن می‌کند؛ اما در Lazy Sweeping:
مرحله Sweep به‌جای اینکه یکجا انجام شود، به درخواست‌های تخصیص حافظه تقسیم می‌شود.
وقتی یک Mutator (برنامه‌ای که در حال اجراست) درخواست حافظه می‌دهد، تخصیص‌دهنده (Allocator) فقط به اندازه‌ی نیاز، Sweep انجام می‌دهد.

Lazy Sweeping مراحل زیر را انجام می‌دهد:
در فاز Stop-the-World ، کلکتور، تمام اشیای زنده را علامت‌گذاری می‌کند. به‌جای جارو کردن کل حافظه، فقط بلوک‌هایی که کاملاً خالی هستند به تخصیص‌دهنده برگردانده می‌شوند. بقیه بلوک‌ها به لیست بازیابی (Reclaim List) اضافه می‌شوند.
وقتی Mutator درخواست تخصیص حافظه می‌دهد:
ابتدا بلوک‌های موجود در لیست بازیابی جارو می‌شوند. اگر باز هم حافظه کافی نبود، بلوک‌های جدید تخصیص داده می‌شوند.

Lazy Sweeping چند مزیت مهم دارد:
  1. بهبود locality: شکاف‌های حافظه بلافاصله بعد از جارو شدن، استفاده می‌شوند.
  2. کاهش پیچیدگی زمانی: این روش، پیچیدگی زمانی را به O(L) کاهش می‌دهد؛ مشابه الگوریتم‌های کپی.
  3. کاهش توقف Mutator: چون Sweep به صورت تنبل انجام می‌شود، زمان توقف برنامه کاهش پیدا می‌کند.

چالش‌ها :
  1. احتمال نشت حافظه (Memory Leak): اگر بلوک‌های استفاده‌نشده، جارو نشوند، ممکن است حافظه آزاد نشود.
  2. مشکلات با بلوک‌های جداگانه: در سیستم‌هایی که حافظه به بلوک‌های مختلف برای اندازه‌های مختلف تقسیم می‌شود، ممکن است یک کلاس، اندازه حافظه‌اش تمام شود ولی بلوک‌های دیگر هنوز جارو نشده باشند.

راه‌حل‌ها برای چالش‌ها
  1. علامت‌گذاری چرخه‌ای (Cyclic Marking): به‌جای استفاده از یک بیت برای علامت‌گذاری، از چند بیت استفاده می‌شود تا اشیایی که در چرخه‌های قبلی علامت‌گذاری شدند مشخص شوند.
  2. جارو کردن بلوک‌های کامل: اگر هیچ شیء زنده‌ای در یک بلوک نباشد، کل بلوک می‌تواند به تخصیص‌دهنده برگردانده شود.
  3. اولویت‌بندی بلوک‌ها: بلوک‌هایی که بیشتر احتمال جارو شدن دارند، اولویت بیشتری برای Sweep می‌گیرند.

atomic collect() {
    markFromRoots();
    for each block in Blocks {
        if not isMarked(block)
            add(blockAllocator, block);  /* no objects marked in this block? */
                                         /* return block to block allocator */
        else
            add(reclaimList, block);     /* queue block for lazy sweeping */
    }
}

atomic allocate(sz) {
    result ← remove(sz);                 /* allocate from size class for sz */
    if result = null {
        lazySweep(sz);                    /* if no free slots for this size.. */
        result ← remove(sz);               /* sweep a little */
    }
    return result;                        /* if still null, collect */
}

lazySweep(sz) {
    repeat {
        block ← nextBlock(reclaimList, sz);
        if block ≠ null {
            sweep(start(block), end(block));
            if spaceFound(block)
                return;
        }
    } until block = null;                 /* reclaim list for this size class empty */
    allocSlow(sz);                         /* get an empty block */
}

allocSlow(sz) {
    block ← allocateBlock();               /* allocation slow path */
    if block ≠ null
        initialise(block, sz);              /* from the block allocator */
}

در این قطعه کد:
collect: اشیای زنده را علامت‌گذاری می‌کند. بلاک‌های بدون داده‌ی زنده، آزاد می‌شوند و بقیه برای lazy sweeping در لیست reclaimList قرار می‌گیرند.
allocate(sz): سعی می‌کند حافظه‌ای را با اندازه‌ی sz تخصیص دهد. اگر فضا نباشد، lazySweep را اجرا می‌کند.
lazySweep(sz): بلاک‌های علامت‌گذاری‌شده را بررسی کرده و اگر فضایی پیدا نشود، مسیر allocSlow را طی می‌کند.
allocSlow(sz): بلاک جدیدی را تخصیص می‌دهد.