حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

مفاهیم Object Life Time و Application Root در مدیریت حافظه دات نت

در مقالات قبلی که به بررسی و آشنایی با زبان برنامه نویسی سی شارپ پرداختیم، با نحوه تخصیص و مدیریت حافظه در برنامه های دات نت آشنا شدیم. در این مقاله قصد داریم به صورت تخصصی تر این بحث را ادامه داده و با برخی مفاهیم و سرویس ها برای مدیریت حافظه در دات نت آشنا شویم. ابتدا جهت یادآوری برخی مفاهیم را به صورت خلاصه بررسی می کنیم:

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران
  1. حافظه stack: حافظه ای محدود اما سریع که متغیر های نوع Value Type در آن ذخیره می شوند.
  2. حافظه heap: حافظه ای با گنجایش بیشتر از حافظه stack اما سرعت پایین تر که اشیاء از نوع Reference Type در آن نگهداری می شوند.
  3. GC یا Garbage Collector: سرویسی که توسط CLR برای مدیریت و پاک سازی حافظه Heap در بازه های زمانی مختلف اجرا می شود.

Object Life Time چیست؟

زمانی که شما برنامه ای را به زبان سی شارپ یا هر برنامه مبتنی بر دات نت می نویسید، نگران مدیریت حافظه نیستید. زیر می دانید که سرویس GC کار مدیریت حافظه را برای شما انجام می دهد. تنها کاری که شما می کنید ایجاد شئ بوسیله کلمه کلیدی new بوده و بعد خیالتان بابت همه چیز راحت است! اما سرویس GC از کجا متوجه می شود که شئ ای در حافظه Heap باید حذف شود؟ جواب ساده است: «در صورتی که شئ مورد نظر توسط هیچ یک از بخش های کد قابل دسترس نباشد!». برای مثال، فرض کنید متدی را در کلاس Program تعریف کرده اید که عملیات ایجاد یک شئ را انجام می دهد:

public static void CreatePerson()
{
    var person = new Person();
}

دقت کنید که شئ person، تنها تا زمانی قابل استفاده است که کد داخل متد در حال اجرا است و پس از خروج از متد، دیگر نیازی به شئ person نیست، پس سرویس GC می داند که باید این شئ را از حافظه حذف کند. حال اگر شئ ساخته شده به عنوان مقدار بازگشتی متد استفاده میشد، زمانی شئ person حذف می شود که دیگر نیازی به شئ بازگردانده شده است متد نباشد. وجود قابلیتی مثل GC برای برنامه نویسانی که کار مدیریت حافظه را به صورت دستی انجام می داند.

مثل برنامه نویسان ++C، حکم بهشت را دارد! فقط برنامه نویسان ++C می توانند درک کنند که عملیات مدیریت حافظه در محیط های مدیریت نشده (Unmanaged) چه کار طاقت فرسایی است! اما زمانی که کلمه کلیدی new برای ساخت شئ اجرا می شود، دقیقاً چه اتفاقاتی در پشت پرده رخ می دهد؟ ابتدا بهتر است نگاهی به کد IL ایجاد شده برای ساخت شئ Person در متد CreatePerson داشته باشیم:

.method public hidebysig static void  CreatePerson() cil managed
{
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] class OLT.Person person)
  IL_0000:  nop
  IL_0001:  newobj     instance void OLT.Person::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ret
} // end of method Program::CreatePerson

حافظه Heap و روند کاری آن چیزی بیشتر از یک حافظه ساده که مقداری در آن ذخیره می شود می باشد! سرویس GC عملیات های زیادی برای Optimzie کردن و استفاده بهینه از Heap انجام می دهد. برای این منظور همیشه در حافظه Heap، یک اشاره گر (pointer) که محل ذخیره سازی شئ بعدی را مشخص می کند وجود دارد که به آن Next Object Pointer یا New Object Pointer می گویند. در کد بالا، عبارت newobj را مشاهده می کنید. این عبارت کارهای زیادی انجام می دهد که در زیر به بررسی آن ها می پردازیم:

  1. ابتدا میزان حافظه مورد نیاز برای ذخیره سازی شئ در حافظه Heap محاسبه می شود، این فضا شامل خود شئ و تمامی کلاس های پایه (Base) شئ مورد نظر می باشند.
  2. حافظه Heap را بررسی می کند تا از وجود میزان فضای مورد نیاز برای ایجاد شئ اطمینان حاصل کند. در صورت وجود فضا، سازنده کلاس فراخوانی می شود و مقدار بازگشتی فراخوانی سازنده، آدرس خانه ای از حافظه Heap است که شئ در آن ذخیره شده است. اگر حافظه موجود نباشد، سرویس GC برای پاک سازی Heap اجرا شده و سپس عملیات فراخوانی سازنده انجام می شود.
  3. در قدم بعدی، آدرس بازگشتی حاصل از فراخوانی سازنده به متغیر ایجاد شده در حافظه stack بازگردانده شده و Next Object Pointer بر اساس خانه جدیدی که GC تصمیم می گیرد کجا باشد تغییر می کند.

در تصویر زیر، نمونه ای از روند ذکر شده را مشاهده می کنید:

ست کردن مقدار null به یک شئ

برنامه نویسان ++C برای حذف یک شئ از حافظه می بایست مقدار آن را برابر null قرار دهند. شاید برای شما این سوال پیش بیاد که اگر این کار را در زبان سی شارپ و تحت پلاتفرم دات نت انجام دهیم چه اتفاقی می افتد؟ کد زیر را در نظر بگیرید:

public static void CreatePerson()
{
    var person = new Person();
    person = null;
}

با ابزار ildasm، نگاهی به کد IL می اندازیم:

.method public hidebysig static void  CreatePerson() cil managed
{
  // Code size       10 (0xa)
  .maxstack  1
  .locals init ([0] class OLT.Person person)
  IL_0000:  nop
  IL_0001:  newobj     instance void OLT.Person::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldnull
  IL_0008:  stloc.0
  IL_0009:  ret
} // end of method Program::CreatePerson

یک عبارت جدید به کد ما اضافه شده است، عبارت ldnull. این opCode، مقدار null را در Virtual Execution Stack قرار میدهد که در حقیقت ست کردن مقدار null به متغیر person ما است. موضوعی که ما می خواهیم به آن اشاره کنیم این است که قرار دادن مقدار null در یک متغیر، تضمینی برای اجرای Garbage Collector و آزاد سازی فضای حافظه نیست! تنها کاری که شما انجام می دهید، قطع کردن ارتباط میان متغیر person در حافظه stack و شئ ایجاد شده در حافظه heap است، البته در اولین اجرای GC، شئ مورد نظر از حافظه Heap حذف خواهد شد.

Application Root چیست؟

به یاد دارید چگونه GC تشخیص می داد که یک شئ دیگر استفاده نشده و می بایست حذف شود؟ برای آشنایی با جزئیات این مورد، می بایست با مفهومی به نام Application Root آشنا شویم. به زبان ساده، یک root یک مکان ذخیره سازی است که آدرس یک شئ در Heap در آن نگهداری می شود. می توان مفهوم root را در هر یک از دسته های زیر جا داد:

  1. Reference به شئ های Global (در سی شارپ امکان استفاده از شئ های Global وجود ندارد، اما IL این قابلیت را پشتیبانی می کند)
  2. Reference به شئ های static و اعضاء static
  3. Reference به شئ های تعریف شده در یک scope
  4. Reference به شئ های پاس داده شده به یک متد به عنوان پارامتر
  5. Reference به شئ هایی که در انتظار عملیات Finalize هستند.
  6. Register های CPU که به یک شئ Reference دارند

در طول عملیات GC، تمام اشیاء داخل Heap بررسی می شوند تا مشخص شود آن ها توسط کدهای برنامه در دسترس هستند یا خیر. برای این کار، ابتدا CLR یک گراف از شئ ها می سازد که به آن Object Graph می گویند. Object Graph ها برای این ساخته می شوند تا شئ هایی که باید نگهداری شوند شناسایی شوند. توجه کنید، CLR هیچ گاه یک شئ را دوبار در گراف قرار نمی دهد، اتفاقی که در برنامه های COM رخ می دهد که اصطلاحاً در COM به آن Circular Reference گفته می شود.

بعد از تشکیل گراف، مشخص می شود که کدام یک از اشیاء در دسترس Application Root ها هستند. بعد از مشخص شدن اشیاء ای که توسط هیچ Application Root ای Reference ای به آن ها داده نشده، آن ها از حافظه حذف شده و عملیات Defragment بر روی حافظه Heap انجام می شود تا اشیاء داخل Heap مرتب شده و فضای های خالی شده قابل دسترس باشند. این کار نیازمند ست کردن مجدد Application Root ها می باشد.

کاربرد Object Generation ها

زمانی که CLR تصمیم به شناسایی اشیاء بلا استفاده می گیرد، عملیات شناسایی بر روی هر شئ ای که در Managed Heap قرار دارد انجام نمی شود. انجام همچین کاری می تواند به شدت بر روی Performance برنامه ها تاثیر بگذارید. برای بهینه سازی این روند، از تکنیلی به نام Object Generation ها استفاده می شود. فلسفه استفاده از این تکنیک خیلی ساده است: هر چقدر شئ ای بیشتر در Heap حضور داشته باشد، احتمال استفاده از آن بیشتر است. برای مثال، در برنامه های ویندوز یک شئ برای فرم اصلی وجود دارد که این شئ از ابتدا تا انتهای برنامه نباید از بین برود. بر اساس همین سناریو، هر شئ بر اساس زمان ایجاد در یک Generation قرار میگیرد. در کل سه Generation مختلف وجود دارد که در زیر آن ها را بررسی می کنیم:

  1. Generation 0: اشیاء ای را مشخص می کند که جدیداً ایجاد شده و تا به حال برای حذف بررسی نشده اند
  2. Generation 1: اشیاء ای که توسط GC بررسی شده اند، اما از عملیات حذف نجات پیدا کرده اند، حال دلیل این موضوع می تواند وجود Reference از یک Application Root به شئ باشد یا عدم نیاز به حذف شئ به دلیل وجود فضا در حافظه Heap
  3. Generation 3: اشیاء ای که بیش از یکبار از پروسه حذف اشیاء توسط GC نجات پیدا کرده اند

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


نویسنده: حسین احمدی
انجمن تخصصی فناوری اطلاعات ایران


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

نظرات