يكفي أنه في Java ، تتم تهيئة برامج تسجيل الدخول في الوقت الذي تتم فيه تهيئة الفصل ، فلماذا تملأ عملية الإطلاق بالكامل؟ جون روز لإنقاذ!
إليك ما قد يبدو عليه:
lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
يوسع هذا المستند سلوك المتغيرات النهائية ، مما يتيح لك دعم التنفيذ البطيء اختياريًا - سواء في اللغة نفسها أو في JVM. من المقترح تحسين سلوك الآليات الحالية للحوسبة البطيئة عن طريق تغيير الدقة: الآن لن تكون دقيقة للفئة ، ولكنها دقيقة لمتغير محدد.

الدافع
بنيت جافا بعمق في الحوسبة البطيئة. يمكن لكل عملية ارتباط تقريبًا سحب رمز كسول. على سبيل المثال ، تنفيذ طريقة <clinit>
( <clinit>
الفصل) أو باستخدام طريقة التمهيد (لموقع استدعاء تم استدعاؤه أو CONSTANT_Dynamic
ثوابت ديناميكية).
إن مُهيئو الفصل هم شيء وقح للغاية من حيث الدقة عند مقارنته بالآليات التي تستخدم أساليب التمهيد ، حيث أن عقدهم هو تشغيل كل كود التهيئة للفئة ككل ، بدلاً من أن يقتصر على التهيئة المتعلقة بمجال معين من الفصل. من الصعب التنبؤ بآثار مثل هذا التهيئة الأولية. من الصعب عزل الآثار الجانبية لاستخدام حقل ثابت واحد من الفئة ، لأن حساب حقل واحد يؤدي إلى حساب جميع الحقول الثابتة لهذه الفئة.
إذا لمست مجالًا واحدًا ، فستؤثر عليهم جميعًا. في المترجمين AOT ، هذا يجعل من الصعب بشكل خاص تحسين مراجع الحقول الثابتة ، حتى بالنسبة للحقول التي لها قيمة ثابتة سهلة التحليل. بمجرد أن يتكدس حقل ثابت واحد على الأقل تم إعادة تصميمه بين الحقول ، يصبح من المستحيل تحليل جميع حقول هذه الفئة بالكامل. تتجلى مشكلة مماثلة في الآليات المقترحة سابقًا لتنفيذ الالتفاف من الثوابت (أثناء تشغيل جافا سكريبت ) للحقول الثابتة مع مُبدئات معقدة.
مثال على تهيئة الحقل المعاد تصميمه ، والذي يحدث في مشاريع مختلفة في كل خطوة ، في كل ملف ، هو تهيئة المسجل.
private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
يبدأ هذا التهيئة غير الضار ، تحت غطاء المحرك ، مقدارًا كبيرًا من العمل الذي سيتم تنفيذه أثناء تهيئة الفصل - ومع ذلك ، من غير المحتمل للغاية أن يكون المسجل مطلوبًا بالفعل في الوقت الذي تتم فيه تهيئة الفصل ، أو ربما لا تكون هناك حاجة على الإطلاق. ستؤدي القدرة على تأجيل إنشائه حتى الاستخدام الحقيقي الأول إلى تبسيط التهيئة ، وفي بعض الحالات سيساعد على تجنب هذا التهيئة تمامًا.
المتغيرات النهائية مفيدة للغاية ، فهي الآلية الرئيسية لـ Java API من أجل الإشارة إلى ثبات القيم. كما عملت المتغيرات الكسولة بشكل جيد. بدءًا من Java 7 ، بدأوا يلعبون دورًا مهمًا بشكل متزايد في @Stable
الداخلية من JDK ، حيث تم وضع علامة على التعليق التوضيحي @Stable
. يمكن لـ JIT تحسين المتغيرات النهائية والمستقرة - أفضل بكثير من بعض المتغيرات فقط. ستسمح إضافة المتغيرات النهائية البطيئة بأن يصبح نمط الاستخدام المفيد هذا أكثر شيوعًا ، مما يجعل من الممكن استخدامه في أماكن أكثر. أخيرًا ، سيسمح استخدام المتغيرات النهائية البطيئة للمكتبات مثل JDK بتقليل الاعتماد على رمز <clinit>
، والذي بدوره يجب أن يقلل من وقت بدء التشغيل ويحسن جودة تحسينات AOT.
الوصف
يمكن الإعلان عن الحقل باستخدام معدّل lazy
جديد ، وهو كلمة أساسية سياقية يُنظر إليها حصريًا كمُعدِّل. يسمى هذا الحقل بالحقل الكسول ، ويجب أن يحتوي أيضًا على معدِّلات static
final
.
يجب أن يكون للحقل الكسول مُهيئ. يوافق المترجم ووقت التشغيل على بدء التهيئة بالضبط عند استخدام المتغير لأول مرة ، وليس عند تهيئة الفئة التي ينتمي إليها هذا الحقل.
يرتبط كل حقل lazy static final
في وقت الترجمة بعنصر تجمع ثابت يمثل قيمته. نظرًا لأن عناصر التجمع الثابت نفسها يتم حسابها بشكل كسول ، يكفي ببساطة تعيين القيمة الصحيحة لكل متغير نهائي كسول ثابت مرتبط بهذا العنصر. (يمكنك ربط أكثر من متغير كسول بعنصر واحد ، ولكن هذه بالكاد ميزة مفيدة أو ذات معنى.) اسم السمة هو LazyValue
، ويجب أن يشير إلى عنصر جنس ثابت يمكن ترميزه بـ ldc إلى قيمة قابلة للتحويل إلى نوع حقل كسول . يُسمح فقط MethodHandle.invoke
المستخدم بالفعل في MethodHandle.invoke
.
وبالتالي ، يمكن اعتبار الحقل الثابت البطيء كاسم مستعار لعنصر تجمع ثابت داخل الفئة التي أعلنت هذا الحقل. قد تحاول أدوات مثل المترجمين بطريقة ما استخدام هذا المجال.
لا يمثل الحقل البطيء متغيرًا ثابتًا (بمعنى JLS 4.12.4) ويتم استبعاده بشكل صريح من المشاركة في التعبيرات الثابتة (بمعنى JLS 15.28). لذلك ، لا تلتقط السمة ConstantValue
أبدًا ، حتى إذا كان المُهيئ تعبيرًا ثابتًا. بدلاً من ذلك ، يلتقط الحقل الكسول نوعًا جديدًا من سمة LazyValue
تسمى LazyValue
، والتي LazyValue
JVM عند الارتباط بهذا الحقل المعين. يتشابه تنسيق هذه السمة الجديدة مع السمة السابقة ، حيث يشير أيضًا إلى عنصر تجمع ثابت ، في هذه الحالة ، العنصر الذي تم حله لقيمة الحقل.
عند ربط حقل ثابت كسول ، يجب ألا تختفي العملية العادية لتنفيذ مُهيّئ الصف. بدلاً من ذلك ، تتم تهيئة أي أسلوب فئة فئة <clinit>
وفقًا للقواعد المحددة في JVMS 5.5. بمعنى آخر ، يؤدي getstatic
نفس الارتباط مثل أي حقل ثابت. بعد التهيئة (أو أثناء التهيئة التي بدأت بالفعل لمؤشر الترابط الحالي) ، يحل JVM عناصر التجمع الثابتة المرتبطة بالحقل ويحفظ القيم التي تم الحصول عليها من التجمع الثابت في هذا الحقل.
نظرًا لأن الكسولة الساكنة البطيئة لا يمكن أن تكون فارغة ، فلا يمكن تعيين أي قيم لها - حتى في السياقات القليلة التي يعمل فيها هذا مع المتغيرات النهائية الفارغة.
أثناء التجميع ، تتم تهيئة جميع الحقول الثابتة البطيئة بشكل مستقل عن الحقول الثابتة غير البطيئة ، بغض النظر عن موقعها في شفرة المصدر. لذلك ، لا تنطبق القيود على موقع الحقول الثابتة على الحقول الثابتة البطيئة. يمكن لمهيئ المجال الثابت البطيء استخدام أي حقل ثابت من نفس الفئة ، بغض النظر عن الترتيب الذي تظهر به في المصدر. يمكن لمهيئ أي حقل غير ثابت أو مُهيئ الفصل الوصول إلى الحقل البطيء ، بغض النظر عن الترتيب في المصدر المتعلق ببعضهما البعض. عادة ، لا يكون القيام بذلك هو الفكرة الأكثر منطقية ، حيث يتم فقد المعنى الكامل للقيم الكسولة ، ولكن يمكن استخدامها بطريقة أو بأخرى في التعبيرات الشرطية أو على تدفق التحكم. لذلك ، يمكن التعامل مع الحقول الثابتة البطيئة مثل الحقول من فئة أخرى - بمعنى أنه يمكن الرجوع إليها بأي ترتيب من أي جزء من الفصل الذي تم الإعلان عنها فيه.
يمكن الكشف عن الحقول البطيئة باستخدام API الانعكاس باستخدام طريقتين جديدتين لواجهة برمجة التطبيقات في java.lang.reflect.Field
. طريقة isLazy
الجديدة ترجع true
إذا وفقط إذا كان الحقل يحتوي على معدّل lazy
. isAssigned
أسلوب isAssigned
الجديد false
إذا وفقط إذا كان الحقل كسولًا ولم تتم تهيئته في وقت isAssigned
. (يمكن أن يكون صحيحًا تقريبًا في المكالمة التالية في نفس الخيط ، اعتمادًا على وجود السباقات). لا توجد طريقة لمعرفة ما إذا تمت تهيئة الحقل ، بخلاف استخدام isAssigned
.
(المكالمة isAssigned
مطلوبة isAssigned
للمساعدة في المشكلات النادرة المتعلقة بحل التبعيات الدائرية. ربما يمكننا القيام بذلك بدون تنفيذ هذه الطريقة. ومع ذلك ، فإن الأشخاص الذين يكتبون الرمز بمتغيرات كسولة يريدون أحيانًا معرفة سواء تم تعيين القيمة على هذا المتغير أو لم يتم بعد ، بنفس الطريقة التي يرغب بها مستخدمو mutex في بعض الأحيان في معرفة ما إذا كان مقفلًا أم لا ، لكنهم لا يريدون حقًا أن يتم قفلهم)
هناك قيود غير معتادة على الحقول النهائية البطيئة: لا يجب أن تتم تهيئتها مطلقًا إلى قيمها الافتراضية. بمعنى ، يجب ألا تتم تهيئة الحقل المرجعي البطيء إلى قيمة null
، ولا يجب أن تحتوي الأنواع الرقمية على قيمة فارغة. يمكن تهيئة القيمة المنطقية البطيئة بقيمة واحدة فقط - true
، لأن القيمة false
هي القيمة الافتراضية. إذا قام مُهيئ الحقل الثابت الكسول بإرجاع قيمته الافتراضية ، فسوف يفشل ربط هذا الحقل مع الخطأ المقابل.
تم تقديم هذا التقييد لذلك. للسماح لتطبيقات JVM بحفظ القيم الافتراضية كقيمة رقابة داخلية تحدد حالة حقل غير مهيأ. تم تعيين القيمة الافتراضية بالفعل في القيمة الأولية لأي حقل ، تم تعيينها في وقت التحضير (هذا موضح في JLS 5.4.2). لذا فإن هذه القيمة موجودة بالفعل بشكل طبيعي في بداية دورة حياة أي مجال ، وبالتالي فهي خيار منطقي للاستخدام كقيمة رقابة تراقب حالة هذا المجال. باستخدام هذه القواعد ، لا يمكنك أبدًا الحصول على القيمة الافتراضية الأصلية من حقل ثابت كسول. لهذا ، يمكن لـ JVM ، على سبيل المثال ، تنفيذ حقل كسول كرابط غير قابل للتغيير إلى عنصر التجمع الثابت المقابل.
يمكن التحايل على القيود على القيم الافتراضية من خلال التفاف القيم (التي قد تكون مساوية للقيم الافتراضية) في مربعات أو حاويات من نوع مناسب. يمكن التفاف الرقم صفر في مرجع عدد صحيح غير صفري. يمكن لف الأنواع غير البدائية في اختياري ، والذي يصبح فارغًا إذا وصل إلى قيمة فارغة.
للحفاظ على الحرية في طرق تنفيذ الميزات ، isAssigned
التقليل من متطلبات طريقة isAssigned
خاص. إذا استطاع JVM أن يثبت أنه يمكن تهيئة متغير ثابت كسول بدون تأثيرات خارجية ملحوظة ، فيمكنه القيام بهذا التهيئة في أي وقت. في هذه الحالة ، isAssigned
true
حتى إذا لم يتم استدعاء getfield
مطلقًا. الشرط الوحيد المفروض على isAssigned
هو أنه إذا isAssigned
false
، فلا يجب ملاحظة أي من الآثار الجانبية isAssigned
المتغيرة في الخيط الحالي. وإذا عاد true
، فقد يلاحظ الخيط الحالي في المستقبل الآثار الجانبية للتهيئة. يسمح هذا العقد ldc
باستبدال ldc
بـ getstatic
الخاصة ، مما يسمح لـ JVM بعدم مراقبة الحالات التفصيلية للمتغيرات النهائية التي تحتوي على عناصر مشتركة أو متدهورة في التجمع الثابت.
يمكن أن تدخل العديد من سلاسل الرسائل حالة السباق لتهيئة حقل نهائي كسول. كما يحدث بالفعل مع CONSTANT_Dynamic
، تحدد JVM فائزًا تعسفيًا في هذا السباق وتوفر قيمة هذا الفائز لجميع سلاسل الرسائل المشاركة في السباق ، وتكتبه لجميع المحاولات اللاحقة للحصول على قيمة. للتغلب على السباق ، قد تحاول تطبيقات JVM محددة استخدام عمليات CAS ، إذا كانت المنصة تدعمها ، سيرى الفائز في السباق القيمة الافتراضية السابقة ، وسيرى الخاسرون القيمة غير الافتراضية التي فازت بالسباق.
وبالتالي ، تستمر القواعد الحالية للتعيين الفردي للمتغيرات النهائية في العمل وتلتقط الآن جميع صعوبات الحوسبة البطيئة.
ينطبق نفس المنطق على النشر الآمن باستخدام الحقول النهائية - وهو نفس الشيء بالنسبة لكل من الحقول البطيئة وغير البطيئة.
لاحظ أنه يمكن للفئة تحويل حقل ثابت إلى حقل ثابت كسول دون كسر التوافق الثنائي. بيان العميل getstatic
متطابق في كلتا الحالتين. عندما يتغير إعلان متغير إلى كسول ، getstatic
ربط getstatic
بطريقة مختلفة.
حلول بديلة
يمكنك استخدام الفئات المتداخلة كحاويات للمتغيرات الكسولة.
يمكنك تعريف شيء ما مثل واجهة برمجة تطبيقات المكتبة لإدارة القيم البطيئة أو (بشكل عام) أي بيانات رتيبة.
إعادة صياغة ما كانوا بصدد صنع متغيرات ثابتة كسولة بحيث تحولوا إلى طرق ثابتة باطلة وتم نشر أجسادهم باستخدام ldc CONSTANT_ ثوابت ديناميكية ، بطريقة ما.
(ملاحظة: لا توفر الحلول الموضحة أعلاه طريقة متوافقة ثنائية لفصل الثوابت الثابتة الموجودة تطوريًا من <clinit>
)
إذا تحدثنا عن توفير المزيد من الوظائف ، يمكنك السماح للحقول الكسولة بأن تكون غير ثابتة أو غير نهائية ، مع الحفاظ على المراسلات والتماثلات الحالية بين سلوك الحقول الثابتة وغير الثابتة. لا يمكن أن يكون التجمع الثابت مستودعًا للحقول غير الثابتة ، ولكن لا يزال بإمكانه الاحتفاظ بأساليب التمهيد (اعتمادًا على المثيل الحالي). المصفوفات المجمدة (إذا نفذت) يمكن أن تحصل على خيار كسول. مثل هذه الدراسات هي أساس جيد للمشاريع المستقبلية المبنية على أساس هذه الوثيقة. وبالمناسبة ، فإن مثل هذه الفرص تجعل قرارنا بحظر القيم الافتراضية أكثر فائدة.
يجب تهيئة المتغيرات الكسولة باستخدام تعبيرات التهيئة الخاصة بها. يبدو هذا أحيانًا قيدًا مزعجًا للغاية يعيدنا إلى وقت اختراع المتغيرات النهائية الفارغة. تذكر أنه يمكن تهيئة هذه المتغيرات النهائية الفارغة باستخدام كتل عشوائية من التعليمات البرمجية ، بما في ذلك منطق المحاولة أخيرًا ، ويمكن تهيئتها في مجموعات بدلاً من تزامنها. في المستقبل ، سيكون من الممكن محاولة تطبيق نفس الاحتمالات على المتغيرات النهائية الكسولة. ربما يمكن ربط واحد أو أكثر من المتغيرات الكسولة مع كتلة خاصة من تهيئة التهيئة التي تتمثل مهمتها في تعيين كل متغير مرة واحدة بالضبط ، كما يحدث مع مُهيئ الفئة أو مُنشئ الكائن. يمكن أن تصبح بنية هذه الميزة أكثر وضوحًا بعد ظهور أجهزة التفكيك ، نظرًا لأن المهام التي يحلونها تتقاطع إلى حد ما.
دقيقة من الدعاية. سيعقد مؤتمر Joker 2018 قريبًا جدًا ، حيث سيكون هناك العديد من المتخصصين البارزين في Java و JVM. عرض القائمة الكاملة للمتحدثين والتقارير على الموقع الرسمي .
المؤلف
جون روز هو مهندس ومهندس JVM في Oracle. المهندس الرئيسي لمشروع Da Vinci Machine (جزء من OpenJDK). مهندس رئيسي JSR 292 (دعم اللغات المكتوبة ديناميكيًا على منصة Java) ، متخصص في المكالمات الديناميكية والموضوعات ذات الصلة مثل توصيف النوع وتحسينات المترجم المتقدمة. في السابق ، عمل في الفصول الداخلية ، وصنع منفذ HotSpot الأصلي على SPARC ، و Unsafe API ، كما طور العديد من اللغات الديناميكية والمتوازية والهجينة ، بما في ذلك Common Lisp ، Scheme ("esh") ، والمجلدات الديناميكية لـ C ++.
مترجم
Oleg Chirukhin - في وقت كتابة هذا النص ، كان يعمل كمدير مجتمع في شركة JUG.ru Group ، وهو يشارك في تعميم منصة Java. قبل انضمامه إلى JRG ، شارك في تطوير أنظمة المعلومات المصرفية والحكومية ، ونظام بيئي للغات البرمجة المكتوبة ذاتيًا ، والألعاب عبر الإنترنت. تشمل الاهتمامات البحثية الحالية الأجهزة الافتراضية والمترجمين ولغات البرمجة.