پلتفرمهای مدرن برنامهنویسی، توسعهدهندگان را از نگرانیهای مربوط به مدیریت اشیاء در حافظه بینیاز کردهاند. محیطهای زمان اجرا (Runtime Environments) بهطور خودکار وظایف ایجاد اشیاء و پاکسازی حافظه را بر عهده دارند. با این حال، بازیابی حافظه نباید خیلی زود انجام شود. جمعآورندههای زباله (Garbage Collectors) مدرن با حذف اشیاء غیرضروری برای آزادسازی حافظه یا ارتقاء (Promoting) آنها به نسلهای قدیمیتر عمل میکنند. در نتیجه، اشیاء از فرآیند جمعآوری زباله جان سالم به در میبرند و به مرور زمان به نسلهای قدیمیتر منتقل میشوند. اما سوال اینجاست که چه تعداد نسل حافظه باید وجود داشته باشد؟ فرآیند جمعآوری زباله هر چند وقت یکبار باید انجام شود؟ و چرا نمیتوانیم از یک فضای حافظه واحد برای همه اشیاء استفاده کنیم؟
طول عمر متفاوت اشیاء
ابتدا باید در نظر بگیریم که چرا اشیاء نمیتوانند بلافاصله پس از استفاده حذف شوند. اشیاء را میتوان بر اساس طول عمرشان (Lifetime) – مدتی که در حال استفاده هستند – گروهبندی کرد. برخی از اشیاء بسیار کوتاه عمر هستند، مانند متغیرهای محلی (Local Variables)، اشیاء موقت ایجاد شده در داخل حلقهها (Loops) یا مقادیر بازگشتی متدها (Method Return Values). سایر اشیاء، مانند لیستها (Lists)، حافظههای نهان (Caches) یا دادههای وضعیت خاص (Certain State Data)، ممکن است در طول یک جلسه تعامل سرویس گیرنده-سرویس دهنده (Client-Server Interaction) زنده بمانند. و در نهایت، اشیائی وجود دارند که برای کل طول عمر برنامه پایدار میمانند، مانند تکنمونهها (Singletons)، تنظیمات پیکربندی (Configuration Settings) و اشیاء مدیریت شده توسط چارچوبها (Frameworks).
اشیاء مختلف برای مدت زمانهای متفاوتی توسط یک برنامه استفاده میشوند، بنابراین قرار دادن همه آنها در یک ناحیه حافظه واحد که جمعآورنده زباله باید بهطور مداوم آن را اسکن کند، ناکارآمد خواهد بود. در چنین ساختاری، اشیاء با عمر طولانی تقریباً پس از هر جمعآوری زباله بدون تغییر باقی میمانند، اما جمعآورنده زباله همچنان زمان خود را برای تجزیه و تحلیل آنها تلف میکند.
فرضیه نسل بندی حافظه (Generational Hypothesis)
در سال 1984، دیوید اونگار (David Ungar) رویکرد جدیدی را برای جمعآوری زباله پیشنهاد کرد که از نسلهای حافظه استفاده میکرد. دیوید اونگار به خاطر کارش بر روی زبان اسمالتاک (Smalltalk) شناخته شده است و همچنین یکی از خالقان زبان سلف (Self) است. او در توسعه ماشینهای مجازی (Virtual Machines) نیز مشارکت داشته است.
جمعآورندههای زباله موجود در آن زمان اغلب سرعت اجرای برنامه را کاهش میدادند زیرا مجبور بودند کل فضای حافظه را برای یافتن زباله اسکن کنند. این کاهش سرعت به ویژه در سیستمهای با رابط کاربری تعاملی (Interactive User Interfaces) قابل توجه بود.
مشاهدات کلیدی اونگار عبارت بودند از:
- اکثر اشیاء عمر کوتاهی دارند.
- اگر یک شیء زنده بماند، احتمالاً برای همیشه زنده خواهد ماند.
این مشاهدات او را به ایده تقسیم حافظه به نسلها سوق داد.
نسلهای بیشتر؟
در مقاله خود، اونگار تنها دو نسل را توصیف کرد: اشیاء جوان (Young Objects) و اشیاء پیر (Old Objects). برای نیازهای آن زمان، این رویکرد کافی بود و مدیریت حافظه به تفکیک دقیقتری نیاز نداشت.
با بزرگتر شدن برنامهها، دو نسل دیگر کافی نبود. همه اشیاء در مدل "یا بلافاصله میمیرند یا برای همیشه زنده میمانند" قرار نمیگرفتند. برخی از آنها چندین بار از جمعآوری زباله جان سالم به در میبردند اما در نهایت پس از تغییرات در وضعیت کاربر میمردند. چنین اشیائی در برنامههای تعاملی بزرگ، سیستمهای دارای حافظه نهان (Caching) و پردازش دادههای جریانی (Streaming Data Processing) رایج هستند.
برای بهینهسازی عملکرد در چنین برنامههایی، یک نسل میانی بین نسل جوان و نسل پیر اضافه شد. نسل میانی به جلوگیری از آلوده شدن نسل پیر با اشیاء "شبه پیر" (Pseudo-Old Objects) – اشیائی که تنها یک بار از جمعآوری زباله جان سالم به در بردهاند – کمک میکند. این احتمال وجود دارد که آنها در جمعآوری زباله بعدی زنده نمانند. این رویکرد به نسل پیر، که برای اشیاء با عمر طولانی در نظر گرفته شده است، اجازه میدهد تا کندتر پر شود. پاکسازی اضافی در نسل پیر مفید نخواهد بود، زیرا بسیاری از اشیاء همچنان زنده خواهند بود. هرچه اشیاء کمتری به یک نسل منتقل شوند، دفعات کمتری نیاز به پاکسازی آن نسل وجود خواهد داشت.
نحوه عملکرد جمعآوری زباله در سیستمهای مدرن
داتنت از مدلی با سه نسل استفاده میکند. اشیاء جدید در نسل 0 قرار میگیرند. هنگامی که فضای نسل 0 تمام میشود، جمعآورنده زباله اشیاء استفاده نشده را حذف کرده و اشیاء زنده باقیمانده را به نسل 1 ارتقاء میدهد. نسل 1 نیز به همین ترتیب پاکسازی میشود. اشیاء موجود در نسل 2 منتقل نمیشوند؛ آنها یا حذف میشوند یا در همانجا باقی میمانند.
اندازههای نسلها به این صورت مرتبط هستند: اندازه نسلها از نسل 0 به نسل 2 افزایش مییابد. دلیل این امر این است که بیشتر اشیاء ایجاد شده و قرار گرفته در نسل 0 تقریباً بلافاصله به زباله تبدیل میشوند. نگهداری چنین اشیائی برای مدت طولانی در حافظه فایدهای ندارد. علاوه بر این، هرچه اندازه نسل کوچکتر باشد، فرآیند جمعآوری زباله در آن نسل بیشتر اتفاق میافتد.
سایر پلتفرمها نیز ساختاری مشابه دارند. شایان ذکر است که آنها همچنین شامل نواحی اضافی در هیپ (Heap) هستند. به عنوان مثال، در داتنت، هیپ اشیاء بزرگ (Large Object Heap - LOH) برای اشیاء بزرگ وجود دارد. در این مقاله، ما بهطور خاص بر حافظه مورد استفاده برای رایجترین اشیاء تمرکز میکنیم که به نسلها تقسیم میشود.
جمعآوری زباله در ماشین مجازی جاوا (JVM)
جمعآورنده زباله در جاوا را میتوان بهطور جداگانه در نظر گرفت. بیایید نگاهی به نسلهایی بیندازیم که اشیاء پس از جمعآوری زباله به آنها منتقل میشوند. دو نسل اصلی وجود دارد: نسل جوان (Young) و نسل پیر (Old). نسل جوان از فضای ادن (Eden Space) و فضاهای بازمانده (Survivor Spaces - S0 و S1) تشکیل شده است. یک شیء جدید در ادن ایجاد میشود. هنگامی که ادن سرریز میشود، تمام اشیاء بازمانده به فضاهای بازمانده منتقل میشوند. هدف از فضاهای بازمانده این نیست که اشیاء را به سرعت به نسل پیر منتقل کنند، بلکه به آنها فرصت میدهند تا مدتی "بنشینند". اشیاء از S0 به S1 منتقل میشوند. تعداد دفعاتی که یک شیء از جمعآوری زباله جان سالم به در برده است، پیگیری میشود. اگر یک شیء به اندازه کافی از این انتقالات جان سالم به در ببرد، به نسل پیر ارتقاء مییابد. بهطور خلاصه، این چهار فضا را میتوان در سه دسته گروهبندی کرد: جوان، غربالگر (Sifting) و پیر.
نتیجهگیری
اکنون منطق استفاده از سه نسل حافظه را بررسی کردهایم. این تعداد بر اساس چندین مشاهده استوار است. اکثر اشیاء کوتاه عمر هستند – حدود 90٪ از کل اشیاء برنامه تقریباً بلافاصله به زباله تبدیل میشوند. هرچه یک شیء بیشتر عمر کند، احتمال زنده ماندن آن حتی بیشتر میشود. با این حال، تشخیص بین اشیائی که صرفاً یک بار از جمعآوری جان سالم به در بردهاند و اشیائی که واقعاً متعلق به نسل با عمر طولانی هستند، مهم است. نسل میانی (یا قدیمیترین بخش نسل جوان) به عنوان یک غربال عمل میکند و اشیاء با عمر متوسط را فیلتر میکند تا آنها زودتر از موعد وارد نسل پیر نشوند. بنابراین، استفاده از سه نسل حافظه نشان دهنده یک مصالحه بین سرعت، کارایی و پیچیدگی پیادهسازی است.
اگر تنها یک نسل وجود داشت، به سرعت با اشیاء کوتاه عمر پر میشد و منجر به جمعآوری زباله مکرر میگردید. با این حال، این جمعآوریها بر تمام اشیاء برنامه تأثیر میگذاشت و به طور قابل توجهی بر عملکرد تأثیر میگذاشت.
اگر تنها دو نسل وجود داشت، لایهای برای ردیابی اشیائی که بلافاصله نمیمیرند وجود نداشت. این امر منجر به پر شدن سریعتر نسل پیر میشد و در نتیجه جمعآوری زباله مکرر در آن اتفاق میافتاد. این امر ناکارآمد خواهد بود، زیرا بیشتر اشیاء برای مدت طولانی زنده میماندند. در نتیجه، جمعآوری زباله پرهزینه بیشتر اتفاق میافتاد.
اگر بیش از سه نسل وجود داشت، هر نسل اضافی الگوریتمهایی را برای ارتقاء اشیاء اضافه میکرد، پیچیدگی جمعآورنده زباله را افزایش میداد و هیچ سود قابل توجهی ارائه نمیکرد.