عنوان:

‫مدیریت حافظه و جمع‌آوری زباله (Garbage Collection) - قسمت اول


نویسنده: امیر مکارچی
تاریخ: ۱۴۰۳/۱۱/۱۰ ۱۹:۵۰
آدرس: www.dntips.ir
در زبان‌هایی مثل C و ++C، برنامه‌نویس باید به صورت دستی از توابعی مثل free یا عملگر delete برای آزاد کردن حافظه استفاده کند که به آزادسازی صریح حافظه یا Explicit Deallocation شناخته میشود. حال مشکل زمانی ایجاد میشود که این آزادسازی، به درستی انجام نگردد که دو نوع خطای اساسی را ممکن است بوجود بیاورد: Dangling Pointer و Memory Leak.
وقتی حافظه‌ای را آزاد می‌کنید ولی هنوز ارجاع‌هایی به حافظه وجود دارد؛ این ارجاع‌ها Dangling Pointer یا اشاره‌گر آویزان نامیده میشوند. حالا اگر برنامه بخواهد از این اشاره‌گر استفاده کند، نتیجه، غیرقابل پیش‌بینی می‌شود. ممکن است برنامه کرش کند، یا بدتر، ادامه بدهد و خروجی‌های اشتباهی را تولید کند که خیلی سخت قابل ردیابی هستند.

برای حل این مشکل، یکی از روش‌ها، استفاده از Fat Pointer می باشد. این نوع اشاره‌گرها، همراه با آدرس حافظه، یک شماره نسخه از هدفی را که دارند هم ذخیره می‌کنند. هر وقت برنامه قصد داشته باشد که از اشاره‌گر استفاده کند، بررسی می‌کند که شماره نسخه با هدف مطابقت دارد یا خیر. ولی از این روش به‌خاطر سربار زیاد، معمولاً فقط برای دیباگ استفاده میگردد.
حالا اگر برنامه‌نویس فراموش کند حافظه‌ای که به آن نیاز ندارد را آزاد کند، آن حافظه اشغال باقی مانده و باعث Memory Leak می‌شود که در برنامه‌های بزرگ ممکن است باعث افت شدید کارآیی یا حتی پر شدن حافظه گردد.


حذف زودهنگام (Premature Deletion)
در ابتدا، A (یک شیء زنده که همچنان در برنامه قابل دسترسی است) به B (شیءای که به‌طور زودهنگام آزاد شده‌است) اشاره می‌کند.
اگر B زودتر از موعد حذف شود، A همچنان یک اشاره‌گر به حافظه‌ی B دارد، اما محتوای B دیگر معتبر نیست.
این مورد منجر به ایجاد یک اشاره‌گر معلق (Dangling Pointer) در A می‌شود.

نشت حافظه (Memory Leak)
شیء C (شیءای که دیگر قابل دسترسی نیست؛ اما حافظه‌ی آن آزاد نشده‌است ) تنها از طریق B قابل دسترسی است.
با حذف شدن B، دیگر راهی برای دسترسی به C وجود ندارد؛ اما چون حافظه‌ی C آزاد نشده است، این حافظه در سیستم باقی می‌ماند و دیگر قابل استفاده نیست.
این یک نشت حافظه (Memory Leak) محسوب می‌شود.


وقتی چند Thread به یک شیء دسترسی دارند، مشکلاتی مثل Deadlock، Livelock و ABA Error هم بوجود می آیند. برای این چالش‌ها، در زبان‌هایی مثل ++C، استفاده از کلاس‌های اشاره‌گر هوشمند یا Smart Pointers مثل unique_ptr و shared_ptr راه‌حل است. این‌ها عملیات تخصیص و آزادسازی حافظه را به صورت خودکار انجام می‌دهند. مثلاً unique_ptr مالکیت انحصاری شیء را به عهده دارد و وقتی خودش از بین برود، حافظه را آزاد می‌کند.
ولی این روش‌ها محدودیت‌هایی هم دارند. مثلاً auto_ptr که قبلاً در ++C بود، به‌خاطر مشکلاتی که داشت حذف شد. همچنین، Reference Counting نمی‌تواند ساختارهای داده‌ای حلقوی (Cyclic Data Structures) را مدیریت کند و مهم‌تر از همه، اگر این اشاره‌گرها بخواهند در برنامه‌های چندنخی استفاده شوند، سربار زیادی را ایجاد می‌کنند.

این مشکلات به‌قدری رایج هستند که مثلاً پروژه‌ی Chromium گزارش داده‌است که 70 درصد از باگ‌های امنیتی بحرانی مرورگر Chrome، مربوط به مشکلات ایمنی حافظه است که بیشتر ناشی از Use-After-Free میشوند. مایکروسافت هم گزارش داده که 70 درصد از به‌روزرسانی‌های امنیتی 12 سال اخیر، مربوط به مشکلات مشابه است. برای حل این مشکلات پیشرفت‌هایی اتفاق افتاده‌است که یکی از روش‌های مایکروسافت، استفاده از MemGC ست، که یک جمع‌کننده‌ی زباله‌ی محافظه‌کار (Conservative Garbage Collector) برای DOM در مرورگر Internet Explorer و Edge است.

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

مدیریت خودکار حافظه پویا (Automatic Dynamic Memory Management)
مدیریت خودکار حافظه یا همان Garbage Collection، خیلی از مشکلات مدیریت دستی حافظه را حل می‌کند. مثلاً یکی از بزرگ‌ترین مزیت‌هایی که دارد این است که جلوی ایجاد Dangling Pointer را می‌گیرد. چون یک شیء تا زمانیکه هیچ ارجاعی به آن وجود نداشته باشه، آزاد نمی‌شود. یعنی تضمین می‌کند همه‌ی زباله‌های حافظه، آزاد شوند. ولی دو نکته وجود دارد؛ اول اینکه تعریف «زباله» توسط GC ممکن است شامل تمام اشیایی که دیگر استفاده نمی‌شوند، نگردد. دوم اینکه، برای افزایش کارآیی، بعضی از GCها ممکن است عمداً بعضی از اشیا را آزاد نکنند.
با این کار مشکلاتی مثل آزادسازی دوباره حافظه (Double-Free) هم حل می‌شود، چون فقط GC تصمیم می‌گیرد، چه چیزی آزاد شود. آزادسازی دوباره حافظه (Double-Free) زمانی رخ می‌دهد که یک برنامه سعی می‌کند حافظه‌ای را که قبلاً آزاد شده (با free در C++/C یا معادل آن) دوباره آزاد کند. این خطا می‌تواند باعث خرابی حافظه (Memory Corruption)، خرابی برنامه (Crash) یا حملات امنیتی مانند استفاده پس از آزادسازی (Use-After-Free) شود.
این سیستم، دیدی کلی نسبت به ساختار اشیاء در هیپ و ارجاع‌های موجود دارد و همین باعث می‌گردد مشکلاتی‌که از تصمیم‌های محلی برنامه‌نویس ناشی می‌شوند، حذف شوند. اما به این معنا نیست که GC تمام مشکلات حافظه را حل میکند. GC جلوی Dangling Pointer و Double-Free را می‌گیرد، ولی مشکل Space Leak، همچنان ممکن است پیش بیاید. مثلاً اگر ساختاری وجود داشته باشد که هنوز به آن دسترسی هست، ولی هیچ‌وقت از آن استفاده‌ای نمی‌شود، GC نمی‌تواند کاری برای آن انجام دهد. نشتی فضا (Space Leak) زمانی رخ می‌دهد که یک برنامه به دلیل عدم آزادسازی حافظه غیرضروری، فضای بیشتری از حافظه را اشغال می‌کند، بدون اینکه به‌درستی از آن استفاده کند. این مشکل معمولاً باعث افزایش مصرف حافظه (Memory Bloat) و در نهایت کاهش کارآیی یا کرش برنامه می‌شود.
فرض کنید درون برنامه‌، یک کش (Cache) دارید که مدام داده به آن اضافه میکنید، ولی هیچ‌وقت داده‌ای از آن را حذف نمی‌کنید. این باعث می‌شود حافظه‌ی کش، بدون محدودیت رشد کند و GC هم نمی‌تواند کاری کند؛ چون داده‌ها هنوز قابل دسترسی هستند.