مدیریت حافظه در سی شارپ | به زبان ساده
بخش garbage collector زبان #C در قیاس با زبان ++C عملکرد بسیار کاملتری دارد و میتوانید با خیال راحت و بدون نگرانی از شیوه تخصیص و آزادسازی حافظه کدهای خودتان را بنویسید. اما اگر به عملکرد کد خود اهمیت میدهید، دانستن شیوه مدیریت حافظه در سی شارپ از سوی محیط زمان اجرای NET. به شما کمک میکند که کد بهتری بنویسید.
انواع مقداری در برابر انواع ارجاعی
دو نوع متغیر در NET. وجود دارند و این موضوع به صورت مستقیم روی شیوه مدیریت حافظه تأثیر میگذارد. «انواع مقداری» (Value types) همان انواع مقدماتی یا primitive هستند که اندازه ثابتی دارند و از آن جمله انواع int ،bool ،float ،double و غیره محسوب میشوند. این موارد به صورت «با مقدار» ارسال میشوند، یعنی اگر تابعی مانند someFunction(int arg) را فراخوانی کنید، آرگومانها کپی شده و در مکان جدیدی از حافظه قرار میگیرند.
انواع مقداری
انواع مقداری (به طور معمول) در پسزمینه در «پشته» (stack) ذخیره میشوند. این موضوع در مورد متغیرهای لوکال صادق است و البته استثناهای زیادی وجود دارند که این موارد در هیپ (Heap) ذخیره میشوند. اما در همه موارد مکان حافظه که نوع آن مقداری در قرار دارد، در واقع شامل مقدار واقعی خود متغیر است.
پشته صرفاً یک مکان خاص از حافظه است که با مقدار پیشفرض مقداردهی شده است، اما میتواند بسط یابد. پشته یک ساختمان داده «ورودی آخر، خروجی اول» (Last-in, First-out) یا به اختصار LIFO است. آن را میتوان یک سطل تصور کرد که متغیرها به بخش فوقانی سطل اضافه میشوند و زمانی که از دامنه مورد نظر خارج میشویم، NET. به سراغ سطل رفته و متغیرها را یک به یک از بالا حذف میکند تا این که به ته سطل برسد.
پشته به مقدار زیادی سریعتر است، اما همچنان یک مکان روی RAM است و مکانی خاص روی کش CPU محسوب نمیشود. با این حال پشته بسیار کوچکتر از هیپ است و از این رو امکان قرار گرفتن روی کش را دارد که به ارتقای عملکرد کمک میکند.
پشته اغلب عملکرد خود را به لطف ساختمان LIFO به دست آورده است. زمانی که یک تابع را فراخوانی میکنید، همه متغیرهای تعریف شده در آن تابع به پشته اضافه میشوند. زمانی که تابع بازگشت یافته و آن متغیرها از دامنه خارج میشوند، این پشته همه چیزهایی را که تابع در آن قرار داده بود پاک میکند. محیط زمان اجرا (runtime) این موضوع را به وسیله «قابهای پشته» (stack frames) مدیریت میکند که بلوکهای حافظه را برای کارکردهای مختلف تعریف میکند. تخصیصهای پشته بسیار سریع هستند، زیرا صرفاً یک مقدار منفرد را در انتهای قالب پشته مینویسیم.
خطای «سرریز پشته» (StackOverflow) نیز دقیقاً از همین جا ناشی میشود که موجب میشود تابع فراخوانیهای متدهای تودرتوی زیادی را در خود جای داده و کل پشته پر شود.
انواع ارجاعی
در سوی دیگر انواع ارجاعی یا بسیار بزرگ هستند، اندازه ثابتی ندارند و یا به مدت زیادی روی پشته باقی میمانند. به طور معمول این نوع دادهها به شکل شیء و کلاسهایی هستند که وهلهسازی شدهاند، اما شامل آرایهها و رشتهها که اندازه متغیری دارند نمیشوند.
انواع ارجاعی مانند وهلههای کلاسها هستند و غالباً با کلیدواژه new مقداردهی میشوند که یک وهله جدید از کلاس میسازد و ارجاعی به آن بازگشت میدهد. شما میتوانید آن را به یک متغیر لوکال نسبت دهید که به طور معمول از پشته برای ذخیرهسازی ارجاع به مکانی از هیپ استفاده میکند.
هیپ میتواند بسط یابد و تا جایی که حافظه رایانه امکان میدهد گسترش یابد و از این رو گزینهای مناسب برای ذخیرهسازی دادههای حجیم محسوب میشود. با این حال هیپ سازمان نیافته است و در #C باید با یک garbage collector مدیریت شود تا عملکرد صحیحی داشته باشد. تخصیصهای هیپ نیز کندتر از تخصیصهای پشته هستند، گرچه همچنان به قدر کافی سریع محسوب میشوند.
شاید بد نباشد گفت که میتوان «انواع مقداری» و «انواع ارجاعی» را به ترتیب «انواع پشته» و «انواع هیپ» نامید، اما باید توجه کنید که چند استثنا نیز برای این قاعده وجود دارد.
- متغیرهای بیرونی تابعهای لامبدا، متغیرهای لوکال بلوکهای IEnumerator و متغیرهای لوکال متدهای async همگی روی هیپ ذخیره میشوند.
- فیلدهای نوع مقداری کلاسها، متغیرهای بلندمدت محسوب میشوند و همواره روی هیپ ذخیره میشوند. این متغیرها درون یک نوع ارجاعی قرار میگیرند و همراه با آن نوع ارجاعی ذخیره میشوند.
- فیلدهای کلاس استاتیک نیز روی هیپ ذخیره میشوند.
- استراکتهای سفارشی انواع مقداری هستند، اما میتوانند شامل انواع ارجاعی از قبیل لیستها و رشتهها نیز باشند که به صورت معمول روی هیپ ذخیره میشوند. ایجاد یک کپی از استراکت موجب ایجاد کپی جدیدی از همه انواع ارجاعی روی هیپ میشود.
مهمترین استثنا در مورد قاعده فوقالذکر استفاده از stackalloc با <Span<T است که به صورت دستی یک بلوک از حافظه را روی پشته به یک آرایه موقت تخصیص میدهد که وقتی از دامنه خارج شود مانند یک پشته معمولی از حافظه پاک خواهد شد. این کار موجب میشود که یک تخصیص هیپ پرهزینه را دور بزنیم و فشار کمتری روی garbage collector میآورد. این کار موجب میشود که بهرهوری بالاتری داشته باشیم، اما یک قابلیت پیشرفته محسوب میشود و از این رو باید با شیوه صحیح اجرای آن آشنا باشید تا موجب بروز استثنای سرریز پشته نشوید.
منظور از گردآوری زباله چیست؟
پشته یک ساختمان داده کاملاً منظم است، اما هیپ چنین نیست. بدون وجود چیزی که هیپ را مدیریت کند، هیچ چیزی روی آن به صورت خودکار پاک نمیشود و در نهایت اپلیکیشن با بود حافظه مواجه میشود چون هیچ حافظهای آزاد نشده است.
دقیقاً به همین دلیل است که از ابزارهای خاصی به نام «گردآوری زباله» (Garbage Collector) استفاده میکنیم. این ابزار روی نخ پسزمینه به صورت دورهای اجرا میشود و اپلیکیشن را برای یافتن ارجاعهایی که دیگر روی پشته وجود ندارند اسکن میکند و به این ترتیب مشخص میسازد که برنامه به کدام دادههایی که ارجاع دادهاند دیگر اهمیت نمیدهد. محیط زمان اجرای NET. میتواند وارد شده و حافظه را پاکسازی کرده و یا روی یک پردازش جابجا کند تا هیپ نظم بیشتری پیدا کند.
با این حال این کار هزینهای دارد، چون فرایند گردآوری زباله کند و پرهزینه است. این پردازش روی نخ پسزمینه اجرا میشود، اما برخی مواقع وجود دارند که اجرای برنامه باید به دلیل اجرای فرایند گردآوری زباله متوقف شود. این هزینهای است که از برنامهنویسی در #C ناشی میشود و تنها کاری که میتوانیم انجام دهیم، کاهش میزان زبالهای است که تولید میکنیم.
در زبانهایی که Garbage Collector ندارند، شما باید خودتان به صورت دستی حافظههای تخصیص یافته را آزاد کنید که در اغلب موارد سریعتر است، اما برای برنامهنویس کار مشقتباری محسوب میشود. بنابراین از یک نظر Garbage Collector مانند یک جاروبرقی رباتیک است به صورت خودکار کف زمین را تمیز میکند، اما به هر حال کندتر از زمانی عمل میکند که خودتان دست به کار شده و جاروبرقی را روشن کرده و خانه را تمیز میکنید.