در عوض اجازه دهید تخصیص حافظه را در heap زمان اجرای C بررسی کنیم. در یک heap زمان اجرای C تخصیص حافظه برای یک شی به حرکت در میان ساختارهای داده از یک لیست پیوندی نیاز دارد. زمانی که یک بلاک حافظه با اندازه لازم پیدا شد این بلاک حافظه تقسیم میشود و شیی مذکور در آن ایجاد میشود و اشاره گرهای موجود در لیست پیوندی برای نگه داری در آن شیی تغییر داده میشوند. برای managed heap تخصیص حافظه برای یک شیی به معنای اضافه کردن یک مقدار به اشاره گر است. در حقیقت تخصیص حافظه به یک شیی در managed heap تقریبا به سرعت ایجاد یک متغییر در stack است! به علاوه در بیشتر heapها مانند heap زمان اجرای C حافظه در جایی اختصاص داده میشود که فضای خالی کافی یافت شود. بنابراین اگر چند شیی بلافاصله بعد از هم در برنامه ایجاد شوند، ممکن است این اشیا چندین مگابایت آدرس حافظه با هم فاصله داشته باشند ولی در managed heap ایجاد چند شیی بلافاصله بعد از هم باعث قرار گرفتن ترتیبی این اشیا در حافظه میشود.
در بیشتر برنامه ها وقتی برای یک شیی حافظه در نظر گرفته میشود که یا بخواهد با یک شیی دیگر ارتباط قوی داشته باشد یا بخواهد چندین بار در یک قطعه کد استفاده شود. برای مثال معمولا وقتی یک حافظه برای شیی BinaryWriter ایجاد شد بلافاصله بعد از آن یک حافظه برای FileStream گرفته شود. سپس برنامه از BinaryWriter استفاده میکند که در حقیقت به صورت درونی از شیی FileStream هم استفاده میکند. در یک محیط کنترل شده به وسیله Garbage Collector برای اشیای جدید به صورت متوالی فضا در نظر گرفته میشود که این عمل موجب افزایش راندمان بدلیل موقعیت ارجاعها میشود. به ویژه این مورد به این معنی است که مجموعه کارهای پروسه شما کمتر شده و این نیز متشابه قرار گرفتن اشیای مورد استفاده توسط برنامه در CPU Cache است.
تا کنون اینگونه به نظر میرسید که managed heap بسیار برتر از heap زمان اجرای C است و این نیز به دلیل سادگی پیاده سازی و سرعت آن است. اما نکته دیگری که اینجا باید در نظر گرفته شود این است که managed heap این توانایی ها را به این دلیل به دست می آورد که یک فرض بزرگ انجام میدهد و آن فرض این است که فضای آدرس و حافظه بینهایت هستند. به وضوح این فرض کمی خنده دار به نظر میرسد و مسلما managed heap باید یک مکانیسم ویژه ای را به کار برد تا بتواند این فرض را انجام دهد. این مکانیسم Garbage Collector نامیده میشود ، که در ادامه طرز کار آن شرح داده میشود.
زمانی که یک برنامه عملگر new را فراخوانی میکند ممکن است فضای خالی کافی برای شیی مورد نظر وجود نداشته باشد. heap این موضوع را با اضافه کردن حجم مورد نیاز به آدرس موجود در NextObjPtr متوجه میشود. اگر نتیجه از فضای در نظر گرفته شده برای برنامه تجاوز کرد heap پر شده است و Garbage Collector باید آغاز به کار کند.
مهم: مطالبی که ذکر شد در حقیقیت صورت ساده شده مسئله بود. در واقعیت یک Garbage Collection زمانی رخ می دهد که نسل صفر کامل شود. بعضی Garbage Collector ها از نسل ها استفاده می کنند که یک مکانیسم به شمار می رود و هدف اصلی آن افزایش کارایی است. ایده اصلی به این صورت است که اشیای تازه ایجاد شده نسل صفر به شمار می روند و اشیایی قدیمی تر در طول عمر برنامه در نسل های بالاتر قرار می گیرند. جداسازی اشیا و دسته بندی انها به نسل های مختلف می تواند به Garbage Collector اجازه دهد اشیایی موجود در نسل خاصی را به جای تمام اشیا مورد بررسی قرار دهد. در بخش های بعدی نسل ها با جزئیات تمام شرح داده می شوند. اما تا ان مرحله فرض می شود که Garbage collector وقتی رخ می دهد که heap پر شود.
الگوریتم Garbage Collection:
Garbage Collection بررسی می کند که ایا در heap شیی وجود دارد که دیگر توسط برنامه استفاده نشود. اگر چنین اشیای در برنامه موجود باشند حافظه گرفته شده توسط این اشیا آزاد میشود (اگر هیچ حافظه ای برای اشیای جدید در heap موجود نباشد خطای OutOfMemoryException توسط عملگر new رخ میدهد). اما چگونه Garbage Collector تشخیص میدهد که آیا برنامه یک متغیر را نیاز دارد یا خیر؟ همانطور که ممکن است تصور کنید این سوال پاسخ ساده ای ندارد.
هر برنامه دارای یک مجموعه از rootها است. یک root اشاره گری است به یک نوع داده ارجاعی. این اشاره گر یا به یک نوع داده ارجاعی در managed heap اشاره میکند یا با مقدار null مقدار دهی شده است.برای مثال تمام متغییرهای استاتیک و یا عمومی(Global Variables) یک root به شمار میروند . به علاوه هر متغیر محلی که از نوع ارجاع باشد و یا پارامترهای توابع در stack نیز یک root به شمار میروند. در نهایت، درون یک تابع، یک ثبات CPU که به یک شیی از نوع ارجاع اشاره کند نیز یک root به شمار میرود.
زمانی که کامپایلر JIT یک کد IL را کامپایل میکند علاوه بر تولید کدهای Native یک جدول داخلی نیز تشکیل میدهد. منطقا هر ردیف از این جدول یک محدوده از بایتهای آفست را در دستورات محلی CPU برای تابع نشان میدهند و برای هر کدام از این محدوده ها یک مجموعه از آدرسهای حافظه یا ثباتهای CPU را که محتوی rootها هستند مشخص میکند. برای مثال جدول ممکن است مانند جدول زیر باشد:
Sample of a JIT compiler-produced table showing mapping of native code offsets to a method's roots
Starting Byte Offset Ending Byte Offset Roots
0x00000000 0x00000020 this, arg1, arg2, ECX, EDX
0x00000021 0x00000122 this, arg2, fs, EBX
0x00000123 0x00000145 fs
اگر یک Garbage Collector زمانی که کدی بین آفست 0x00000021 و 0x00000122 در حال اجرا است آغاز شود، Garbage Collector میداند که پارامترهای thisو arg2 و متغییرهای محلی fs و ثبات EBX همه root هستند و به اشیایی درون heap اشاره میکنند که نباید زباله تلقی شوند. به علاوه Garbage Collector میتواند بین stack حرکت کند و rootها را برای تمام توابع فراخوانی شده با امتحان کردن جدول داخلی هر کدام از این توابع مشخص کند. Garbage Collector وسیله دیگری را برای بدست آوردن مجموعه rootهای نگه داری شده توسط متغیرهای ارجاعی استاتیک و عمومی به کار میبرد.
نکته: در جدول بالا توجه کنید که آرگومان arg1 تابع بعد از دستورات CPU در آفست 0x00000020 دیگر به چیزی اشاره نمیکند و این امر بدین معنی است که شییی که arg1 به آن اشاره میکند هر زمان بعد از اجرای این دستورات میتواند توسط Garbage Collector جمع آوری شود (البته فرض بر اینکه هیچ شیی دیگری در برنامه به شیی مورد ارجاع توسط arg1 اشاره نمیکند). به عبارت دیگر به محض اینکه یک شیی غیر قابل دسترسی باشد برای جمع آوری شدن توسط Garbage Collector داوطلب میشود و به همین علت باقی ماندن اشیا تا پایان یک متد توسط Garbage Collector تضمین نمیشود.
با وجود این زمانی که یک برنامه زمانی که در حالت debug اجرا شده باشد و یا ویژگی System.Diagnostics.DebuggableAttribute به اسمبلی برنامه اظافه شده باشد و یا اینکه پارامتر isJITOptimizeDisabled با مقدار true در constructor برنامه تنظیم شده باشد، کامپایلر JIT طول عمر تمام متغیرها را، چه از نوع ارجاعی و چه از نوع مقدار، تا پایان محدوده شان افزایش میدهد که معمولا همان پایان تابع است( کامپایلر C# مایکروسافت یک سوییچ خط فرمان به نام /debug را ارائه میدهد که باعث اضافه شدن DebuggableAttribute به اسمبلی میشود و نیز پارامتر isJITOptimizeDisabled را نیز true میکند). این افزایش طول عمر از جمع آوری شدن متغیرها توسط Garbage Collector در محدوده اجرایی آنها در طول برنامه جلوگیری میکند و این عمل فقط در زمان debug یک برنامه مفید واقع میشود.
زمانی که Garbage Collector شروع به کار میکند، فرض میکند که تمام اشیای موجود در heap زباله هستند. به عبارت دیگر فرض میکند که هیچ کدام از rootهای برنامه به هیچ شیی در heap اشاره نمیکند. سپس Garbage Collector شروع به حرکت در میان rootهای برنامه میکند و یک گراف از تمام rootهای قابل دسترسی تشکیل میدهد. برای مثال Garbage Collector ممکن است یک متغیر عمومی را که به یک شیی در heap اشاره میکند موقعیت یابی کند. شکل زیر یک heap را با چندین شیی تخصیص داده شده نشان میدهد. همانطور که در شکل مشخص است rootهای برنامه فقط به اشیای A و C و D و F به طور مستقیم اشاره میکنند. بنابراین تمام این اشیا از اعضای گراف محسوب میشوند. زمان اظافه کردن شیی D، Garbage Collector متوجه میشود که این شیی به شیی H اشاره میکند، بنابراین شیی H نیز به گراف برنامه اضافه میشود و به همین ترتیب Garbage Collector تمام اشیای قابل دسترسی در heap را مشخص میکند.