۱۳۹۰ اردیبهشت ۳۰, جمعه

Garbage Collector در .NET

طی دو ماه گذشته، بارها دوستان برنامه نویس سوالاتی مبنی بر چگونگی کارکرد Grabage Collector (GC) در .Net پرسیده اند و اینکه چرا GC در برخی موارد، حتی وقتی به طور صریح فراخوانی می شود، توان پاک کردن حافظه اختصاص داده شده به برخی از کلاس ها یا متغیر ها را ندارد؟

به همین دلیل تصمیم گرفتم در جهت روشن تر شدن روش کار GC مطلبی بنویسم و در انتها به پاسخ این سوالات برسم.

اغلب برنامه نویسانی که کار با زبانهایی که کد unmanaged تولید می کنند را تجربه کرده اند (مثل ANSI C++ و C) ، برای کار با هر نوع داده ای و هر ساختار باید دو عمل اصلی را انجام می دادند: اختصاص حافظه و آزاد سازی آن. این اعمال به ظاهر ساده، به صورت بالقوه و گاهی در عمل منشا پیچیده ترین و گیج کننده ترین اشکالات یا به اصطلاح باگ ها در نرم افزارها هستند. کافی است پس از پایان استفاده از بخشی از حافظه، آن را آزاد نکنید و یا اقدام به استفاده از حافظه ای کنید که پیش از آن آزاد شده است. اشکال عمده این نوع خطاها این است که نتیجه آن به آسانی و به شکل یک پیغام خطای زمان اجرا و یا اشکال منطقی قابل ردیابی نبوده و همیشه تحت شرایط یکسان تکرار نمی شوند. با هر بار اجرای برنامه و یا تحت شرایط مختلف دیگر، اتفاقات عجیب و متفاوتی را می شود از برنامه انتظار داشت. با ظهور زبانهای managed مثل جاوا و  زبانهای مبتنی بر .NET مدیریت حافظه بر عهده واسط اجرا کننده کد گذاشته شد. این واسط در جاوا JRE (Java Runtime Environment) و در .NET، CLR (Common Library Runtime) است. اصولا زبانهای managed به زبانهایی می گویند که مدیریت حافظه در آنها به طور اتوماتیک و به دور از چشم برنامه نویس انجام می شود. مزیت این نوع زبانها این است که  توسط آنها با سرعت بالاتر و خطای کمتر می توان نرم افزار جدید تولید نمود. در عین حال عیب عمده این زبانها سرعت پایین نرم افزارهای تولید شده توسط آنهاست.  به دلیل سربار بالای عملیات مدیریت حافظه اتوماتیک، این سرعت کم اجتناب ناپذیر است و همین امر آنها را برای تولید نرم افزارهایی که به سرعت بالای محاسبات نیاز دارند، مثل بازیهای کامپیوتری، نامناسب می سازد.

در .NET یکی از وظایف اصلی مدیریت حافظه را Garbage Collector (GC) بر عهده دارد. کار GC اختصاص و آزادسازی اتوماتیک حافظه به اشیا است. با هر بار فراخوانی کلمه new برای استفاده از یک شیء مقداری حافظه توسط GC به آن اختصاص داده شده و پس از پایان کار با آن شیء این حافظه باز پس گرفته می شود. اما نکته ای که در این مطلب به رویش تمرکز خواهد شد، چگونگی باز پس گیری حافظه است.

به طور کلی در .NET دو نوع ارجاع به اشیاء وجود دارد. ارجاع قوی (Strong Reference) و ارجاع ضعیف (Weak Reference). همه ارجاع ها به طور پیش فرض قوی هستند مگر آنکه صراحتا ضعیف تعریف شوند. همانطور که می دانید یک ارجاع با کلمه کلیدی ref در C# و ByRef در VB مشخص می شود و مفهوم آن این است که تنها آدرس متغیر مورد نظر به عنوان پارامتر به یک تابع ارسال می شود و نه یک کپی از مقدار آن. این به آن معنا است که یک متغیر موجود در یک بلوک مشخص از حافظه بین چند تابع به اشتراک گذاشته می شود. علاوه بر کلمه کلیدی ref، ارجاع به صورت دیگری هم می تواند وجود داشته باشد. بسیاری از انواع داده ای از نوع ارجاعی هستند، به این معنی که از همان ابتدا که شیء جدید ساخته می شود، تنها آدرس محل قرار گیری آن شیء، در متغیر مورد نظر قرار می گیرد و خود شیء در Heap نگهداری می شود. تمام اشیائی که برای مقدار دهی اولیه (Initialization) نیاز به کلمه کلیدی new دارند از این نوع هستند. زمانی که این نوع داده ها را به عنوان پارامتر به یک تابع ارسال می کنید، به طور اتوماتیک تنها آدرس آنها ارسال می شود. همه این نوع ارجاعات ارجاع قوی محسوب می شوند.

اگر یک شیء در حافظه وجود داشته باشد، از حافظه پاک نخواهد شد مگر اینکه همه توابعی که به طور قوی به آن ارجاع داده شده اند، کارشان تمام شده و از حافظه خارج شوند. این جمله به آن معنی است که حتی اگر تابع سازنده شیء مورد نظر از حافظه خارج شود ولی تابع دیگری وجود داشته باشد که هنوز با ارجاع قوی در حال استفاده از آن متغیر باشد، متغیر در حافظه باقی می ماند. در مقابل از ارجاع های ضعیف در هنگام خالی سازی حافظه صرف نظر می شود و اگر تنها ارجاع های موجود به یک شیء از نوع ارجاع ضعیف باشند، به گونه ای عمل می شود که گویی ارجاعی به شیء وجود ندارد و شیء از heap پاک می شود. کلیه این قوانین در مورد ارجاع های قوی و ضعیف، توسط GC و به هنگام پاک سازی heap اعمال می شود.

GC به هر شیء یک عدد صحیح به عنوان شماره نسل اختصاص می دهد. سه نسل برای اشیاء در نظر گرفته می شود. نسل 0، 1 و 2. هر شیء به هنگام ایجاد شدن در نسل 0 قرار می گیرد. GC برای پاک سازی هر نسل از اشیا در فواصل زمانی مشخص اقدام می کند. تعداد دفعات تکرار عمل پاک سازی برای نسل 0 بیشترین است. به این معنی که GC در فواصل زمانی کوتاهتری اقدام به جمع آوری اشیاء نسل 0 می کند. پس از آن و در فواصل طولانی تری نسل 1 و سپس نسل 2. هرگاه GC برای پاک سازی شیءی از حافظه اقدام می کند ولی آن شیء به دلیل وجود یک ارجاع قوی به آن غیر قابل پاک کردن است، یک شماره به نسل آن اضافه می شود. مگر اینکه شی در نسل 2 باشد که در این صورت شماره نسل آن تغییر نمی کند. این به آن دلیل است که هرگاه شیءی در یک اقدام به پاک سازی هنوز قابل پاک کردن نبود به احتمال زیاد مدت زمان بیشتری به آن نیاز خواهد بود و ارجاعات قوی بیشتری به آن وجود دارد و یا توابع ارجاع داده شده به آن کند تر اجرا می شوند. در این صورت برای پایین تر آوردن سربار اجرای GC و بالاتر بردن سرعت اجرای نرم افزار، یک شماره به نسل آن اضافه می شود تا در فواصل طولانی تری اقدام به پاک کردن فضای اختصاص داده شده به آن متغیر در heap شود.

این عمده ترین دلیلی است که GC ممکن است در هر اقدام به پاک سازی، لزوما موفق به پاک کردن شیء و آزاد سازی بلوک حافظه مربوط به آن نشود، حتی اگر به طور صریح و توسط تابع GC.Collect() در کد برنامه اقدام به پاک سازی heap شود.

هیچ نظری موجود نیست:

ارسال یک نظر