در ماشین مجازی جاوا (JVM)، به پردازههای در حال اجرا روی سیستم عامل حافظهای اختصاص داده میشود که به اصطلاح به آن حافظهی Native گفته میشود. این فضا شامل بخشهایی از جمله Heap، دادهساختارهای مدیریت Heap، حافظهی Stack، تعاریف کلاسها و هر چیز دیگری است که JVM برای کار کردن به آن نیاز خواهد داشت. به طور خاص، حافظهی Heap یک فضای ذخیرهسازی زمان اجرا است که JVM، آبجکتهای مختلف را به محض ایجاد در آن ذخیره میکند و در پردازش فراخوانیها نیز به آبجکتهای Heap ارجاع میدهد. موجودیتهایی مانند فراخوانیها، متغیرهای محلی، تعاریف کلاسها و متادادهها در بخشهای دیگر حافظه در JVM یا به اصطلاح، حافظهی Non-Heap ذخیره میشوند. گرچه JVM به صورت خودکار حافظه را مدیریت میکند، اما در مواردی لازم میشود کاربر با مداخله در تنظیمات عملکرد آن را بهبود بخشد و خطاهای مربوط به حافظه (مانند java.lang.OutOfMemoryError) را حل کند. با توجه به اهمیت حافظهی Heap در عملکرد JVM، در این مقاله کارکرد و ساختار آن را دقیقتر بررسی میکنیم. سپس برخی از شاخصهای مانیتورینگ مرتبط در پلتفرم مانیتورینگ معین را معرفی میکنیم.
حافظهی Heap همزمان با شروع به کار JVM ایجاد میشود و بین تمام thread های JVM مشترک است. اندازهی Heap را میتوان در حین اجرای برنامهها نیز به صورت پویا تغییر داد. به طور کلی، زمانی که Heap پر میشود، garbage collection رخ میدهد تا آبجکتهایی که دیگر به آنها ارجاعی صورت نمیگیرد، از Heap پاک شوند و فضای خالی برای آبجکتهای جدید ایجاد شود. وقتی garbage collection اتفاق میافتد، تمام تردهای برنامه متوقف میشوند، فضای حافظه از آبجکتهای بلااستفاده آزاد میشود، و احتمال دارد اندازهی Heap نیز در برخی موارد مجددا تنظیم شود. سپس تردهای برنامه میتوانند کار خود را از سر بگیرند. برای بررسی دقیقتر سازوکار Heap، در ادامه تقسیمبندیهای آن و اثر اندازهی بخشهای مختلف آن در عملکرد برنامهها را بررسی میکنیم.
معمولا Heap به دو ناحیه (generation) تقسیم میشود که young generation و old generation نامیده میشوند. فضای young به آبجکتهای جدید تخصیص پیدا میکند و زمانی که پر شود، garbage collection اتفاق میافتد و آبجکتهایی که به اندازهی کافی در فضای young بودهاند، به فضای old که tenured space نیز نامیده میشود، میروند (young/minor collection). زمانی که فضای old پر شود، یک garbage collection کامل رخ میدهد (old/major collection). دلیل در نظر گرفتن ناحیهی young این است که بسیاری از آبجکتها برای افق زمانی کوتاهی استفاده میشوند. Young collection آبجکتهای جدیدی را که هنوز مورد ارجاع (زنده) باشند پیدا میکند و آنها را از فضای Young خارج میکند. معمولا این نوع از garbage Collection حافظهی heap را سریعتر از old collection (و نیز حالتی که heap اصلا فضای young نداشته باشد) خالی میکند. خود ناحیهی Young نیز از دو بخش Eden space و Survivor space (شامل S0 و S1) که Keep space نیز خوانده میشود، تشکیل میشود. آبجکتهای جدید وقتی در کد تعریف میشوند، ابتدا وارد فضای Eden میشوند و سپس به تدریج و پس از زنده ماندن طی چند دوره garbage collection، به نواحی Survivor منتقل میشوند. در واقع بخشهای survivor آبجکتهای جدید را که تا young collection بعدی جمعآوری نمیشوند در خود نگه میدارد. این کار باعث میشود آبجکتهایی که با فاصلهی زمانی بسیار اندکی قبل از young collection فعلی وارد Heap شدهاند به سرعت به بخش Old منتقل نشوند.
یک بخش دیگر به نام Permanent Generation نیز تا قبل از Java 8 بخشی از Heap به شمار میرفت. متادادههایی مربوط به کلاسها و متدها را در بر میگرفت (توجه کنید که آبجکتهای ناحیهی Old پس از سپری شدن زمان معین وارد ناحیهی Permanent نمیشدند و کارکرد این ناحیه فرق داشت). از جایی که این ناحیه اندازهی کوچک و محدودی داشت، وقتی کلاسهای داینامیک زیادی در آن بارگذاری میشدند باعث خطای OutOfMemoryError میشد، از Java 8 به بعد، این ناحیه از Heap حذف شد و به جای آن Metaspace معرفی شد که متادادهی کلاسها را در حافظهی Native سیستم عامل خارج از Heap نگهداری میکند، و مدیریت اندازه و garbage collector در آن کارآمدتر است.
یکی از مواردی که بر سرعت تخصیص حافظه، و نیز تعداد و مدت زمان garbage collection اثر میگذارد، اندازهی Heap است. اگر Heap کوچک باشد، به سرعت پر میشود و باید به دفعات بیشتری garbage collection در آن رخ دهد. همچنین احتمال دارد fragmentation بیشتر در آن رخ دهد که باعث میشود تخصیص آبجکت کندتر شود. از طرفی اگر اندازهی Heap بزرگ باشد، باعث overhead در garbage collection میشود. اگر Heap از حافظهی فیزیکی موجود در سیستم بزرگتر باشد، ناگزیر ذخیرهسازی به فضای دیسک کشیده میشود و در نتیجه، دسترسی به اطلاعات Heap زمان بیشتری طول میکشد. این موضوع باعث میشود garbage collection تاخیرهایی طولانی در برنامهها ایجاد کند.
به طور کلی سربار ناشی از یک Heap بزرگتر ، به علت مزیتی که به واسطهی کاهش دفعات garbage Collection و افزایش تخصیص حافظه به آبجکتها حاصل میشود، کمتر است (البته مادامی که Heap از دیسک استفاده نکند). پس مقداری برای اندازهی Heap مناسب است که در محدودهی حافظهی فیزیکی موجود به اندازهی کافی بزرگ باشد.
در رابطه با تقسیمبندی فضای داخلی Heap، اندازهی فضای Young مانند اندازهی کلی Heap بر سرعت و دفعات garbage collection و نیز سرعت تخصیص حافظه به آبجکتها مؤثر است. اگر اندازهی این فضا کوچک باشد، به سرعت پر میشود و garbage collection نیز باید بیشتر انجام شود. در مقابل، garbage collection برای یک فضای بزرگ بیشتر طول میکشد. بهترین اندازه برای فضای Young مقداری است که باعث شود garbage collection تا حد ممکن در فضای young انجام شود و کمتر به فضای old کشیده شود. معمولا این مقدار نصف فضای آزاد Heap در نظر گرفته میشود. پس اندازهی بهینه نسبتا بزرگ است و این عامل میتواند به افزایش مدت زمان young collection منجر شود. از آنجایی که تمام تردهای جاوا در حین young collection متوقف میشوند، اینجا مصالحهای در تنظیم اندازهی فضای young پیش میآید. شاید در مواردی تصمیم گرفته شود که اندازهی فضای مورد نظر کمتر از مقدار بهینه تنظیم شود تا توقفهای ناشی از young collection کمتر شوند.
اندازهی فضای Heap هم در young collection و هم در old collection مؤثر است. اگر این فضا بزرگ باشد، میتواند young collection هایی با فرکانس بالا را به دنبال داشته باشد؛ در حالی که اگر فضای Heap بیش از حد کوچک باشد، فرکانس old collection ها بیشتر میشود و آبجکتها زودتر به مرحلهی بعدی وارد میشوند.
برای تنظیم اندازهی بخشهای مختلف حافظه در جاوا از flagهایی استفاده میشود. برای مثال، با «Xms-» میتوان اندازهی ابتدایی Heap حین شروع به کار JVM را تنظیم کرد؛ با «Xmx-» میتوان بیشینهی اندازهی Heap را تعیین کرد؛ و با «Xmn-» میتوان اندازهی ناحیهی Young را مشخص کرد. همچنین با «XX:NewRatio-» میتوان نسبت ناحیهی Old به Young و با «XX:SurvivorRatio-» میتوان نسبت هر فضای survivor به eden را تنظیم کرد.
همچنین برای اطلاع از میزان مصرف حافظه میتوان با استفاده از متد MemoryMXBean.getHeapMemoryUsage() در جاوا مقادیر زیر را در مورد حافظهی Heap خواند:
-init: اندازهی حافظهی مورد نیاز JVM برای شروع به کار را مشخص میکند. سیستم عامل در ابتدا باید با توجه به این مقدار به JVM حافظه اختصاص دهد. در ادامه به تناسب نیاز میتوان حافظه اشغال یا آزاد کرد. این مقدار میتواند تعریفنشده باشد.
-used: میزان حافظهی مورد استفادهی فعلی را نشان میدهد.
-committed: میزان حافظهای را نشان میدهد که تضمین میشود برای استفاده در JVM در دسترس باشد. این مقدار همیشه اندازهای حداقل برابر با اندازهی حافظهی used خواهد داشت و میتواند در طول زمان تغییر کند.
-max: بیشترین مقدار حافظهای را نشان میدهد که میتوان در کل برای مدیریت حافظه استفاده کرد. البته این مقدار میتواند تعریفنشده هم باشد. اگر تعریف شود، مقادیر used و committed همواره از این مقدار کمتر یا مساوی آن خواهند بود. در کل تضمینی وجود ندارد که سیستم عامل همواره بتواند این مقدار را به JVM اختصاص دهد؛ در حالی که حافظهی committed برای JVM اشغال شده و برای ذخیرهسازی آبجکتها در دسترس است.
در نهایت، میتوان گفت دو عامل کلیدی در عملکرد موفق Heap در JVM عبارتند از:
-سربار garbage collection: نسبتی از زمان که در حالت توقف در حین GC صرف میشود، در مقابل زمانی که صرف اجرای برنامه میشود.
-زمان توقف در garbage collection: مدت زمانی که توقف تردها در چرخههای garbage collection طول میکشد.
بسته به هدف و برنامهی مورد نظر، نیازمندیهای متفاوتی روی مقدار این دو عامل مطرح میشوند. برای مثال، در برنامههای بلادرنگ زمان توقف میتواند مهمتر از سربار garbage collection باشد. به طور کلی، میتوان به سربار کمتر از یک درصد و زمان توقف زیر یک ثانیه رسید. معمولا سربار بیش از ۱۰ درصد و زمان توقف بیش از دو ثانیه قابل کاهش هستند.
در سامانهی معین امکان مانیتورینگ JVM فراهم شده است و در این راستا، شاخصهایی برای نظارت بر حافظهی Heap و نیز Garbage Collector معرفی شدهاند. از جملهی این شاخصها میتوان به میزان و درصد استفاده از حافظهی Heap و نیز حافظهی Committed در آن اشاره کرد.
همچینین شاخصهایی از جمله تعداد، نرخ و متوسط زمان garbage collection نیز در داشبورد معین قابل دسترسی هستند و با توجه به آنها میتوان در صورت لزوم بهینهسازیهای لازم را در عملکرد JVM ایجاد کرد. مطابق توضیحات بالا، یکی از راههای بهینهسازی میتواند تنظیم اندازهی حافظهی Heap باشد.
● https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/garbage_collect.html
● https://www.geeksforgeeks.org/metaspace-in-java-8-with-examples/
● https://sematext.com/glossary/jvm-heap/
● https://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/geninfo/diagnos/memman.html
● https://sematext.com/blog/java-garbage-collection/
● https://www.ibm.com/docs/en/integration-bus/10.0?topic=development-jvm-heap-sizing
● https://docs.oracle.com/javase/8/docs/api/java/lang/management/MemoryMXBean.html#getHeapMemoryUsage–
● https://stackoverflow.com/questions/41468670/difference-in-used-committed-and-max-heap-memory