با سلام جناب استاد فست کد اتفاقا یه مقدار راجبش خوندم مفدترینش چیزهایی که من خوندم اینا هستن
وقتی شما Object ای رو new می کنید، باعث میشید تا ctor اون Object فراخوانی بشه. اما در مورد destructor ها وقتی اون object رو null میذارید، destructor فراخوانی نمیشه. در واقع شما کنترلی روی زمان فراخوانی اون ندارید. destructor وقتی فراخوانی میشه که GC صلاح بدونه. اما GC چی هستش؟ Garbage Collector یا همون GC، مدیریت تخصیص و آزادسازی حافظه یه برنامه رو در .NET به عهده داره. همه چیز در heap مدیریت شده رخ میده، heap ای که بسیار شبیه Heap کدهای native هستش با این تفاوت که Object ها توسط برنامه نویس آزاد نمیشن، بلکه هنگامی آزاد میشن که دیگه نیازی بهشون نیست. (بد نیست بدونید که GC در درون خودش حاوی pointer به object هایی هستش که تو heap مدیریت شده نگه داشته میشه. به این pointer ها root میگن. وقتی برنامه شروع به کار میکنه یا هر وقت شما Object ای رو new می کنید، CLR یه graph از root ها رو میگیره و تو یه زمان از پیش تعیین شده شروع به آزاد سازی همه اون object هایی میکنه که root اونها null شده. هر وقت شما یه object ای رو که new کردین، مساوی null بذارین، سیستم تشخصی میده و root مربوط به اون object رو هم null میکنه تا بعدا بتونه سر فرصت اونهایی که null هستن رو، release کنه).
اما سیستم از کجا می خواد بفهمه که دیگه نیازی به یه Object نیست؟ برای فهمیدن جواب این سوال ایتدا باید بدونیم که C# دو نوع عمومی از type ها رو پشتیبانی میکنه: value type ها و Reference Type ها.
value-type ها تایپهای enum، built-in ها و struct ها هستن. اونها میتونند مستقیم مقادیر رو در خودشون حفظ کنن. Stack، حافظه ای هستش که این نوع تایپها توسط سیستم در اون نگهداری میشن. در مقابل reference-type ها رو داریم، که در واقع reference یا pointer به یک instance رو در stack نگه میدارن و خود instance رو در heap مدیریت شده. البته من قبلا در مورد value-type ها و reference ها در این پست توضیحاتی داده بودم که خوندنش خالی از لطف نیست.
اما چه اتفاقی می افته وقتی شما یه Object ای رو new می کنید؟ CLR فرض میکنه که حافظه سیستم نامحدوده و شروع به تخصیص حافظه بصورت ترتیبی می کنه. یعنی خونه به خونه حافظه رو میره جلو و اختصاص میده به Object هایی که شما new می کنید. اما واقعیت اینه که سیستم، حافظه محدودی داره و تا ابد نمیتونه این مساله ادامه پیدا کنه، پس یه نفر باید فضاهای بلا استفاده تو heap رو آزاد کنه و اینجاست که GC توسط root هایی که بالا توضیح دادم، اقدام به آزادسازی حافظه می کنه. اما هنوز یه کار دیگه هم باید انجام بشه، و اون Compact کردن حافظه هستش. اما همه این مراحل به نظرتون باندازه کافی سریع هستش؟ فکرش رو کنید، وقتی GC شروع به آزاد سازی حافظه کنه، باید بره لیست همه root هایی که مساوی null هستن رو بیاره و دونه به دونه اقدام به آزادسازی اونا کنه که کاملا مشخصه که بسیار روند کندی هستش.
پس اومدن گفتن جای اینکه تو یه فاز بریم و object هایی که root اونها الان null هست رو آزاد کنیم، بیاییم این کار رو تو چند مرحله انجام بدیم. اما با توجه به این مفروضات که اولا Compact کردن بخشی از heap سریعتر از compact کردن کل heap هستش و ثانیا Object های قدیمیتر احتمالا بیشتر باقی می مونن تا object های جدیدتر.
اما فازها: گفتن ما 3 تا فاز تعریف میکنیم. فاز اول وقتی هستش که یه object ای new میشه. همه object هایی که new میشن تو فاز 1 ذخیره میشن تا وقتی که فاز یک پر بشه. هر وقت این اتفاق افتادش، GC شروع به آزادسازی تمام Object هایی میکنه که دیگه جایی reference نشدن و مابقی که هنوز در حال استفاده هستن compact میشن و به سمت چپ میان. (حافظه رو خونه های چپ به راست افقی در نظر بگیرید). تمام Object هایی که باقی می مونن (یعنی هنوز در حال استفاده هستن)، میرن تو فاز 2. وقتیکه دوباره فاز 1 پر شدش، همه Object هایی که تو فاز 2 بودن میرن تو فاز 3. هر وقت هر 3 فاز پر بشن، OutOfMemoryException، داده میشه.
////////////////////
بحث مدیریت حافظه ،یکی از بحث های مهم در عرصه نرم افزار نویسی است.مثلا شرکت موزیلا بعد از FIreFox2 یک فراخوان برای برنامه نویس ها میده تا مشکل استفاده از حافظه زیاد موقع بارگذاری رو کم کنه (و انصافا توی FireFox3 موفق هم بوده).این مطلب اونقدر در مورد این نرم افزار مهم بوده که طرفدارای دو آتشه IE این مشکل رو به عنوان اولین ایراد FireFox مطرح می کردند!
با این موضوع که در نرم افزار های داخلی هم این مطلب رعایت نمیشه (البته معمولا) موافقم.شاید به این دلیل که برنامه نویسی که داره برنامه رو می نویسی ،خودش روی یک سیستم آنچنانی (از نرم افزار تا سخت افزار) کار می کنه و نمی دونه بعضی ها می خواهند این نرم افزار رو روی یک سیستم با حداقل امکانات اجرا کنند! به همین دلیل سینه چاک های VB زیادن و VC کارها کم! در هر حال بگذریم....
اما دوستان به منابع مدیریت شده (که مقصود مدیریت توسط GC) و منابع مدیریت نشده (که همچنان کمافی السابق باید توسط برنامه نویس مدیریت بشه - مثل فایلها ، Connection ها و ...) اشاره کردند.ولی قبل از این که در مورد این دو سئوال که 'آیا این بیهوده نبوده که متغیرهای محلی dt وda را هم Dispose کرده' و 'آیا این کلاس مدیریت شده است' با هم بحث کنیم.می خواهم یک بار دیگر مراحل کار GC رو مرور کنیم (هر چند همه می دونیم ولی برای استناد به اون به تکرار نیازه!).درضمن این مطالب برای انواع دارای نوع Refence است نه انواع مقدار (مثل int یا struct و ..).
1) وقتی از کلمه کنیدی new استفاده می کنید،CLR یک فضا برای اون تخصیص داده و به GC اعلام می کنه کی متغیر جدید داره و یک شمارنده Refrence برای اون ایجاد میکنه!
2) اگر این فضای تخصیص داده شده به متغیری تخصیص داده بشه (مثلا متغیری که به این فضا اشاره دارد به متغیر دیگری تخصیص داده بشه)،به شکل اتوماتیک به شمارنده Refrence افزوده می شه.به همین دلیل است که دات نت اجازه پیاده سازی عملگر انتساب (=) رو به برنامه نویسی نمی ده (در یک کلاس همه عملگر ها به غیر از انتساب قابل پیاده سازی هستند!).
3) هنگامی که یک متغیر نابود می شه (مثلا یک متغیر محلی که با خروج از تابع نابود میشه)،یا متغیری که قبلا یک Refrence به محلی از حافظه داشته،حالا اشاره به محل دیگه ای کنه (با انتساب مقدار دیگه ای به اون)،به شکل اتوماتیک از شمارنده Refrence کاسته می شه.توجه کنید که به محض به صفر رسیدن این شمارنده ،فضای مورد بجث آزاد نمیشه!
4) هر گاه GC اقدام به پاک سازی حافظه کنه (اینجاست که زمان این کار معلوم نیست) ،دو عملیات رخ میده اول اینکه فضا هایی که شمارنده Refrence اونها صفر است ،رها سازی می شوند.دوم تمام اطلاعات موجود در حافظه دوباره باز آرایی میشوند تا فضا های مورد استفاده در کنار هم قرار گیرند (بحث Boxing هم مربوط به این کار GC است).البته برنامه نویس با برنامه متوجه جابجایی اطلاعات در حافظه نمی شوند و عملا با همان Refrence که در متغیر ها هستند دسترسی به مقادیر ممکن است!(باید بگم این بحث کاملتر از این توضیح کوچک است ولی تا همینجا کافی به نظر می رسه).
حالا سئوال اینه که 'کی GC اقدام به رها سازی حافظه می کنه (مرحله4)؟'.زمان این عمل معلوم نیست (درضمن این عملیات دریک Thread مستقل از Thread اصلی انجام میشه).شاید چند ثانیه نیز طول بکشه! پس نتیجه میگیریم که بین مراحل سوم و چهارم ،با اینکه دیگر نیاز به حافظه مورد نحث نیست،ولی هنوز در حافظه مکانی برای آن منظور شده.شاید برای متغیر های کوچک ،خیلی مهم نباشد ولی برای متغیر هایی با حافظه مصرفی بزرگ،قطعا می تواند نگران کننده باشد.
برای این کار دو راه حل را میشناسم (راه های دیگری هم شاید باشد که من از آن بی خبر باشم).راه اول استفاده از متد Dispose اشیای است.در این حالت فضای مصرفی آنها تا حد امکان کوچک میشود.مثلا کد زیر را در یکی از صفحات همین سایت دیدم :
public void PaintGradient(Control _control, LinearGradientMode _direction, Color _gradientColorStart, Color _gradientColorEnd)
{
//https://barnamenevis.org/showpost.php...3&postcount=35
LinearGradientBrush gradBrush;
gradBrush = new LinearGradientBrush(new Rectangle(0, 0, _control.Width, _control.Height), _gradientColorStart, _gradientColorEnd, _direction);
Bitmap bmp = new Bitmap(_control.Width, _control.Height);
Graphics g = Graphics.FromImage(bmp);
g.FillRectangle(gradBrush, new Rectangle(0, 0, _control.Width, _control.Height));
_control.BackgroundImage = bmp;
_control.BackgroundImageLayout = ImageLayout.Stretch;
}
به نظر شما فراخوانی یک متد مثل Paint برای یک کنترل چند بار صورت می گیرد.شاید چندین بار در یک ثانیه.البته در یک پیاده سازی بد می توان این مقدار را به چند هزار بار در یک ثانیه رساند (توجه کنید که فراخوانی یک متد مثل این، کاملا به پیاده سازی وابسته است).همینطور که ملاحظه می کنید شی gradBrush هرگز نابود نمی شود تا وقتی که GC به حسابش برسد! حالا اگر فرض کنید که در یک برنامه این متد چند ده بار در ثانیه فراخوانی شود و GC نیز 5 ثانیه یک بار عمل کند (تکرار می کنم : این که GC کی عمل کند به خیلی عوامل وابسته بوده و قابل پیش بینی نیست!).در نتیجه تا زمانی که GC عمل کند ،چه مقدار حافظه،بی مورد مصرف شده است!در حالی که در انتهای این متد می توان با یک خط ساده مثل
gradBrush.Dispose();
این حافظه را به سیستم برگرداند!در این متد فقط یک متغیر اینگونه است.ممکن است در برنامه های بزرگتر،تعداد متغیر ها هم بیشتر و هم فضای مورد استفاده آنها بزرگتر و تعداد فراخوانی آنها بیشتر باشد! پس بهتر است در کلاس هایمان اولا عادت کنیم که متد Dispose را پیاده سازی کنیم و ثانیا در انتهای هر متد متغیر های بلا مصرف را Dispose کنیم (کار از محکم کاری عیب نمی کند!).توجه کنید که مخرب کلاس (destructor) در فرایند مرحله 4 GC فراخوانی می شود.
راه حل دوم هم استفاده از متدهای خوده GC است مثل Collect و WaitForPendingFinalizers (این متد Thread اصلی را می خواباند تا فرآیند پاک سازی حافظه انجام شود.به عبارت دیگر یک جور فعال سازی GC برای مرحله 4 است)،که معمولا به شکل
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
و چند تا دیگه