
ستركز المقالة على تطبيق ضغط المؤشر في Java Virtual Machine 64-bit ، والذي يتم التحكم فيه بواسطة خيار UseCompressedOops ويتم تمكينه افتراضيًا للأنظمة 64 بت التي تبدأ من Java SE 6u23.
وصف المشكلة
في JVM 64 بت ، تشغل المؤشرات مساحة ذاكرة (مفاجأة) 2 مرات أكثر من مساحة 32 بت. يمكن أن يؤدي ذلك إلى زيادة حجم البيانات بمقدار 1.5 مرة مقارنة بالرمز نفسه للعمارة 32 بت. في الوقت نفسه ، في بنية 32 بت ، يمكن معالجة فقط 2 ^ 32 بايت (4 جيجابايت) ، وهو صغير جدًا في العالم الحديث.
دعنا نكتب برنامجًا صغيرًا وننظر في عدد وحدات البايت العددية التي تشغلها:
import java.util.stream.IntStream; import java.util.stream.Stream; class HeapTest { public static void main(String ... args) throws Exception { Integer[] x = IntStream.range(0, 1_000_000).boxed().toArray(Integer[]::new); Thread.sleep(6000000); Stream.of(x).forEach(System.out::println); } }
نحن هنا نسلط الضوء على مليون قطعة من فئة Integer ونغفو لفترة طويلة. هناك حاجة إلى السطر الأخير حتى لا يتجاهل المترجم إنشاء الصفيف بشكل مفاجئ (على الرغم من أنه في الجهاز الخاص بي ، يتم إنشاء الكائنات بشكل طبيعي دون هذا السطر).
نقوم بتجميع وتشغيل البرنامج باستخدام ضغط مؤشر معطل:
> javac HeapTest.java > java -XX:-UseCompressedOops HeapTest
باستخدام الأداة المساعدة jcmd ، ننظر إلى تخصيص الذاكرة:
> jps 45236 HeapTest ... > jcmd 45236 GC.class_histogram

تُظهر الصورة أن إجمالي عدد الكائنات هو 1000128 ، وحجم الذاكرة التي تشغلها هذه الكائنات هو 24003072 بايت . أي 24 بايت لكل كائن (لماذا بالضبط 24 ستكتب أدناه).
وهنا هي ذاكرة البرنامج نفسه ، ولكن مع العلم استخدام UseCompressedOops :

الآن يحتل كل كائن 16 بايت .
مزايا الضغط واضحة =)
الحل
كيف ضغط JVM المؤشرات؟ هذه التقنية تسمى مضغوط عفوا . Oop تعني مؤشر كائن عادي أو مؤشر كائن عادي .
الحيلة هي أنه في نظام 64 بت ، يتم محاذاة البيانات الموجودة في الذاكرة مع كلمة الآلة ، أي 8 بايت لكل منهما. ويحتوي العنوان دائمًا على ثلاث بتات صفرية في النهاية.
إذا قمت بحفظ المؤشر عن طريق تحويل العنوان بمقدار 3 بتات إلى اليمين (وتسمى العملية الترميز ) ، وقبل الاستخدام ، قم بالتحويل بمقدار 3 بتات إلى اليسار (على التوالي ، فك ترميز ) ، ثم يمكنك وضع مؤشرات 32 بت بحجم 35 بت ، أي: معالجة ما يصل إلى 32 جيجابايت (2 ^ 35 بايت).
إذا كان حجم الكومة للبرنامج أكثر من 32 جيجابايت ، فإن الضغط يتوقف عن العمل وتصبح جميع المؤشرات 8 بايت في الحجم.
عند تمكين الخيار UseCompressedOops ، يتم ضغط أنواع المؤشرات التالية:
- حقل فئة لكل كائن
- كائنات حقل الفئة
- عناصر مجموعة من الكائنات.
لا يتم ضغط كائنات JVM نفسها أبدًا. في هذه الحالة ، يحدث الضغط على مستوى الجهاز الظاهري ، وليس الرمز الثنائي.
اقرأ المزيد عن وضع الأشياء في الذاكرة
الآن ، دعونا نستخدم الأداة المساعدة jol (Java Object Layout) لنلقي نظرة فاحصة على مقدار الذاكرة التي يأخذها رقمنا الصحيح في JVMs المختلفة:
> java -jar jol-cli-0.9-full.jar estimates java.lang.Integer ***** 32-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 8 (object header) N/A 8 4 int Integer.value N/A 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM: ********************************************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 16 (object header) N/A 16 4 int Integer.value N/A 20 4 (loss due to the next object alignment) Instance size: 24 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total ***** 64-bit VM, compressed references enabled: *************************** java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total ***** 64-bit VM, compressed references enabled, 16-byte align: ************ java.lang.Integer object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 12 (object header) N/A 12 4 int Integer.value N/A Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
الفرق بين "64 بت VM" و "64 بت VM ، تمكين المراجع المضغوطة" هو تقليل رأس الكائن بمقدار 4 بايت. بالإضافة إلى ذلك ، في حالة عدم وجود ضغط ، يصبح من الضروري إضافة 4 بايت إضافية لمحاذاة البيانات في الذاكرة.
ما هو رأس الكائن؟ لماذا انخفض بنسبة 4 بايت؟

تُظهر الصورة رأس كائن يتكون من 12 بايت ، أي مع تمكين الخيار UseCompressedOops. يتكون الرأس من بعض علامات JVM الداخلية ، بالإضافة إلى مؤشر إلى فئة هذا الكائن. يمكن ملاحظة أن المؤشر إلى الفصل يستغرق 32 بت. بدون ضغط ، سيشغل 64 بت وسيكون حجم رأس الكائن بالفعل 16 بايت.
بالمناسبة ، يمكنك أن ترى أن هناك خيارًا آخر لمحاذاة 16 بايت. في هذه الحالة ، يمكنك زيادة الذاكرة حتى 64 جيجابايت.
سلبيات ضغط المؤشرات
بطبيعة الحال ، فإن مؤشرات الضغط لديها ناقص واضح - تكلفة تشفير وفك تشفير العمليات في كل مرة يتم الوصول إلى المؤشر. ستختلف الأرقام الدقيقة حسب التطبيق.
على سبيل المثال ، فيما يلي رسم بياني مؤقتًا لإيقاف أداة تجميع مجمعي البيانات المهملة مؤقتًا عن المؤشرات المضغوطة وغير المضغوطة ، مأخوذة من هنا Java GC in Numbers - OOPs المضغوطة

يمكن أن نرى أنه مع تشغيل الضغط ، فإن GC توقف لفترة أطول. يمكنك قراءة المزيد حول هذا الموضوع في المقالة نفسها (المقال قديم جدًا - 2013).
المراجع
عفوا مضغوط في Hotspot JVM
كيف JVM تخصيص الكائنات
CompressedOops: مقدمة إلى المراجع المضغوطة في Java
خدعة وراء عفوا JVM المضغوط
جافا هوت سبوت تحسين أداء الجهاز الظاهري