عنوان:

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


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۳/۱۱/۱۴ ۲۰:۲۰
آدرس: www.dntips.ir
اهداف سیستم‌های مدیریت حافظه
سه وظیفه اصلی را می‌توان متصور بود:
  1. تخصیص حافظه (Allocation) برای اشیای جدید.
  2. شناسایی اشیای زنده (Identify Live Objects) که هنوز توسط برنامه استفاده می‌شوند.
  3. آزادسازی حافظه‌ی مرده (Reclaim Dead Objects)؛ اشیایی که دیگر استفاده نمی‌شوند.

که Mark-Sweep عمدتاً بر روی شناسایی اشیای زنده و آزادسازی حافظه مرده تمرکز دارد. تخصیص حافظه معمولاً توسط بخشی به اسم Allocator مدیریت می‌شود.


جمع‌آوری زباله به روش Mark-Sweep

Mark-Sweep یکی از ساده‌ترین و قدیمی‌ترین الگوریتم‌های جمع‌آوری زباله‌‌است که اولین بار توسط John McCarthy در سال 1960 معرفی شد. این الگوریتم بر اساس مفهوم دسترسی‌پذیری ارجاعی (Pointer Reachability) کار می‌کند که در قسمت‌های قبل توضیح داده‌شد.
این الگوریتم دو مرحله دارد:
مرحله‌ی Mark (علامت‌گذاری): Collector از ریشه‌ها (Roots) شروع می‌کند که شامل متغیرهای استاتیک، استک Thread ها و غیره هستند. سپس تمام اشیایی را که از طریق زنجیره‌ای از ارجاعات قابل دسترسی هستند، علامت‌گذاری می‌کند.
مرحله Sweep (جارو کردن): Collector کل هیپ را بررسی می‌کند. هر شیءای که علامت‌گذاری نشده باشد، به عنوان زباله (Garbage) شناسایی می‌شود و حافظه‌اش آزاد می‌گردد.

الگوریتم Mark-Sweep در اصل دو مرحله‌ای‌است؛ ولی در پیاده‌سازی واقعی، این مراحل به چند بخش جداگانه تقسیم می‌شوند تا خواناتر و مدیریت‌شده‌تر باشند.


تخصیص حافظه و اجرای جمع‌آوری زباله

New():
	ref ← allocate()
	if ref = null
    	collect()  	/* Heap is full */
    	ref ← allocate()
	if ref = null
    	error "Out of memory"  /* Heap is still full */
	return ref
در اینجا، ابتدا سعی می‌شود حافظه‌ای برای شیء جدید اختصاص داده شود. قبل از اجرای مرحله‌های Mark و Sweep، بررسی می‌کنیم که آیا حافظه‌ای موجود هست یا خیر. این مرحله جزو مراحل اصلی جمع‌آوری زباله نیست؛ ولی برای اجرای کارکرد درست سیستم ضروری است.
در این مرحله بررسی می‌کند که آیا مقداری فضای خالی در هیپ موجود است و اگر فضا کم باشد و تخصیص ناموفق باشد (یعنی فضای هیپ پر شده باشد)، collect اجرا می‌گردد که مراحل Mark و Sweep را انجام می‌دهد. در نهایت، اگر بعد از اجرای collect همچنان فضای کافی موجود نباشد، خطای "Out of memory" صادر می‌شود. در نهایت، شیء اختصاص داده‌شده، بازگردانده می‌شود.


مرحله‌ی نشانه‌گذاری (Marking)

markFromRoots():
	initialise(worklist)
	for each fld in Roots
    	ref ← fld
    	if ref ≠ null && not isMarked(ref)
        	setMarked(ref)
        	add(worklist, ref)
	mark()

initialise(worklist):
	worklist ← empty

mark():
	while not isEmpty(worklist)
    	ref ← remove(worklist)  /* ref is marked */
    	for each fld in Pointers(ref)
        	child ← *fld
        	if child ≠ null && not isMarked(child)
            	setMarked(child)
            	add(worklist, child)
این اولین مرحله‌ی واقعی از الگوریتم Mark-Sweep می باشد که وظیفه‌اش پیدا کردن و علامت‌گذاری اشیای زنده‌است.
در این مرحله از ریشه‌های برنامه (Roots) شروع می‌کند (مثل متغیرهای استاتیک، استک، و غیره). همه‌ی اشیایی را که از این ریشه‌ها قابل دسترسی هستند، علامت‌گذاری می‌کند. هر شیءای که در دسترس باشد و هنوز علامت‌گذاری نشده باشد، در لیست کاری (worklist) قرار داده می‌شود. این کار باعث می‌شود که در مرحله‌ی بعد (Sweep) فقط اشیای بدون علامت حذف شوند. الگوریتم به‌صورت بازگشتی به تمام اشیای متصل دسترسی پیدا کرده و آن‌ها را علامت‌گذاری می‌کند. نتیجه‌ی این مرحله این است که همه‌ی اشیای زنده (reachable) علامت‌گذاری می‌شوند. این مرحله تضمین می‌کند که اشیای هنوز زنده، علامت‌گذاری شوند و برای حذف انتخاب نشوند.


مرحله‌ی جاروب (Sweeping)

sweep(start, end):
	scan ← start
	while scan < end
    	if isMarked(scan)
        	unsetMarked(scan)
    	else
        	free(scan)
    	scan ← nextObject(scan)
این مرحله، دومین مرحله‌ی اصلی Mark-Sweep است که در آن اشیای بدون علامت، حذف می‌شوند.
در این مرحله‌، جاروب از ابتدای هیپ (start) تا انتهای آن (end) انجام می‌شود. کل حافظه را اسکن می‌کند و هر شیء‌ای را که علامت‌گذاری نشده‌است، آزاد می‌کند. اشیای علامت‌گذاری شده را به حالت اولیه برمی‌گرداند (حذف علامت برای چرخه‌ی بعدی) و حافظه‌ی آزادشده را برای تخصیص‌های جدید آماده می‌کند. نتیجه‌ی این مرحله این است که حافظه‌ی اشیای مرده، آزاد شده و می‌توان مجدداً از آن استفاده کرد.


مزایای Mark-Sweep
  1. سادگی: پیاده‌سازی و درک این الگوریتم نسبتاً آسان است.
  2. انعطاف‌پذیری: می‌تواند برای انواع مختلفی از برنامه‌ها استفاده شود.

معایب Mark-Sweep
  1. وقفه‌ی طولانی: چون Mutator باید متوقف شود، زمان توقف ممکن است برای برنامه‌های تعاملی مشکل‌ساز شود.
  2. هزینه‌ی مرور کل هیپ: مرحله‌ی Sweep باید تمام هیپ را بررسی کند که ممکن است برای هیپ‌های بزرگ گران باشد.

در حالیکه روش‌های مستقیم، مثل Reference Counting، لایو بودن یک شیء را مستقیماً و بدون نیاز به بررسی گراف کل اشیاء تشخیص می‌دهند، در این الگوریتم، Mutator حین اجرا متوقف می‌شود که اصطلاحا به آن Stop-the-World گفته می‌شود. در این حالت، همه‌ی Threadهای Mutator متوقف می‌شوند و Collector کارش را انجام می‌دهد. این کار برای این است که مطمئن شویم، Collector می‌تواند تمام ریشه‌ها را پیدا کند. Mutator بعد از جمع‌آوری زباله، به درستی از همانجایی که متوقف شده بود، ادامه می‌دهد. به این نقاط که باعث توقف‌ها می شوند GC Safe-Points گفته می شوند. این نقاط باید جاهایی باشند که Collector بتواند بدون مشکل، شروع به‌کار کند. مثلاً، هنگام تخصیص اشیاء جدید یا جاهایی که یک Thread ممکن است معلق شود.