مشاهده نسخه کامل
: مديريت حافظه در Net.
B O L O T
11-06-2007, 12:15
در اين مقاله سعي شده است چگونگي ساخته شدن Objectها توسط برنامه ها , چگونگي مديريت طول عمر اشيا در .NET و چگونگي آزاد شدن حافظه هاي گرفته شده توسط Garbage Collector شرح داده شود.
درک مباني کار Garbage Collector:
هر برنامه به نحوي از منابع مشخصي استفاده ميکند. اين منابع ميتوانند فايلها، بافرهاي حافظه، فضاهاي صفحه نمايش، ارتباطات شبکه اي، منابع بانک اطلاعاتي و مانند اينها باشند. در حقيقت در يک محيط شيي گرا هر نوع داده تعريف شده در برنامه معرف يك سري منابع مربوط به آن برنامه هستند. براي استفاده از هر نوع از اين داده ها لازم است که براي ارايه آن نوع مقداري حافظه تخصيص داده شود. موارد زير براي دسترسي به يک منبع مورد نياز است:
1)تخصيص حافظه براي نوع داده اي که منبع مورد نظر را ارايه ميدهد. اين تخصيص حافظه با استفاده از دستور newobj در زبان IL صورت ميگيرد که اين دستور از ترجمه دستور new در زبانهايي مثل C# و Visual Basic و ديگر زبانهاي برنامه نويسي ايجاد ميشود.
2)مقداردهي اوليه حافظه براي تنظيم حالت آغازين(Initial state) منابع و قابل استفاده کردن آن. توابع Constructor در اين نوع داده ها مسئول اين تنظيمات براي ايجاد اين حالت آغازين هستند.
3)استفاده از منابع با دسترسي به اعضاي موجود در نوع داده.
4)از بين بردن حالت کلي منابع براي پاک کردن آن.
5)آزادسازي حافظه. Garbage Collector مسئول مطلق اين مرحله به شمار مي رود.
اين نمونه به ظاهر ساده يکي از ريشه هاي اصلي خطاهاي ايجاد شده در برنامه نويسي به شمار ميرود. مواقع زيادي پيش مي آيد که برنامه نويس آزادسازي يک حافظه را وقتي ديگر مورد نياز نيست فراموش مي کند. مواقع زيادي پيش مي آيد که برنامه نويس از يک حافظه که قبلا آزاد شده استفاده کند.
اين دو باگ برنامه ها از اکثر آنها بدتراند زيرا معمولا برنامه نويس نميتواند ترتيب يا زمان به وجود آمدن اين خطاها را پيش بيني کند. براي ديگر باگها شما ميتوانيد با مشاهده رفتار اشتباه يک برنامه آن را به سادگي تصحيح کنيد. اما اين دو باگ موجب نشت منابع (Resource Leak) (مصرف بيجاي حافظه) و از بين رفتن پايداري اشيا ميشوند که کارايي برنامه را در زمانهاي مختلف تغيير ميدهد. براي کمک به يک برنامه نويس براي تشخيص اين نوع خطاها ابزارهاي ويژه اي مانند Windows Task Manager و System Monitor ActiveX Control و NuMega Bounds Checker و ... طراحي شده اند.
يک مديريت منبع مناسب بسيار مشکل و خسته کننده است. اين مورد تمرکز برنامه نويس را بر روي مطلب اصلي از بين ميبرد. به همين دليل نياز به يک مکانيسم که مديريت حافظه را به بهترين نحو انجام دهد در اين زمينه به وضوح احساس ميشد. در پلتفرم .NET اين امر توسط Garbage Collector انجام ميشود.
Garbage Collection کاملا برنامه نويس را از کنترل استفاده از حافظه و بررسي زمان آزادسازي آن راحت ميکند. اگرچه Garbage Collector درمورد منابع ارائه شده توسط نوع داده در حافظه هيچ چيز نميداند، يعني Garbage Collector نميداند چه طور ميتواند مرحله 4 از موارد بالا را انجام دهد: از بين بردن حالت کلي منابع براي پاک کردن آن. برنامه نويس بايد کدهاي مربوط به اين قسمت را انجام دهد چون او ميداند بايد چه گونه حافظه را به درستي و کاملا آزاد کند. البته Garbage Collector ميتواند در اين زمينه نيز قسمتهايي از کار را براي برنامه نويس انجام دهد.
البته، بيشتر نوع داده ها، مانند Int32، Point ، Rectangle ، String ،ArrayList و SerializationInfo از منابعي استفاده مي کنند که احتياجي به نوع ويژه اي از آزادسازي حافظه ندارند. براي مثال منابع يک شئي از نوع Point به راحتي و با نابود کردن فيلدهاي X و Y در حافظه شيي آزاد ميشود.
از طرف ديگر، يک نوع داده که منابع مديريت نشده اي را ارائه ميدهد، مانند يک فايل، يک ارتباط بانک اظلاعاتي، يک سوکت، يک Bitmap، يک آيکون و مانند اينها هميشه به اجراي مقداري کد ويژه براي آزاد کردن حافظه گرفته شده نياز دارند.
CLR نياز دارد که حافظه تمام منابع از يک heap مخصوص که managed heap ناميده ميشود تخصيص داده شود. اين heap شبيه heap زمان اجراي C است و فقط از يک لحاظ متفاوت است و آن اين است که در اين heap شما هيچ وقت حافظه تخصيص داده شده را آزاد نميکنيد. در حقيقت اشيا موجود در اين heap وقتي ديگر نيازي به آنها نباشد آزاد ميشوند. اين مورد اين سوال را ايجاد ميکند که چگونه managed heap متوجه ميشود که ديگر نيازي به يک شيي خاص نيست؟
چندين الگوريتم از Garbage Collector در حال حاضر در مرحله آزمايش هستند و هر کدام از اين الگوريتمها براي يک محيط خاص و نيز براي کسب بهترين راندمان بهينه سازي شده اند. در اين مقاله روي الگوريتم Garbage Collector استفاده شده در Microsoft .NET Framework CLR متمرکز شده است.
زماني که يک پروسه مقداردهي اوليه(Initialize) ميشود، CLR يک قسمت پيوسته از آدرس حافظه را براي آن اختصاص ميدهد اين آدرس فضاي حافظه managed heap ناميده ميشود. اين heap همچنين يک اشاره گر مخصوص هم دارد که ما از اين به بعد آن را NextObjPtr مي ناميم. اين اشاره گر مکان قرار گيري شيي بعدي را در heap مشخص ميکند. در ابتدا اين اشاره گر به آدرس ابتداي فضاي گرفته شده براي managed heap اشاره ميکند.
دستور newobj در زبان IL باعث ايجاد يک شيي جديد ميشود. بيشتر زبانها از جمله C# و Visual Basic براي درج اين دستور در کد IL عملگر new را در برنامه ارائه ميدهند. اين دستور IL باعث ميشود که CLR مراحل زير را انجام دهد:
1)محاسبه تعداد بايتهاي مورد نياز براي اين نوع داده
2) اضافه کردن بايتهاي مورد نياز براي overhead شيي. هر شيي دو فيلد overhead دارد: يک اشاره گر به جدول تابع و يک SyncBlockIndex. در سيستمهاي 32بيتي، هر کدام از اين فيلدها 32 بيت هستند، که 8 بايت را به هر شيي اضافه مي کند. در سيستم هاي 64 بيتي، هر کدام از اين فيلدها 64 بيت است که 16 بايت را براي هر شيي اضافه مي کند.
3)سپس CLR چک ميکند که حافظه مورد نياز براي شيي جديد در managed heap موجود باشد. اگر فضاي کافي موجود باشد اين شيي در آدرسي که NextObjPtr به آن اشاره ميکند ايجاد ميشود. تابع constructor شيي مذکور فراخواني ميشود (اشاره گر NextObjPtr به عنوان پارامتر this به constructor فرستاده ميشود) و دستور newobj آدرس شيي ايجاد شده را برميگرداند. درست قبل از اينکه آدرس برگردانده شود، NextObjPtr به بعد از شيي ايجاد شده پيشروي ميکند و مثل قبل آدرسي که بايد شيي بعدي در آن قرار گيرد را در خود نگه ميدارد.
شکل زير يک managed heap را که سه شيي Aو B و C را درخود نگه ميدارد را نشان ميدهد. اگر يک شيي جديد ايجاد شود اين شيي دقيقا در جايي که NextObjPtr به آن اشاره ميکند قرار ميگيرد
B O L O T
11-06-2007, 12:17
در يک 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 پر شود.
B O L O T
11-06-2007, 12:17
الگوريتم 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 را مشخص ميکند.
B O L O T
11-06-2007, 12:18
زماني که اين بخش از گراف کامل شد Garbage Collector، rootهاي بعدي را چک ميکند و مجددا اشيا را بررسي ميکند. در طول اينکه Garbage Collector از يک شيي به يک شيي ديگر منتقل ميشود، اگر سعي کند که يک شيي تکراري را به گراف اضافه کند، Garbage Collector حرکت در آن مسير را متوقف ميکند. اين نوع رفتار دو هدف را دنبال ميکند: اول اينکه چون Garbage Collector از هيچ شيي دو بار عبور نميکند راندمان برنامه را به شکل قابل توجهي افزايش ميدهد. دوم اينکه هرچقدر هم در برنامه ليستهاي پيوندي دايره اي از اشيا موجود باشند ، Garbage Collector در حلقه هاي بينهايت نمي ماند.
زماني که تمام rootها بررسي شدند، گراف Garbage Collector محتوي تمام اشيايي است که به نحوي از طريق rootهاي برنامه قابل دسترسي مي باشند و هر شيي که در گراف نباشد به اين معني است که توسط برنامه قابل دسترسي نيست و يک زباله محسوب ميشود. بعد از اين Garbage Collector به صورت خطي heap را طي ميکند و دنبال بلاکهاي پيوسته از زباله هاي مشخص شده توسط گراف ميگردد (که هم اکنون فضاي خالي محسوب ميشوند). اگر بلاکهاي کوچکي پيدا شوند Garbage Collector اين بلاکها را به همان حال قبلي رها ميکند.
اگر يک بلاک پيوسته وسيع توسط Garbage Collector يافت شد، در اين حال، Garbage Collector اشياي غير زباله را به سمت پايين حافظه heap شيفت ميدهد (و اين کار با تابع استاندارد memcopy انجام ميشود) و به اين طريق heap را فشرده ميکند. طبيعتا حرکت دادن اشيا به سمت پايين در heap باعث نامعتبر شدن تمام اشاره گرهاي موجود براي آن اشيا ميشود. به علاوه، اگر شيي محتوي اشاره گري به شيي ديگري بود Garbage Collector مسئول تصحيح اين اشاره گرها ميشود. بعد از اين که اين عمل فشرده سازي روي heap انجام شد NextObjPtr به آخرين شيي غير زباله اشاره ميکند. شکل زير يک managed heap را بعد از اتمام کار Garbage Collector نشان ميدهد.
Garbage Collector يک افزايش بازدهي قابل توجهي را ايجاد ميکند. اما به ياد داشته باشيد زماني Garbage Collector شروع به کار ميکند که نسل صفر کامل شود و تا آن زمان managed heap به صورت قابل توجهي سريعتر از heap زمان اجراي C است. در نهايت Garbage Collector مربوط به CLR روش بهينه سازي را ارائه مي دهد که راندمان کاري Garbage Collector را مقدار زيادي افزايش مي دهد.
کد زير نشان ميدهد که چگونه به اشيا حافظه تخصيص داده ميشود و آنها مديريت ميشوند:
کد:
Class App
{
static void Main()
{
// ArrayList object created in heap, a is now a root
ArrayList a = new ArrayList();
// Create 10000 objects in the heap
for(Int32 x=0;x<10000;x++
{
a.Add(new Object()); // Object created in heap
}
// Right now, a is a root (on the thread's stack). So a is
// reachable and the 10000 objects it refers to
// are reachable.
Console.WriteLine(a.Length);
// After a.Length returns, a isn't referred to in the code
// and is no longer a root. If another thread were to start
// a garbage collection before the result of a.Length were
// passed to WriteLine, the 10001 objects would have their
// memory reclaimed.
Console.WriteLine("End Of Method");
}
}
نکته: اگر فکر ميکنيد که Garbage Collector يک تکنولوژي با ارزش محسوب ميشود، ممکن است تعجب کنيد که چرا در ANSI C++ قرار نمي گيرد. دليل اين مورد اين است که Garbage Collector احتياج دارد که rootهاي موجود در برنامه را تعيين هويت کند و نيز بايد بتواند تمام اشاره گرهاي اشيا را پيدا کند. مشکل با C++ مديريت نشده اين است که اين برنامه تغيير نوع يک اشاره گر را از يک نوع به يک نوع ديگر مجاز ميداند و هيچ راهي براي فهميدن اين که اين اشاره گر به چه چيز اشاره ميکند وجود ندارد. در CLR، managed heap هميشه ميداند که نوع واقعي يک شيي چيست و از اطلاعات metadata براي مشخص کردن اينکه کدام عضوها از يک شي به اشياي ديگر اشاره مي کنند استفاده مي کند.
B O L O T
11-06-2007, 12:19
نسلها :
همانطور که پيشتر ذکر شد نسلها مکانيسمي درون CLR Garbage Collector به شمار ميرود که هدف اصلي آن بهبود کارايي برنامه است. يک Garbage Collector که با مکانيسم نسلها کار ميکند (همچنين به عنوان Garbage Collector زودگذر هم ناميده ميشود) فرضهاي زير را براي کار خود در نظر ميگيرد:
1)هر چه يک شيئ جديدتر ايجاد شده باشد طول عمر کوتاهتري هم خواهد داشت.
2)هر چه يک شيئ قديميتر باشد طول عمر بلندتري هم خواهد داشت.
3)جمع آوري قسمتي از heap سريعتر از جمع آوري کل آن است.
مطالعات زيادي معتبر بودن اين فرضيات را براي مجموعه بزرگي از برنامه هاي موجود تاييد کرده اند و اين فرضيات بر طرز پياده سازي Garbage Collector تاثير داشته اند. در اين قسمت طرز کار اين اين مکانيسم شرح داده شده است.
زماني که يک managed heap براي بار اول ايجاد ميشود داراي هيچ شيئ نيست. اشيايي که به heap اظافه شوند در نسل صفر قرار ميگيرند. اشياي موجود در نسل صفر اشياي تازه ايجاد شده اي هستند که تا کنون توسط Garbage Collector بررسي نشده اند. تصوير زير يک برنامه را که تازه آغاز به کار کرده است نشان ميدهد که داراي پنج شيئ است(از A تا E ). بعد از مدتي اشياي C و E غير قابل دسترسي ميشوند.
B O L O T
11-06-2007, 12:19
زماني که CLR آغاز به کار ميکند يک مقدار نهايي را براي نسل صفر در نظر ميگيرد که به طور پيش فرض 256 کيلوبايت است(البته اين مقدار مورد تغيير قرار ميگيرد). بنابراين اگر شيي بخواهد ايجاد شود و در نسل صفر فضاي کافي وجود نداشته باشد Garbage Collector آغز به کار ميکند. اجازه دهيد تصور کنيم که اشياي A تا E 256 کيلوبايت فضا اشغال کرده اند زماني که شيي F بخواهد تشکيل شود Garbage Collector بايد آغاز به کار کند. Garbage Collector تشخيص ميدهد که اشياي C و E زباله محسوب ميشوند و بنابراين شيي D بايد فشرده شود بنابراين اين شيي به کنار شيي B ميرود. اشيايي که بعد از اين مرحله باقي ميمانند (اشياي A و B و D) وارد نسل يک ميشوند. اشياي موجود در نسل يک به اين معني هستند که يک بار توسط Garbage Collector بررسي شده اند. Heap برنامه مفروض بعد از اولين مرحله به صورت تصوير زير در مي آيند.
بعد از يک بار اجراي Garbage Collector هيچ شيي در نسل صفر باقي نمي ماند. مثل هميشه اشيايي که بعد از اين ايجاد مي شوند به نسل صفر اضافه ميشوند. شکل زير اجراي برنامه و به وجود آمدن اشياي F تا K را نشان ميدهد. به علاوه در طول اجراي برنامه اشياي B و H و J غير قابل استفاده شده اند و حافظه گرفته شده توسط آنها بايد آزاد شود.
B O L O T
11-06-2007, 12:20
حال فرض کنيم با تخصيص حافظه براي شي L در نسل صفر مقدار داده هاي موجود در اين نسل از 256کيلوبايت فراتر رود. چون نسل صفر به سرحد خود رسيده است Garbage Collector بايد آغاز به کار کند. زماني که عمل Garbage Collection آغاز ميشود Garbage Collector بايد تصميم بگيرد که کدام نسل بايد مورد بررسي قرار گيرد. پيشتر ذکر شد که زماني که CLR آغاز به کار ميکند براي نسل صفر 256 کيلوبايت فضا اختصاص ميدهد. همچنين CLR يک سرحد نيز براي نسل يک درنظر ميگيرد. فرض ميکنيم اين مقدار فضا شامل 2 مگابايت باشد.
زماني که عمل Garbage Collection انجام ميشود، Garbage Collector همچنين بررسي ميکند که چه مقدار فضا توسط نسل يک اشغال شده است. در اين حالت نسل يک فضايي کمتر از 2 مگابايت را اشغال کرده است بنابراين Garbage Collector فقط نسل صفر را مورد بررسي قرار ميدهد. يک بار ديگر فرضيات Garbage Collector را که در ابتدا ذکر شد مرور کنيد. اولين فرض اين بود که اشياي تازه ايجاد شده داراي عمر کوتاه تري هستند. بنابراين اين گونه به نظر ميرسد که نسل صفر داراي بيشترين مقدار زباله باشد و جمع آوري حافظه از اين نسل موجب آزاد سازي مقدار زيادي حافظه ميشود. بنابراين Garbage Collector نسل يک را رها ميکند و فقط به جمع آوري نسل صفر ميپردازد که اين عمل موجب افزايش سرعت کارکرد پروسه Garbage Collector ميشود.
به وضوح، رها سازي اشياي نسل يک وجب افزايش سرعت و کارايي Garbage Collector ميشود. با وجود اين کارايي Garbage Collector بيشتر افزايش پيدا ميکند چون تمام اشياي موجود در Managed Heap را بررسي نميکند. اگر يک root يا يک شيي از اين نسل به شيي از نسل قديمي تر اشاره کند، Garbage Collector ميتواند ارجاعات داخلي اشياي قديمي تر را در نظر نگيرد، و بدين وسيله زمان مورد نياز را براي تشکيل گرافي از اشياي قابل دسترس کاهش ميدهد. البته اين امر ممکن است که يک شيي قديمي به يک شيي جديد اشاره کند. براي اطمينان از اين که اين شيي قديمي نيز مورد بررسي قرار ميگيرد Garbage Collector از يک مکانيسم داخلي JIT استفاده ميکند به اين نحو که زماني که يک فيلد ارجاع يک شيي تغيير کرد يک بيت را تنظيم ميکند. اين پشتيباني توسط JIT باعث ميشود که Garbage Collector بتواند تشخيص دهد کدام از اشياي قديمي از زمان آخرين عمل جمع آوري تا کنون تغيير کرده اند. فقط اشياي قديمي که داراي فيلدهاي تغيير کرده هستند احتياج به بررسي شدن براي اينکه آيا به شييي از نسل صفر اشاره ميکنند احتياج دارند.
نکته: تستهاي کارايي مايکروسافت نشان ميدهند که عمل Garbage Collection در نسل صفر کمتر از يک ميلي ثانيه در يک کامپيوتر پنتيوم با سرعت 200 مگاهرتز زمان ميبرد.
يک Garbage Collector که از نسلها استفاده ميکند همچنين تصور ميکند که اشيايي که مدت زيادي است که در حافظه مانده اند به زودي نيز از حافظه خارج نميشوند. بنابراين اين احتمال ميرود که اشياي موجود در نسل يک همچنان در طول برنامه قابل دسترس خواهند بود. بنابراين اگر Garbage Collector اشياي موجود در نسل يک را بررسي کند احتمالا مقدار زيادي متغيير غير قابل دسترسي در برنامه نخواهد يافت و احتمالا حافظه زيادي را آزاد نخواهد کرد. بنابراين آزاد سازي نسل يک چيزي جز اتلاف وقت نخواهد بود. اگر هر زباله اي در نسل يک به وجود بيايد در همان نسل باقي خواهد ماند.
B O L O T
11-06-2007, 12:21
د تمام اشياي که از نسل صفر باقي مانده اند وارد نسل يک شده اند. چون Garbage Collector نسل يک را بررسي نميکند شيي B حافظه اي را که گرفته است آزاد نميکند با وجود اينکه از آخرين عمل Garbage Collector تا کنون اين متغيير در برنامه قابل استفاده نبوده است. مجددا بعد از جمع آوري نسل صفر داراي هيچ شيي نخواهد بود و بنابراين مکاني براي قرارگيري اشياي جديد محسوب ميشود. در ادامه برنامه به کار خود ادامه ميدهد و اشياي L تا O را ايجاد ميکند. و در حال اجرا برنامه استفاده از اشياي G و L و M را پايان ميدهد و آنها را غير قابل دسترس ميکند
اجازه دهيد فکر کنيم که تخصيص حافظه براي شيي P باعث تجاوز نسل صفر از سرحد خود شود و اين عمل موجب اجراي مجدد Garbage Collector شود. چون تمام اشياي موجود در نسل يک کمتر از 2 مگابايت است Garbage Collector مجددا تصميم ميگيرد که فقط نسل صفر را بررسي کند و از اشياي غير قابل دسترسي در نسل يک چشم پوشي کند(اشياي B و G).
B O L O T
11-06-2007, 12:22
ه نسل يک به مرور در حال رشد و افزايش حجم است. اجازه دهيد تصور کنيم که اشياي موجود در نسل يک تا سرحد 2 مگابايت فضاي قابل استفاده در نسل يک را اشغال کرده اند. در اين مرحله، برنامه مراحل اجراي خود را همچنان ادامه ميدهد و در اين مرحله اشياي P تا S توليد ميشوند که اين اشيا نسل صفر را نيز تا سرحد خود پر ميکنند. Heap در اين مرحله مشابه شکل زير ميشود.
زماني که برنامه سعي در تخصيص حافظه براي شيي T دارد نسل صفر کاملا پر است و Garbage Collector بايد آغاز به کار کند. اين مرتبه، اشياي موجود در نسل يک هر 2 مگابايت فضاي خود را تا سرحد فضاي نسل يک اشغال کرده اند. علاوه بر اشياي موجود در نسل صفر، تصور ميشود که بعضي از اشياي موجود در نسل يک هم به صورت غير قابل استفاده در آمده اند. بنابراين اين مرتبه، Garbage Collector تصميم ميگيرد که تمام اشياي موجود در نسل يک و نسل صفر را مورد بررسي قرار دهد. بعد از اينکه هر دو نسل به طور کامل توسط Garbage Collector مورد بررسي قرار گرفتند،
B O L O T
11-06-2007, 12:23
انند قبل، اشيايي که در اين مرحله از جمع آوري از نسل صفر باقي ماندند وارد نسل يک ميشوند و نيز اشيايي که در اين مرحله از نسل يک باقي ماندند وارد نسل دو ميشوند. مثل هميشه، بلافاصله نسل صفر از اشيا خالي ميشود و اشياي جديد ميتوانند در اين قسمت قرار گيرند. اشياي موجود در نسل دو اشياي هستند که حداقل دو بار توسط Garbage Collector مورد بررسي قرار گرفته اند. ممکن است بعد از يک يا دو بار جمع آوري نسل صفر، با انجام اين عمل در نسل يک مقداري حافظه آزاد شود، اما اين عمل تا زماني که نسل يک به سرحد خود نرسد انجام نميشود که اين کار ممکن است نياز به چندين بار اجراي جمع آوري در نسل صفر باشد.
Managed heap فقط سه نسل را پشتيباني ميکند: نسل صفر، نسل يک و نسل دو. بنابراين چيزي به نام نسل سه وجود ندارد. زماني که CLR آغاز به کار ميکند، سرحدهايي را براي هر سه نسل در نظر ميگيرد. همانطور که پيشتر ذکر شد، سرحد براي نسل صفر حدود 256کيلوبايت است، سرحد براي نسل يک حدودا 2 مگابايت است و سرحد براي نسل دو حدود 10 مگابايت است. بنابراين سرحد نسلها به گونه اي انتخاب شده است که موجب افزايش بازدهي و راندمان برنامه شود. هرچه سرحد يک نسل بيشتر باشد عمل Garbage Collection کمتر روي آن نسل صورت ميگيرد. و دوباره، بهبود کارايي به وجود مي آيد که به دليل فرضيات اوليه است: اشياي جديد داراي طول عمر کوتاهتري هستند، اشياي قديمي طول عمر بيشتري دارند.
Garbage Collector موجود در CLR يک جمع آوري کننده با تنظيم کننده خودکار است. اين بدين معنا است که Garbage Collector از رفتار برنامه شما مي آموزد که چه زماني بايد عمل جمع آوري را انجام دهد. براي مثال اگر برنامه شما اشياي زيادي را ايجاد کند و از آنها براي مدت زمان کوتاهي استفاده کند، اين امر ممکن است که آزاد سازي حافظه در نسل صفر مقدار زيادي حافظه را آزاد کند.حتي ممکن است تمام حافظه گرفته شده در نسل صفر آزاد شود.
اگر Garbage Collector مشاهده کند که بعد از انجام جمع آوري نسل يک تعداد محدودي از اشيا باقي ماندند، ممکن است که تصميم بگيرد که سرحد نسل صفر را از 256 کيلوبايت به 128 کيلوبايت کاهش دهد. اين کاهش در فضاي معين به اين معني است که عمل جمع آوري بايد در فواصل زماني کوتاه تري رخ دهد اما فضاي کمتري را بررسي کند. بنابراين کارهاي پروسه شما به صورت قابل توجهي افزايش نمي يابد. اگر تمام اشياي موجود در نسل صفر زباله محسوب شوند ديگر احتياجي به فشرده سازي حافظه توسط Garbage Collector نيست. اين عمل ميتواند به سادگي با آوردن اشاره گر NextObjPtr به ابتداي حافظه مورد نظر براي نسل صفر انجام شود. اين عمل به سرعت حافظه را آزاد ميکند!
نکته:Garbage Collector به بهترين نحو با برنامه هاي ASP.NET و سرويسهاي وب مبتني بر XML کار ميکند. براي برنامه هاي تحت ASP.NET، يک تقاضا از طرف کلاينت ميرسد، يک جعبه از اشياي جديد تشکيل ميشود، اشيا کارهاي تعيين شده توسط کلاينت را انجام ميدهند، و نتيجه به سمت کلاينت بر ميگردد. در اين مرحله تمام اشياي موجود براي انجام تقاضاي کلاينت زباله تلقي ميشوند. به بيان ديگر، هر تقاضاي برنامه هاي تحت ASP.NET باعث ايجاد حجم زيادي از زباله ميشوند. چون اين اشيا اغلب بلافاصله بعد از ايجاد ديگر قابل دسترسي نيستند هر عمل جمع آوري موجب آزاد سازي مقدار زيادي از حافظه ميشود. اين کار مجموعه کارهاي پروسه را بسيار کاهش ميدهد بنابراين راندمان Garbage Collector محسوس خواهد بود.
به بيان ديگر، اگر Garbage Collector نسل صفر را مورد بررسي قرار دهد و مشاهده کند که مقدار زيادي از اشيا وارد نسل يک شدند، مقدار زيادي از حافظه توسط Garbage Collection آزاد نميشود، بنابراين Garbage Collector سرحد نسل صفر را تا 512 کيلوبايت افزايش ميدهد. در اين مرحله کمتر انجام ميشود اما با هر بار انجام اين عمل مقدار زيادي حافظه آزاد ميشود.
در طول اين قسمت چگونگي تغيير ديناميک سرحد نسل صفر شرح داده شد. اما علاوه بر سرحد نسل صفر سرحد نسلهاي يک و دو نيز بر اساس همين الگوريتم تغيير ميکنند. به اين معني که زماني که اين نسلها مورد عمل جمع آوري قرار ميگيرند Garbage Collector بررسي ميکند که چه مقدار فضا آزاد شده است و چه مقدار از اشيا به نسل بعد رفته اند. بر اساس نتايج اين بررسيها Garbage Collector ممکن است ظرفيت اين نسلها را کاهش يا افزايش دهدکه باعث افزايش سرعت اجراي برنامه ميشود.
ديگر نتايج کارايي Garbage Collector:
پيشتر در اين مقاله الگوريتم کار Garbage Collector شرح داده شد. با اين وجود در طول اين توضيحات يک فرض بزرگ صورت گرفته بود: اينکه فقط يک ترد در حال اجرا است. اما در مدل واقعي چندين ترد به managed heap دسترسي دارند و يا حداقل اشياي قرار گرفته در managed heap رو تغيير ميدهند. زماني که يک ترد موجب اجراي عمل جمع آوري توسط Garbage Collector ميشود، ديگر تردها حق دسترسي به اشياي موجود در managed heap را ندارند(اين مورد شامل ارجاعهاي اشياي موجود در stack هم ميشود) زيرا Garbage Collector ممکن است مکان اين اشيا را تغيير دهد.
بنابراين وقتي Garbage Collector بخواهد عمل جمع آوري را آغاز کند، تمام تردهايي که در حال اجراي کدهاي مديريت شده هستند به حال تعليق در مي آيند.CLR داراي چندين مکانيسم نسبتا" متفاوت است که ميتواند تردها را به حالت تعليق در آورد بنابراين عمل جمع آوري ميتواند به درستي اجرا شود. دليل اينکه CLR از چندين مکانيسم استفاده ميکند به حالت اجرا نگاه داشتن تردها تا حداکثر زمان ممکن و کاهش سربار کردن آنها در حافظه تا حداقل زمان ممکن است. تشريح اين مکانيسمها از اهداف اين مقاله خارج است اما تا اين حد لازم است ذکر شود که مايکروسافت فعاليتهاي زيادي را براي کاهش فشار پردازشي ناشي از Garbage Collector انجام داده است. و نيز اين مکانيسمها به سرعت در حال تغيير هستند تا به بهترين کارايي خود برسند.
زماني که CLR ميخواهد Garbage Collector را اجرا کند، ابتدا تمام تردها در پروسه جاري را که در حال اجراي کدهاي مديريت شده هستند به حال تعليق در مي آورد. سپس CLR براي تعيين موقعيت هر ترد تمام اشاره گرهاي دستورات در حال اجرا توسط تردها را بررسي ميکند. سپس براي تعين اينکه چه کدي توسط ترد در حال اجرا بوده آدرس اشاره گر دستور با جدول ايجاد شده توسط کامپايلر JIT مقايسه ميشود.
اگر دستور درحال اجرا توسط ترد در يک آفست مشخص شده به وسيله جدول مذکور باشد گفته ميشود که ترد به يک نقطه امن دسترسي دارد. يک نقطه امن نقطه اي است که در آنجا ميتوان بدون هيچ مشکلي ترد را به حال تعليق در آورد تا Garbage Collector کار خود را آغاز کند.اگر اشاره گر دستور در حال اجراي ترد در روي يک آفست مشخص شده توسط جدول دروني تابع قرار نداشت، بنابراين ترد در يک نقطه امن قرار ندارد و CLR نميتواند Garbage Collector را اجرا کند. در اين حالتCLR ترد را هايجک ميکند: به اين معني که CLR استک مربوط به ترد را به گونه اي تغيير ميدهد که آدرس بازگشت به يک تابع خاص پياده سازي شده درون CLR اشاره کند. سپس ترد به ادامه کار خود بازميگردد. زماني که متد در حال اجرا توسط ترد ادامه پيدا کند، اين تابع ويژه اجرا خواهد شد و ترد به حالت تعلق درخواهدآمد.
با وجود اين ممکن است در بعضي مواقع ترد از متد خود بازنگردد. بنابراين زماني که ترد به اجراي خود ادامه ميدهد، CLR 250ميلي ثانيه صبر ميکند. سپس دوباره بررسي ميکند که آيا ترد به يک نقطه امن طبق جدول JIT رسيده است يا نه. اگر ترد به يک نقطه امن رسيده بود CLR ترد را به حالت تعليق درمي آورد و Garbage Collector را اجرا ميکند در غير اين صورت مجددا سعي ميکند با تغيير Stack مربوط به ترد اجراي آن را به تابع ديگري انتقال دهد در صورت شکست مجددا CLR براي چند ميلي ثانيه ديگر نيز صبر ميکند. زماني که تمام تردها به يک نقطه امن رسيدند يا اينکه با موفقيت هايجک شدند، Garbage Collector ميتواند کار خود را آغاز کند. زماني که عمل جمع آوري انجام شد تمام تردها به وضعيت قبلي خود برميگردند و اجراي برنامه ادامه پيدا ميکند. تردهاي هايجک شده هم به متدهاي اوليه خود بازميگردند.
نکته: اين الگوريتم يک پيچ خوردگي کوچک دارد. اگر CLR يک ترد را به حالت تعويق درآورد و دريابد که ترد در حال اجراي يک کد مديريت نشده بود آدرس بازگشت ترد هايجک ميشود و به ترد اجازه داده ميشود که به اجراي خود ادامه دهد. با اين وجود در اين حالت به Garbage Collector اجازه داده ميشود که اجرا شود در حالي که ترد مذکور در حال اجرا است. اين مورد هيچ اشکالي را به وجود نمي آورد زيرا کدهاي مديريت نشده به اشياي موجود در managed heap دسترسي ندارندتا زماني که آن اشيا پين شوند. يک شيي پين شده شي است که Garbage Collector حق حرکت دادن آن را در managed heap ندارد. اگر تردي که در حال حاضر در حال اجراي يک کد مديريت نشده بود، شروع به اجراي يک کد مديريت شده کند، ترد هايجک ميشود و به حالت تعليق درمي آيد تا زماني که Garbage Collection به درستي به اتمام برسد.
علاوه بر مکانيسمهاي ذکر شده(نسلها، نقاط امن، و هايجک کردن)، Garbage Collector از بعضي از مکانيسمهاي اضافي ديگري نيز استفاده ميکند که باعث افزايش بازدهي آن ميشود.
اشياي بزرگ:
فقط يک نکته قابل ذکر ديگر که باعث افزايش سرعت و بازدهي بهتر ميشود باقي مانده است. هر شيي که 85000 بايت يا بيشتر فضاي حافظه را اشغال کند يک شيي بزرگ در نظر گرفته ميشود. اشياي بزرگ در يک heap ويژه اشياي بزرگ قرار ميگيرند. اشياي درون اين heap مانند اشياي کوچک (که راجع به آنها صحبت شد) finalize و آزاد ميشوند. با اين وجود اين اشيا هيچ وقت تحت فشرده سازي قرار نميگيرند زيرا شيفت دادن 85000 بايت بلاک حافظه درون heap مقدار زيادي از زمان CPU را هدر ميدهد.
اشياي بزرگ همواره به عنوان نسل دو در نظر گرفته ميشوند، بنابراين اين اشيا بايد فقط براي منابعي که مدت زمان زيادي در حافظه مي مانند ايجاد شوند. تخصيص اشيايي که داراي طول عمر کوتاه هستند در قسمت اشياي بزرگ باعث ميشود که عمل جمع آوري نسل دو سريعتر انجام شود و اين مورد نيز به بازدهي و کارايي برنامه صدمه وارد ميکند.
Shahrdar
11-06-2007, 17:04
مفید بود مرسی
vBulletin , Copyright ©2000-2025, Jelsoft Enterprises Ltd.