عنوان:

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


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

در Mark-Sweep، اشیاء زنده بدون جابه‌جایی در جای خودشان باقی می‌مانند؛ اما در Mark-Compact، اشیاء زنده در یک سمت حافظه جمع می‌شوند تا فضایی پیوسته برای تخصیص‌های جدید فراهم گردد. مشکل اصلی جمع‌کننده‌های غیر جابه‌جایی (Non-Moving Collectors) مثل Mark-Sweep این است که حافظه به بخش‌های کوچک شکسته می‌شود که ممکن است نتوانند درخواست‌های تخصیص حافظه‌ی جدید را مدیریت کنند. یعنی ممکن است حافظه خالی باشد ولی چون پیوسته نیست، قابل استفاده نباشد که به آن تکه‌تکه شدن خارجی (External Fragmentation) گفته می‌شود. برای حل این مشکل، Mark-Compact اشیاء زنده را فشرده می‌کند و همه‌ی فضای خالی را یک‌جا جمع می‌کند.

روند کلی این الگوریتم چند فاز دارد:
  1. Marking (علامت‌گذاری): مثل Mark-Sweep، تمام اشیای زنده را پیدا می‌کند و علامت‌گذاری می‌کند.
  2. Compacting (فشرده‌سازی): اشیای زنده را به سمت ابتدای حافظه حرکت می‌دهد تا فضای خالی یک‌جا جمع شود.
  3. Updating Pointers (به‌روزرسانی اشاره‌گرها): تمام اشاره‌گرها را تنظیم می‌کند تا به مکان جدید اشیاء اشاره کند.

انواع مرتب‌سازی اشیاء در حین فشرده‌سازی :
  1. Arbitrary (ترتیب تصادفی): بدون در نظر گرفتن ترتیب اصلی، اشیاء به مکان‌های جدید منتقل می‌شوند.
  2. Linearising (مرتب‌سازی رابطه‌ای): اشیای مرتبط، کنار هم قرار می‌گیرند (مثلاً اشیایی که به یکدیگر اشاره دارند یا در یک ساختار داده‌ای قرار دارند).
  3. Sliding (لغزشی): اشیاء در همان ترتیب قبلی‌شان، ولی بدون فاصله‌های خالی، قرار می‌گیرند.

معرفی الگوریتم‌های فشرده‌سازی


Two-Finger Compaction (فشرده‌سازی دو انگشتی)
این یک الگوریتم دو مرحله‌ای، برای فشرده‌سازی اشیاء زنده‌ی در حافظه است که مخصوص اشیای هم‌اندازه طراحی شده است. این روش با استفاده از دو اشاره‌گر (Finger) به نام‌های free و scan، اشیاء زنده را به یک سمت حافظه منتقل می‌کند.

مراحل اجرای Two-Finger Compaction
مرحله اول - جابه‌جایی اشیا زنده:
free از ابتدای حافظه شروع می‌کند و دنبال فضای خالی (gap) می‌گردد. scan از انتهای حافظه شروع می‌کند و دنبال شیء زنده (Live Object) می‌گردد. وقتی هر دو مورد پیدا شدند، شیء زنده از scan به free منتقل میشود. در مکان قدیمی شیء (scan)، یک آدرس جایگزین (Forwarding Address) نوشته می‌شود تا در مرحله‌ی بعدی، اشاره‌گرهای قدیمی تصحیح شوند. فرآیند تا جایی که free و scan به یکدیگر برسند، ادامه پیدا می‌کند.
مرحله دوم - به‌روزرسانی اشاره‌گرها:
در این مرحله، تمام اشاره‌گرهایی که به مکان‌های قدیمی اشیاء اشاره می‌کردند، به مکان‌های جدید هدایت می‌شوند. این کار با استفاده از آدرس جایگزین (Forwarding Address) که قبلاً در مرحله اول ذخیره شده، انجام می‌شود.

compact():
    relocate(HeapStart, HeapEnd, slotSize)
    updateReferences(HeapStart, free, slotSize)

relocate(start, end, slotSize):
    free ← start
    scan ← end

    while free < scan
        while isMarked(free)
            unsetMarked(free)
            free ← free + slotSize    /* find next hole */

        while not isMarked(scan) && scan > free
            scan ← scan - slotSize    /* find previous live object */

        if scan > free
            unsetMarked(scan)
            move(scan, free)
            *scan ← free              /* leave forwarding address (destructively) */
            free ← free + slotSize
            scan ← scan - slotSize

updateReferences(start, end, slotSize):
    for each fld in Roots        /* update roots that pointed to moved objects */
        ref ← *fld
        if ref ≥ end
            *fld ← *ref          /* use the forwarding address left in first pass */

    scan ← start
    while scan < end             /* update fields in live region */
        for each fld in Pointers(scan)
            ref ← *fld
            if ref ≥ end
                *fld ← *ref      /* use the forwarding address left in first pass */
        scan ← scan + slotSize   /* next object */

مزایا و معایب الگوریتم Two-Finger Compaction
مزایا:
  1. سرعت بالا و سادگی: الگوریتم، فقط یک بار کل هیپ را اسکن می‌کند و نیاز به متادیتای اضافی ندارد.
  2. عدم نیاز به حافظه اضافی: چون آدرس‌های جایگزین فقط در مکان‌های قدیمی (که دیگر استفاده نمی‌شوند) ذخیره می‌شوند.
  3. پشتیبانی از اشاره‌گرهای داخلی (Interior Pointers): یعنی اگر اشاره‌گری به داخل یک شیء وجود داشته باشد، این روش می‌تواند آن را مدیریت کند.
  4. الگوی دسترسی به حافظه‌ی قابل پیش‌بینی: به دلیل حرکت یکنواخت scan، سخت‌افزار می‌تواند از Prefetching برای بهبود سرعت استفاده کند.

معایب:
  1. وابسته به اشیای هم‌اندازه: اگر اشیاء اندازه‌های مختلفی داشته باشند، ممکن است فضای خالی مناسبی پیدا نشود و فرآیند فشرده‌سازی مؤثر نباشد.
  2. تغییر ترتیب اشیاء: این الگوریتم چینش اصلی اشیا را تغییر می‌دهد که ممکن است باعث کاهش locality و افت عملکرد برنامه گردد.
  3. عدم بهینه‌سازی locality: چون اشیای مرتبط ممکن است در نقاط مختلف هیپ قرار بگیرند، دسترسی به آن‌ها کندتر می‌شود.


Lisp 2 Sliding Compaction (الگوریتم Lisp 2)
الگوریتم Lisp 2 یکی از معروف‌ترین روش‌های Mark-Compact Garbage Collection است که می‌تواند با اشیای متغیر در اندازه کار کند. این روش، در سه مرحله اجرا می‌شود و برخلاف Two-Finger Compaction، ترتیب اشیاء در حافظه حفظ می‌شود.

مراحل اجرای الگوریتم Lisp 2
بعد از مرحله Mark که اشیای زنده مشخص شدند، سه مرحله زیر اجرا می‌شود:
  • محاسبه مکان جدید هر شیء (Compute Locations)
دو اشاره‌گر داریم: scan که کل حافظه را اسکن می‌کند. free که محل جدید اشیای زنده را مشخص می‌کند. هر شیء زنده، آدرس مقصد خودش را در فیلد forwardingAddress ذخیره می‌کند. free مقدارش افزایش پیدا می‌کند تا فضای بعدی را برای اشیای زنده مشخص کند. اشیای مرده نادیده گرفته می‌شوند.
  • به‌روزرسانی اشاره‌گرها (Update References)
چون اشیاء جابجا می‌شوند، اشاره‌گرهای موجود در ریشه‌ها (Roots) و داخل اشیای زنده باید به مکان جدید هدایت شوند. این کار با استفاده از forwardingAddress انجام میشود.
جابجایی اشیای زنده به محل جدید (Relocate Objects)
اشیای زنده به مکان جدید منتقل می شوند. این مرحله از بالا به پایین اجرا می‌شود، یعنی ابتدا اشیای با آدرس‌های بالاتر به مکان‌های پایین‌تر منتقل می‌شوند.

compact():
    computeLocations(HeapStart, HeapEnd, HeapStart)
    updateReferences(HeapStart, HeapEnd)
    relocate(HeapStart, HeapEnd)

computeLocations(start, end, toRegion):
    scan ← start
    free ← toRegion
    while scan < end
        if isMarked(scan)
            forwardingAddress(scan) ← free
            free ← align(free + size(scan))
        scan ← nextObject(scan)

updateReferences(start, end):
    for each fld in Roots        /* update roots */
        ref ← *fld
        if ref ≠ null
            *fld ← forwardingAddress(ref)

    scan ← start
    while scan < end             /* update fields */
        if isMarked(scan)
            for each fld in Pointers(scan)
                if *fld ≠ null
                    *fld ← forwardingAddress(*fld)
        scan ← nextObject(scan)

relocate(start, end):
    scan ← start
    while scan < end
        if isMarked(scan)
            dest ← forwardingAddress(scan)
            move(scan, dest)
            unsetMarked(dest)
        scan ← nextObject(scan)

مزایا و معایب الگوریتم Lisp 2
مزایا:
  1. حفظ ترتیب اشیاء: در مقایسه با Two-Finger Compaction، این روش ترتیب اصلی اشیا را حفظ می‌کند.
  2. کاهش پراکندگی حافظه (Fragmentation): چون اشیا به یک سمت فشرده می‌شوند، هیچ فضای خالی باقی نمی‌ماند.
  3. پشتیبانی از اشیای متغیر در اندازه: برخلاف Two-Finger Compaction که فقط برای اشیای هم‌اندازه خوب بود، این روش برای همه نوع اشیاء کاربرد دارد.

معایب:
  1. نیاز به یک فیلد اضافی در هدر هر شیء: این فیلد برای ذخیره‌ی forwardingAddress استفاده می‌شود که در برخی سیستم‌ها می‌تواند فضای زیادی را اشغال کند.
  2. اجرای سه مرحله‌ای: نسبت به روش‌هایی مثل Two-Finger Compaction، که فقط دو مرحله دارد، این روش یک مرحله بیشتر دارد و می‌تواند کمی کندتر باشد.
  3. متکی به پردازش‌های متوالی: این الگوریتم حافظه نهان (Cache) را بهینه استفاده نمی‌کند، مگر اینکه پیش‌بارگذاری داده‌ها (Prefetching) انجام شود.