بنية JVM والأداء

أدوات التحليل الأدائي

15 دقيقة الدرس 8 من 13

أدوات التحليل الأدائي

يخبرك القياس المعياري بمدى سرعة كودك. أما التحليل الأدائي (Profiling) فيخبرك بسبب ذلك. يتّصل المحلّل الأدائي بـ JVM قيد التشغيل ويأخذ عيّنات مستمرّة أو يعتمد التوثيق الآلي (instrumentation)، ليمنحك تفصيلًا حيًّا لوقت المعالج وتخصيصات الكومة (heap) وحالات الخيوط وجمع القمامة والتزاحم على الأقفال — وهو بالضبط ما تحتاجه لإصلاح مشكلة قستَها بالفعل.

في هذا الدرس نتناول الأدوات التحليلية الثلاث الأساسية للعمل الاحترافي في Java: Java Flight Recorder (JFR) وVisualVM وفنّ قراءة تفريغ الكومة (heap dump).

Java Flight Recorder

JFR محلّل أدائي آمن للإنتاج ومنخفض الحِمل، مدمج في JVM ذاته. جرى فتح مصدره كجزء من OpenJDK في Java 11 ويأتي مع كل JDK منذ ذلك الحين — لا تنزيل إضافي ولا عامل (agent) ولا رسوم ترخيص. يبلغ حِمله عادةً أقل من 1% في معظم أعباء العمل، ما يجعله آمنًا للتشغيل المستمر في الإنتاج.

يعمل JFR عبر تسجيل أحداث. كل نظام فرعي في JVM (GC وJIT وتحميل الأصناف و I/O للمقبس والأقفال و I/O للملفات وغيرها) يُطلق أحداثًا بطوابع زمنية وبيانات وصفية. يخزّن JFR تلك الأحداث في تنسيق ثنائي عالي الكفاءة في ذاكرة دورية (ring buffer) ويصرفها إلى ملف .jfr عند الطلب. تفتح ذلك الملف لاحقًا في JDK Mission Control (JMC) لتحليله.

بدء التسجيل

ثمّة ثلاث طرق لبدء تسجيل JFR.

1. عند تشغيل JVM (الأفضل للإمساك بالمشكلات منذ البداية):

java -XX:+FlightRecorder \ -XX:StartFlightRecording=duration=120s,filename=myapp.jfr,settings=profile \ -jar myapp.jar

2. عند الطلب عبر jcmd أثناء تشغيل التطبيق (دون إعادة تشغيل):

# ابحث عن PID أوّلًا jps -l # ابدأ تسجيلًا مدّته 60 ثانية على PID 12345 jcmd 12345 JFR.start duration=60s filename=/tmp/myapp.jfr settings=profile # أو تفريغ ما في الذاكرة الدورية في أي وقت jcmd 12345 JFR.dump filename=/tmp/snapshot.jfr

3. برمجيًا من داخل التطبيق:

import jdk.jfr.Recording; import jdk.jfr.consumer.RecordingFile; import java.nio.file.Path; import java.time.Duration; public class JfrDemo { public static void main(String[] args) throws Exception { try (Recording rec = new Recording()) { rec.enable("jdk.CPUSample").withPeriod(Duration.ofMillis(10)); rec.enable("jdk.GCHeapSummary"); rec.enable("jdk.JavaMonitorEnter"); // تزاحم الأقفال rec.start(); // ... شغّل عبء العمل ... Thread.sleep(5_000); rec.stop(); rec.dump(Path.of("workload.jfr")); } // تحليل الملف برمجيًا try (var rf = new RecordingFile(Path.of("workload.jfr"))) { while (rf.hasMoreEvents()) { var event = rf.readEvent(); System.out.println(event.getEventType().getName() + " @ " + event.getStartTime()); } } } }
settings=profile مقابل settings=default: صُمّم ملف default ليكون حِمله قريبًا من الصفر؛ يسجّل أحداث GC وبعض أحداث I/O. يضيف إعداد profile أخذ عيّنات المعالج كل 10 مللي ثانية وتحليل تخصيص الكائنات وتحليل الأقفال — تفاصيل أكثر، حِمل لا يزال منخفضًا جدًا لكن ليس صفرًا. استخدم default في الإنتاج باستمرار، وانتقل إلى profile للتحقيقات المستهدفة.

قراءة ملف JFR في JDK Mission Control

حمّل JMC من jdk.java.net/jmc (تنزيل منفصل عن JDK). افتح ملف .jfr واستكشف:

  • التحليل التلقائي — يفحص JMC التسجيل ويُبرز الشذوذات (توقّف GC طويل، نقطة تزاحم أقفال، تخصيصات مشبوهة). ابدأ من هنا.
  • تحليل الدوالّ — رسم لهب (flame graph) أو شجرة استدعاء لوقت المعالج المُعاين. الإطار الأشد سخونةً هو اختناقك الأدائي.
  • الذاكرة — تحليل التخصيص يُظهر مواقع الاستدعاء الأكثر تخصيصًا، وحجم الكومة الحيّة عبر الزمن.
  • الخيوط — جدول زمني لحالة الخيوط: أخضر = تنفيذ، أصفر = انتظار قفل، بنفسجي = نوم. الشرائط الصفراء العريضة تعني تزاحمًا.
  • جمع القمامة — كل توقّف لـ GC ونوعه ومدّته والكومة قبله وبعده والسبب الذي أطلقه.
التسجيل المستمر في الإنتاج: اضبط JFR بذاكرة دورية ذات حجم ثابت (maxsize=250m بلا duration) وتفريغها عند الطلب حين تقع حادثة — ستحصل على بيانات التحليل الأخيرة لدقائق قبل المشكلة مباشرةً، دون أن تكون خططت مسبقًا. هذا أفيد بكثير من إرفاق محلّل أدائي بعد وقوع الحادثة.

VisualVM

VisualVM محلّل أدائي مجاني قائم على واجهة رسومية يتّصل بـ JVM محلّي أو بعيد عبر JMX. يُثبَّت بشكل منفصل عن JDK (من visualvm.github.io) وهو أيسر نقطة بداية للمطوّرين الراغبين في عرض بصري وآني لعملية جارية.

ما يمنحك إيّاه VisualVM بلمحة:

  • النظرة العامة — أعلام JVM وخصائص النظام ومدة التشغيل وPID.
  • المراقبة — نسبة استخدام المعالج والكومة المستخدمة/المحجوزة وعدد الخيوط والأصناف لحظيًا. يُظهر على الفور ما إذا كانت الكومة تنمو أو يوجد ارتفاع مفاجئ في المعالج.
  • الخيوط — جدول خيوط حيّ، وتفريغ الخيوط عند الطلب. تُكتشف الإغلاقات المتبادلة (deadlocks) وتُبرز تلقائيًا.
  • أخذ العيّنات — أخذ عيّنات المعالج والذاكرة بحِمل منخفض. يُظهر أخذ عيّنات المعالج جدول الدوالّ الساخنة؛ وأخذ عيّنات الذاكرة يُظهر التخصيص حسب الصنف. استخدم هذا التبويب للتحقيقات السريعة دون إيقاف العالم.
  • التحليل الدقيق — وضع التوثيق الآلي (يُحصي كل دخول/خروج للدالّة). أكثر دقةً لكنه يُضيف حِملًا — تجنّبه على حركة الإنتاج.
  • تفريغ الكومة — أطلق تفريغًا أو استورد واحدًا واستعرض أعداد الكائنات والأحجام المحتجزة وسلاسل المراجع.
التحليل بالتوثيق الآلي يُضيف حِملًا غير هيّن. يمكن أن يُبطّئ توثيق تطبيق كبير بمقدار 5–20 ضعفًا. استخدم تبويب أخذ العيّنات للتحقيق في بيئات شبيهة بالإنتاج؛ احتفظ بتبويب التحليل الدقيق للفحص المحلي لمسارات كود معزولة.

للاتّصال بعملية بعيدة، شغّل JVM بتفعيل JMX:

java -Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=9010 \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.ssl=false \ -jar myapp.jar

في VisualVM اختر ملف ← إضافة اتصال JMX وأدخل host:9010.

لا تكشف JMX بلا مصادقة على الإنترنت أبدًا. المثال أعلاه آمن فقط داخل شبكة موثوقة أو خلف نفق SSH. للنفق: ssh -L 9010:localhost:9010 user@host، ثم اتّصل بـ localhost:9010 في VisualVM.

قراءة تفريغ الكومة

تفريغ الكومة هو لقطة لجميع الكائنات الحيّة في كومة JVM في لحظة واحدة. إنّه الأداة القاطعة لتشخيص تسرّبات الذاكرة: كل شيء على الكومة مرئي، إضافةً إلى سلاسل المراجع التي تُفسّر لماذا الكائنات لا تزال حيّة.

التقاط تفريغ الكومة

# عبر jmap (يتّصل بعملية جارية) jmap -dump:format=b,file=heap.hprof <pid> # عند OutOfMemoryError (الأأمن — تلقائي، لا حاجة لأدوات إضافية) java -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/tmp/oom-heap.hprof \ -jar myapp.jar # عبر jcmd jcmd <pid> GC.heap_dump /tmp/heap.hprof
استخدم دائمًا -XX:+HeapDumpOnOutOfMemoryError في الإنتاج. إذا تعطّل التطبيق بسبب نفاد الذاكرة ستحصل على التفريغ تلقائيًا، وهذا في الغالب الفرصة الوحيدة لمعرفة ما كان يستهلك الكومة في تلك اللحظة بالذات.

تحليل التفريغ

افتح ملف .hprof في Eclipse Memory Analyzer Tool (MAT) أو عارض تفريغ الكومة في VisualVM. المفاهيم الأساسية التي يجب فهمها:

  • الحجم الضحل (Shallow size) — الذاكرة التي يستخدمها الكائن نفسه (حقوله). مفيد لفهم تخطيط الكائن.
  • الحجم المحتجز (Retained size) — الذاكرة التي ستُحرَّر لو جُمع هذا الكائن كقمامة، أي الكائن بالإضافة إلى كل ما يمكن الوصول إليه حصريًا عبره. هذا ما يهمّ في تحليل التسرّب.
  • شجرة المهيمنين (Dominator tree) — شجرة تمثّل لكل عقدة الكائنَ الذي ستُحرَّر أكبر كمية ذاكرة بحذفه. قمّة شجرة المهيمنين تُظهر أكبر مستهلكي الذاكرة؛ هذا المكان الذي تنشأ منه التسرّبات عادةً.
  • جذور GC (GC roots) — نقاط البداية التي يتتبّع منها GC قابلية الوصول: الحقول الساكنة، ومكدّسات الخيوط، ومراجع JNI. الكائن حيّ لأن سلسلة مراجع تقوده إليه من جذر GC. التسرّب هو الرابط الذي كان يجب أن يُحذف.

في MAT شغّل تقرير Leak Suspects أوّلًا. يكتشف تلقائيًا كائنات التجميع (مجموعات أو ذاكرات تخزين مؤقّت تحتفظ بعشرات الآلاف من الإدخالات) ويتتبّعها وصولًا إلى جذر GC. ثم تنظر في سلسلة المراجع لتفهم أيّ مكوّن يحتفظ بالمرجع ولماذا لم يُحرَّر أبدًا.

تفريغات الكومة الكبيرة تستغرق وقتًا. يُنتج تفريغ كومة بحجم 4 جيجابايت ملف .hprof بحجم 4 جيجابايت (غير مضغوط). يحتاج MAT إلى ما يقارب 1–1.5 ضعف ذلك كذاكرة Java لتحليله. استخدم MAT بنسخة 64-بت وامنحه على الأقل -Xmx6g عند تحليل تفريغات كبيرة. عارض تفريغ الكومة المدمج في VisualVM يعمل بشكل أفضل مع تفريغات أصغر (أقل من 1 جيجابايت).

اختيار الأداة المناسبة

  • JFR + JMC — التحليل الأدائي المستمر في الإنتاج، توقيت دقيق، حِمل ضئيل، نظام أحداث ثريّ. الخيار الأوّل للتحقيقات الإنتاجية.
  • VisualVM — سريع، بصري، رائع للتطوير المحلي والفحوصات السريعة على التجهيز. سهل لمشاركة النتائج عبر لقطات الشاشة.
  • تفريغ الكومة + MAT/VisualVM — تشخيص تسرّبات الذاكرة وفهم أشجار الكائنات الحيّة. ليست أداة آنية؛ تُستخدم بشكل رجعي.

تجمع سير عمل الأداء الناضج بين الثلاثة: شغّل JFR باستمرار، واستخدم VisualVM للاستكشاف الحيّ أثناء التطوير، واجعل التقاط تفريغ الكومة تلقائيًا عند نفاد الذاكرة.