مرحبا بالجميع!
تجربتنا مع الخطوات في دورة
Java Developer مستمرة ، ومن الغريب ، حتى بنجاح تام (نوعًا ما): كما اتضح ، فإن التخطيط للرافعة المالية لبضعة أشهر مع الانتقال التالي إلى خطوة جديدة في أي وقت مناسب هو أكثر ملاءمة مما لو خصص ما يقرب من ستة أشهر لهذه الدورة الصعبة. لذلك هناك شك في أن الدورات المعقدة هي بالضبط ما سنبدأ في نقله ببطء إلى مثل هذا النظام.
ولكن أنا عننا ، عن أوتسوفسكي ، أنا آسف. كما هو الحال دائمًا ، نواصل دراسة مواضيع مثيرة للاهتمام ، على الرغم من أنها لم يتم تناولها في برنامجنا ، ولكن تمت مناقشتها معنا ، لذلك قمنا بإعداد ترجمة للمقال الأكثر إثارة للاهتمام في رأينا حول أحد الأسئلة التي طرحها مدرسونا.
دعنا نذهب!

المجموعات في JDK هي تطبيقات المكتبة القياسية للقوائم والخرائط. إذا نظرت إلى لقطة من تطبيق Java كبير نموذجي ، فسترى الآلاف أو حتى الملايين من حالات
java.util.ArrayList
و
java.util.HashMap
وما إلى ذلك. المجموعات لا غنى عنها لتخزين البيانات ومعالجتها. ولكن هل فكرت يومًا في ما إذا كانت جميع المجموعات في تطبيقك تستخدم الذاكرة على النحو الأمثل؟ بمعنى آخر ، إذا تعطل تطبيقك مع
OutOfMemoryError
المخزي أو تسبب في توقفات طويلة في جامع البيانات المهملة ، هل راجعت المجموعات المستخدمة من التسريبات.
أولاً ، تجدر الإشارة إلى أن مجموعات JDK الداخلية ليست نوعًا من السحر. إنها مكتوبة بلغة جافا. يأتي كود المصدر الخاص بهم مع JDK ، بحيث يمكنك فتحه في IDE الخاص بك. يمكن أيضًا العثور على رمزهم بسهولة على الإنترنت. وكما اتضح ، فإن معظم المجموعات ليست أنيقة للغاية من حيث تحسين حجم الذاكرة المستهلكة.
خذ بعين الاعتبار ، على سبيل المثال ، واحدة من أبسط المجموعات وأكثرها شعبية - فئة
java.util.ArrayList
. داخليًا ، تعمل كل
ArrayList
مع مصفوفة من
Object[] elementData
. هذا هو المكان الذي يتم فيه تخزين عناصر القائمة. دعونا نرى كيف تتم معالجة هذا الصفيف.
عند إنشاء
ArrayList
باستخدام المُنشئ الافتراضي ، أي استدعاء
new ArrayList()
، تشير
elementData
إلى مصفوفة عامة ذات حجم صفري (يمكن أيضًا تعيين
elementData
على قيمة
null
، ولكن الصفيف يوفر بعض مزايا التنفيذ الثانوية). عند إضافة العنصر الأول إلى القائمة ،
elementData
مصفوفة فريدة حقيقية من
elementData
، ويتم إدراج الكائن المتوفر فيه. لتجنب تغيير حجم المصفوفة في كل مرة ، عند إضافة عنصر جديد ، يتم إنشاؤه بطول يساوي 10 ("السعة الافتراضية"). لذلك اتضح: إذا لم تعد تضيف عناصر إلى
ArrayList
،
elementData
9 من أصل 10 فتحات في صفيف
elementData
فارغة. وحتى إذا قمت بمسح القائمة ، فلن يتم تقليل حجم الصفيف الداخلي. فيما يلي رسم تخطيطي لدورة الحياة هذه:

ما مقدار الذاكرة الضائعة هنا؟ بالقيمة المطلقة ، يتم حسابها على أنها (حجم مؤشر الكائن). إذا كنت تستخدم JVM HotSpot (الذي يأتي مع Oracle JDK) ، فإن حجم المؤشر سيعتمد على الحد الأقصى لحجم الكومة (لمزيد من التفاصيل ، راجع
https://blog.codecentric.de/en/2014/02/35gb-heap-less- 32GB-java-jvm-memory-oddities / ). عادةً ، إذا حددت
-Xmx
أقل من 32 جيجا بايت ، فسيكون حجم المؤشر 4 بايت ؛ لأكوام كبيرة - 8 بايت. وبالتالي ، فإن
ArrayList
، الذي تمت تهيئته بواسطة المُنشئ الافتراضي ، مع إضافة عنصر واحد فقط ، يهدر إما 36 أو 72 بايت.
في الواقع ، فإن
ArrayList
الفارغ يهدر الذاكرة أيضًا لأنه لا يحمل أي عبء عمل ، ولكن حجم
ArrayList
نفسه ليس صفراً وأكبر مما تعتقد على الأرجح. هذا لأنه من ناحية ، يحتوي كل كائن يديره HotSpot JVM على رأس 12 أو 16 بايت ، والذي يستخدمه JVM للأغراض الداخلية. علاوة على ذلك ، تحتوي معظم الكائنات في المجموعة على حقل
size
، أو مؤشر إلى صفيف داخلي أو كائن "وسائط حمل العمل" ، أو حقل
modCount
لتتبع التغييرات في المحتوى ، وما إلى ذلك. لذا ، فحتى أصغر كائن ممكن يمثل مجموعة فارغة ربما يحتاج على الأقل 32 بايت من الذاكرة. البعض ، مثل
ConcurrentHashMap
، يأخذون أكثر من ذلك بكثير.
ضع في اعتبارك مجموعة مشتركة أخرى - فئة
java.util.HashMap
. تشبه دورة حياتها دورة حياة
ArrayList
:

كما ترى ،
HashMap
يحتوي على زوج واحد فقط من قيمة المفتاح ينفق 15 خلية داخلية من الصفيف ، والتي تقابل 60 أو 120 بايت. هذه الأرقام صغيرة ، ولكن مدى فقدان الذاكرة مهم لجميع المجموعات في تطبيقك. وتبين أن بعض التطبيقات يمكن أن تنفق الكثير من الذاكرة بهذه الطريقة. على سبيل المثال ، تفقد بعض مكونات Hadoop الشعبية مفتوحة المصدر التي حللها المؤلف حوالي 20 بالمائة من كومة الذاكرة المؤقتة في بعض الحالات! بالنسبة للمنتجات التي طورها مهندسون أقل خبرة ولا يخضعون لمراجعات أداء منتظمة ، يمكن أن يكون فقدان الذاكرة أعلى. هناك حالات كافية ، على سبيل المثال ، تحتوي 90 ٪ من العقد في شجرة ضخمة على سلالة واحدة أو اثنتين فقط (أو لا شيء على الإطلاق) ، ومواقف أخرى حيث يتم انسداد الكومة بمجموعات من 0 أو 1 أو 2.
إذا وجدت مجموعات غير مستخدمة أو ناقصة الاستخدام في تطبيقك ، فكيف يمكنك إصلاحها؟ فيما يلي بعض الوصفات الشائعة. هنا ، من المفترض أن مجموعتنا الإشكالية هي قائمة
ArrayList
التي تمت الإشارة إليها بواسطة حقل البيانات
Foo.list
.
إذا لم يتم استخدام معظم مثيلات القائمة مطلقًا ، فحاول تهيئتها بتكاسل. لذلك الرمز الذي كان يبدو سابقًا ...
void addToList(Object x) { list.add(x); }
... يجب إعادة بنائه إلى شيء مثل
void addToList(Object x) { getOrCreateList().add(x); } private list getOrCreateList() {
ضع في اعتبارك أنه في بعض الأحيان ستحتاج إلى اتخاذ تدابير إضافية لمعالجة المنافسة المحتملة. على سبيل المثال ، إذا كنت تدعم
ConcurrentHashMap
، والتي يمكن تحديثها من خلال عدة سلاسل رسائل في نفس الوقت ، فإن الشفرة التي تهيئها يجب ألا تسمح لخيوطين لإنشاء نسختين من هذه الخريطة بشكل عشوائي:
private Map getOrCreateMap() { if (map == null) {
إذا كانت معظم مثيلات قائمتك أو خريطتك تحتوي على عناصر قليلة فقط ، فحاول تهيئة هذه العناصر بسعة أولية أكثر ملاءمة ، على سبيل المثال.
list = new ArrayList(4);
إذا كانت مجموعاتك فارغة أو تحتوي على عنصر واحد فقط (أو زوج قيمة مفتاح) في معظم الحالات ، يمكنك التفكير في شكل واحد متطرف من التحسين. يعمل فقط إذا كانت المجموعة مُدارة بالكامل داخل الفئة الحالية ، أي أنه لا يمكن الوصول إلى التعليمات البرمجية الأخرى مباشرةً. تكمن الفكرة في تغيير نوع حقل البيانات ، على سبيل المثال ، من قائمة إلى كائن أكثر عمومية ، بحيث يمكن الآن أن يشير إلى قائمة حقيقية أو مباشرة إلى عنصر قائمة واحد. هنا رسم موجز:
من الواضح أن الشفرة باستخدام هذا التحسين أقل وضوحًا وأكثر صعوبة في الحفاظ عليها. ولكن يمكن أن يكون هذا مفيدًا إذا كنت متأكدًا من أن هذا سيوفر الكثير من الذاكرة أو التخلص من فترات التوقف الطويلة لمجمع القمامة.
ربما تساءلت بالفعل: كيف يمكنني معرفة أي مجموعات في تطبيقي تستهلك الذاكرة وكم؟
باختصار: من الصعب معرفة ذلك بدون الأدوات المناسبة. إن محاولة تخمين مقدار الذاكرة المستخدمة أو التي تنفقها هياكل البيانات في تطبيق معقد كبير لن يؤدي أبدًا إلى أي شيء. ولا تعرف بالضبط أين تذهب الذاكرة ، يمكنك قضاء الكثير من الوقت في ملاحقة الأهداف الخاطئة ، بينما يستمر تطبيقك بعناد في
OutOfMemoryError
مع
OutOfMemoryError
.
لذلك ، يجب عليك التحقق من مجموعة من التطبيقات باستخدام أداة خاصة. من التجربة ، فإن أفضل طريقة لتحليل ذاكرة JVM (التي يتم قياسها وفقًا لكمية المعلومات المتاحة مقارنةً بتأثير هذه الأداة على أداء التطبيق) هي الحصول على تفريغ من الذاكرة ثم عرضها في وضع عدم الاتصال. تفريغ كومة الذاكرة المؤقتة هو في الأساس لقطة كاملة من كومة الذاكرة المؤقتة. يمكنك الحصول عليها في أي وقت عن طريق استدعاء أداة jmap ، أو يمكنك تكوين JVM لتفريغها تلقائيًا إذا تعطل التطبيق مع
OutOfMemoryError
. إذا كنت تستخدم google "JVM heap dump" ، فسترى على الفور عددًا كبيرًا من المقالات التي تشرح بالتفصيل كيفية الحصول على ملف تفريغ.
تفريغ الكومة هو ملف ثنائي بحجم كومة JVM ، لذلك يمكن قراءته وتحليله فقط باستخدام أدوات خاصة. هناك العديد من الأدوات ، سواء مفتوحة المصدر أو تجارية. أكثر أداة مفتوحة المصدر شعبية هي Eclipse MAT. هناك أيضًا VisualVM وبعض الأدوات الأقل قوة والأقل شهرة. تتضمن الأدوات التجارية ملفات تعريف Java للأغراض العامة: JProfiler و YourKit ، بالإضافة إلى أداة واحدة مصممة خصيصًا لتحليل تفريغ الكومة - JXRay (إخلاء المسؤولية: تم تطويره آخر مرة بواسطة المؤلف).
على عكس الأدوات الأخرى ، يحلل JXRay على الفور تفريغ الكومة لعدد كبير من المشاكل الشائعة ، مثل الخطوط المتكررة والكائنات الأخرى ، بالإضافة إلى هياكل البيانات غير الكافية. تقع مشاكل المجموعات الموصوفة أعلاه في الفئة الأخيرة. تقوم الأداة بإنشاء تقرير بكل المعلومات التي تم جمعها بتنسيق HTML. ميزة هذا النهج هو أنه يمكنك عرض نتائج التحليل في أي مكان وفي أي وقت ومشاركتها بسهولة مع الآخرين. يمكنك أيضًا تشغيل الأداة على أي جهاز ، بما في ذلك الأجهزة الكبيرة والقوية ، ولكن "عديمة الرأس" في مركز البيانات.
يحسب JXRay النفقات العامة (مقدار الذاكرة التي ستوفرها إذا تخلصت من مشكلة معينة) بالبايت وكنسبة مئوية من الكومة المستخدمة. فهو يجمع بين مجموعات من نفس الفئة التي لديها نفس المشكلة ...

... ثم تجميع المجموعات الإشكالية التي يمكن الوصول إليها من بعض جذر جامع القمامة من خلال نفس سلسلة الارتباط ، كما في المثال أدناه

إن معرفة سلاسل الارتباط و / أو حقول البيانات الفردية (على سبيل المثال ،
INodeDirectory.children
أعلاه) تشير إلى المجموعات التي تنفق معظم ذاكرتها تسمح لك بتحديد الرمز المسؤول عن المشكلة بسرعة وبدقة ، ثم إجراء التغييرات اللازمة.
وبالتالي ، يمكن لمجموعات Java التي لم يتم تكوينها بشكل كاف أن تهدر الكثير من الذاكرة. في كثير من الحالات ، من السهل حل هذه المشكلة ، ولكن في بعض الأحيان قد تحتاج إلى تعديل التعليمات البرمجية بطرق غير تافهة لتحقيق تحسن كبير. من الصعب جدًا تخمين المجموعات التي تحتاج إلى التحسين من أجل تحقيق أكبر تأثير. لكي لا تضيع الوقت في تحسين الأجزاء الخاطئة من التعليمات البرمجية ، تحتاج إلى الحصول على تفريغ كومة JVM وتحليلها باستخدام الأداة المناسبة.
النهاية
نحن ، كما هو الحال دائمًا ، مهتمون بآرائك وأسئلتك ، والتي يمكنك تركها هنا أو زيارة
درس مفتوح ونطلب من
معلمينا هناك.