Java Garbage Collection
GC با آزاد کردن خودکار حافظه باعث سریعتر شدن برنامه نویسی میشه و از نوشتن کدهای اضافه جلوگیری میکنه. JVM خودش حافظه بازیافت میکنه و برای این کار کلی شی رو مجبوره حذف کنه که این کار مشکلات جدی در عملکرد برنامه ایجاد میکنه. برای کم کردن این مشکلات لازم هستش که درک کاملی راجع به اون داشته باشیم. GC توی یک Daemon Thread توی JVM در حال اجراست. دائما در حال بررسی heap هستش و اشیایی که خیلی وقته به از اونها استفاده نکردیم رو پیدا میکنه. سپس اونها رو نابود میکنه و حافظه اونها رو برای استفاده مجدد آزاد میکنه.
GC سه تا کار انجام میده:
- Mark: مشخص کردن اشیای که دارن استفاده میشن و اونایی که استفاده نمیشن.
- Normal Deletion: پاک کردن اشیایی که استفاده نمیشن و آزاد کردن حافظه اونها.
- Deletion with Compacting: انتقال باقی اشیا به یکی از فضاهای survive. برای بالا بردن پرفورمنس اضافه کردن اشیای جدید.
این فرایند دو تا مشکل داره:
- کارآمد نیست چون طبیعتا همه اشیای جدید بلا استفاده خواهند شد.
- همه اشیای با طول عمر بالا به احتمال زیاد در دوره بعدی GC در حال استفاده شدن هستن.
برای حل این مشکلات، اشیای جدید رو به صورت مرتب شده میریزیم توی یک فضای جداگانه توی heap و هر کدوم از فضاهای توی heap طول عمر مشخصی دارن. GC دارای دو تا فاز هستش minor و major که اشیا رو بررسی میکنن و اونها رو به فضای مناسب انتقال میده قبل از اینکه حذف بشن.
Mark-Sweep-Compact: اصلی ترین قسمت پیاده سازی شده در GC هستش و دو تا فاز اصلی داره:
- Mark: از اشیای root تو GC شروع میکنه و هر شی که استفاده میشه (رفرنسی ازش هستش) رو علامت میزنه. باقی اشیا اشغال در نظر گرفته میشن.
- Sweep: توی heap بین اشیای زنده دنبال فضاهای خالی میگرده و لیست اونها رو برای اینکه در آینده به یک شی دیگه تخصیص داده بشه جمع آوری میکنه.
- Compact: یک فاز دیگه هم داریم که داره رو کنار هم جمع میکنه و از تکه تکه شدن حافظه جلوگیری میکنه.
GC Roots: همون جوری که میدونیم اگه یک شی هیچ رفرنسی نداشته باشه یعنی در دسترس کدهای برنامه نیست و این یعنی برای حذف شده توسط GC واجد شرایطه.
اما چند تا سوال مهم وجود داره:
هیچ رفرنسی یعنی چی؟ اصلا رفرنس چیه؟ اولین رفرنس چیه و از کجا اومده؟
برای جواب به این سوالات باید ی نگاهی به چگونگی رفرنس دادن در دسترس بودن بندازیم.
برای اینکه برنامه ما به یک شی دسترسی داشته باشه، باید یک شی root وجود داشته باشه که شی ما بهش وصل باشه و از خارج از heap هم در دسترس باشه به اشیای میگیم GC Roots. ما کلی GCR داریم، برای متغیرهای محلی، برای ثابتها، برای thread های جاوا و برای خیلی از چیزای دیگه که یک لیست خیلی بزرگی میشه. که مهم نیستش واقعا. نکته مهم این هستش که آیا شی ما به صورت مستقیم یا غیر مستقیم یک رفرنس به یک GCR داره یا نه! تا زمانی که GCR زنده باشه GC شی ما رو در دسترس در نظر میگیره ولی در لحظه ای که شی ما رفرنسش رو به GCR از دست میده و غیرقابل دسترس میشه شرایط لازم رو برای حذف شدن بدست میاره. GC فقط اشیایی را که در دسترس نباشد (unreachable) نابود میکند. این یک فرایند خودکار در پسزمینه ست و به صورت کلی برنامه نویسها با این قسمت کاری ندارن.
نکته: قبل از اینکه GC یک شی رو نابود کنه متد finalize اون رو صدا میزنه و این متد فقط یکبار صدا میشه. به صورت پیشفرض این متد خالی هستش و چیزی توش نیست ولی ما میتونیم اون رو بازنویسی کنیم و یکسری تمیز کاری انجام بدیم. مثلا بستن کانکشن دیتابیس یا بستن فایلی که داشتیم روش کار میکردیم. زمانی که متد finalize به اتمام برسه GC شی رو نابود میکنه.
میشه اشیا رو خیلی سریع از دسترس خارج کنیم و کار رو برای GC راحت کنیم و صبر نکنیم که توی heap مسن بشن.
Nullifying the reference variable: با null کردن رفرنس یک شی اون از دسترس خارج میشه.
Person p = new Person();
p = null;
Re-assigning the reference variable: با تغییر رفرنس یک شی به یک ش دیگه، شی اول از دسترس خارج میشه.
Person p1 = new Person();
Person p2 = new Person();
p1 = p2;
Object created inside the method: همون جوری که قبلا راجع بهش کامل صحبت کردیم، وقتی یک متد صدا زده میشه یک frame ساخته میشه و میره توی stack مربوط به thread ما و وقتی متد به پایان میرسه اون frame از stack حذف میشه (pop) و اشیای مربوط به اون متد هم رفرنسشون رو از دست میدن و از دسترس خارج میشن.
void foo(){ Person p = new Person(); }
Anonymous object: وقتی ما یک شی ایجاد کنیم ولی رفرنسی از نگهداری نکنیم یعنی عملا ما به اون شی دسترس نداریم. تو این حالت شی ما از دسترس خارج هستش.
new Person();
Island of Isolation: وقتی تعدادی تا شی داریم که به هم رفرنس دارن ولی هیچ رفرنسی بیرون از اونها وجود نداره. توی این حالت اون اشیا از دسترس خارج هستن.
Person p1 = new Person();
Person p2 = new Person();
Person p3 = new Person();
p1.x = p2;
p2.x = p3;
p3.x = p1;
p1 = p2 = p3 = null;
زمانی که اشیا از دسترس خارج میشن، واجد شرایط برای نابود شدن هستن ولی این به این معنی نیست که اونها همون لحظه نابود میشن. GC در زمان های مشخصی اجرا میشه و اشیا غیر قابل دسترس رو نابود میکنه. به هر حال ما خودمون هم میتونیم با صدا زدن متد System.gc یا Runtime.getRuntime.gc اون رو فراخوانی کنیم ولی باز هم این به معنی نیست که ۱۰۰ در ۱۰۰ اشیای غیر قابل دسترس در همون لحظه نابود میشن.
GC Execution Strategies
- Serial GC: یک mark-sweep خیلی ساده با young gen و old gen. مناسب برای برنامههای ساده که قرار روی سیستمهای معمولی اجرا بشن. با ram پایین و cpu ضعیف. با XX:+UseSerialGC میشه بهش دسترسی داشت.
- Parallel GC: این استراتژی به صورت موازی (multi thread) چندتا mark-sweep روی minor GC داره و major GC همچنان یکی هستش. با XX:+UseParallelGC میشه بهش دسترسی داشت و با XX:ParallelGCThreads=n میشه تعداد thread هاش رو مشخص کرد و به صورت پیش فرض n برابر با تعداد هسته های cpu هستش.
- Parallel Old GC: مثل استراتژی قبلی هستش فقط major GC هم به صورت موازی (multi thread) اجرا میشه. با XX:+UseParallelOldGC میشه بهش دسترسی داشت.
- Concurrent Mark Sweep: خوب GC به صورت معمول با ایجاد یک مکث در برنامه کار خودش رو انجام میده که این مکث برای major GC بیشتر هستش و این یک مشکل بزرگ توی برنامههایی هستش که باید قابلیت پاسخگویی بالایی داشته باشن و ما نمیتونیم این زمان مکث رو توی برنامه قبول کنیم. این استراتژی با افزایش اجرای major GC همزمان توی thread برنامه این مکث رو به حداقل میرسونه البته manor GC از همون الگوریتمهای موازی استراتژی قبلی استفاده میکنه و هیچ پردازش موازی رو توی thread برنامه انجام نمیده. با XX:+UseConcMarkSweepGC میشه بهش دسترسی داشت و با XX:ParallelCMSThreads=n میشه تعداد thread هاش رو مشخص کرد
- G1 Garbage Collector: استراتژی G1 حافظه heap را به چندین بخش مساوی تقسیم میکنه (دیگه young gen و old gen نداریم) و زمانی که GC فراخوانی میشه ابتدا بخشی که داده کمتری داره رو پردازش میکنه. این پردازش موازی، همزمان و تدریجی هستش و مکث بسیار کمی داره و احتمالا جایگزین CMS بشه. با XX:+UseG1GC میشه بهش دسترسی داشت.
- Shenandoah GC: این یک استراتژی با زمان مکث بسیار پایین هستش. با اجرای همزمان چندین GC با برنامه در حال اجرا زمان توقف برنامه بخاطر پردازش های GC رو کاهش داده. این استراتژی همه کارها رو موازی انجام میده حتی فشرده سازی دادهها رو. این یعنی زمان مکث رابطه مستقیم با اندازه حافظه نداره و زمان کار این استراتژی روی یک حافظه ۲۰۰ گیگی خیلی نزدیک به کار روی یک حافظه ۲ گیگی هستش. با XX:+UseShenandoahGC میشه بهش دسترسی داشت.
- Z GC: این یک استراتژی با زمان تاخیر خیلی پایین هستش که میگه برای حافظه های ترابایتی استفاده میشه و در بدترین حالت زمان تاخیرش چند میلی ثانیه هستش. با XX:+UseZGC میشه بهش دسترسی داشت.
Memory Leaks: اصلی ترین دلیلی که ما بررسی میکنیم GC چجوری کار میکنه این هستش که Memory Leaks رو بهتر شناسایی کنیم. خیلی خلاصه GC راه میوفته اشیای زنده رو پیدا میکنه و اونایی که ازشون استفاده نمیشه رو حذف میکنه و حافظه اونها رو برای بعد آزاد میکنه. اما بعضی وقتها برنامه نویسها یکجوری یک شی غیر قابل استفاده رو رفرنس میکنن و رفرنس اشیا رو میپیچونن و به هم گره میزنن که GC متوجه نمیشه در دسترس نیست و اون رو توی حافظه نگهداری میکنه. موندن این اشیای بی استفاده توی حافظه heap رو ناکارامد میکنه و به این اتفاق میگن نشت حافظه یا memory leak.
پیدا کردن memory leak توی پروژه های بزرگ که کابوسه. یکسری ابزار پیچیده و خفن تحلیل کد داریم که کمک میکنن memory leak ها رو پیدا کنیم ولی اونها هم فقط قسمتهای مشکوک کد مشخص میکنن. پس فقط توصیه میکنم وقتی دارین با رفرنس اشیا بازی میکند مراقب باشید چون بعدا پیدا کردن و درست کردن مشکلات نشت حافظه پوستتون رو میکنه.