الطبخ الجانبي في مطبخ JVM

اسمي ألكسندر كوتسيوروبا ، وأقود تطوير الخدمات الداخلية في DomKlik. يأتي العديد من مطوري Java ذوي الخبرة لفهم الهيكل الداخلي لـ JVM. لتسهيل هذه الرحلة من Java Samurai ، قررت وضع أساسيات Java Virtual Machine (JVM) والعمل مع البايت كود بلغة بسيطة.

ما هو الرمز الثانوي الغامض وأين يعيش؟

سأحاول الإجابة على هذا السؤال باستخدام مثال التخليل.



لماذا أحتاج إلى JVM و bytecode؟


نشأت JVM تحت شعار الكتابة بمجرد التشغيل في أي مكان (WORA) في Sun Microsystems. على عكس مفهوم الكتابة لمرة واحدة في الترجمة في أي مكان (WOCA) ، فإن WORA تتضمن جهازًا افتراضيًا لكل نظام تشغيل يقوم بتنفيذ التعليمات البرمجية التي تم تجميعها مرة واحدة (رمز البايت).


الكتابة بمجرد التشغيل في أي مكان (WORA)


اكتب مرة واحدة Compile Anywhere (WOCA)

JVM و bytecode هي أساس مفهوم WORA وتخلصنا من الفروق الدقيقة والحاجة إلى الترجمة لكل نظام تشغيل.

البايت كود


لفهم ماهية الرمز الثانوي ، دعنا نلقي نظرة على مثال. بالطبع ، هذا الرمز لا يفعل أي شيء مفيد ، ولكنه سيخدم فقط لمزيد من التحليل.

مصدر:

class Solenya(val jarForPickles: Any? = Any(), var ingredientsCount: Int = 0) {


    /**
     *   
     *  @param ingredient -  
     */
    fun add(ingredient: Any) {
        ingredientsCount = ingredientsCount.inc()
        //- 
    }

    /**
     *   
     *  @param duration -   
     */
    fun warmUp(duration: Int) {
        for (x in 1..duration)
            println("Warming")
    }

    init {
        //   
        val jarForPickles = takeJarForPickles()
        // 
        val pickles = Any()
        // 
        val water = Any()

        //
        add(pickles)
        add(water)

        //
        warmUp(10)
    }

    /**
     *   
     */
    private fun takeJarForPickles(): Any = openLocker()

    /**
     *   
     */
    private fun openLocker(): Any = takeKeyForLocker()

    /**
     *     
     */
    private fun takeKeyForLocker(): Any = {}
}

باستخدام أدوات Intellij IDEA المضمنة ( أدوات -> Kotlin -> إظهار Kotlin Bytecode ) نحصل على رمز بايت مفكك (يظهر جزء فقط في المثال):

...
   INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 12 L6
    RETURN
   L7
    LOCALVARIABLE this Lcom/company/Solenya; L0 L7 0
    LOCALVARIABLE ingredient Ljava/lang/Object; L0 L7 1
    LOCALVARIABLE $i$f$add I L1 L7 2
    MAXSTACK = 2
    MAXLOCALS = 5

  // access flags 0x11
  public final warmUp(I)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 19 L0
    ICONST_1
    ISTORE 2
...

للوهلة الأولى - مجموعة غير مفهومة من التعليمات. لفهم كيف وماذا يعملون ، ستحتاج إلى الغوص في مطبخ JVM الداخلي.

مطبخ JVM


دعونا نلقي نظرة على ذاكرة وقت تشغيل JVM:



يمكننا القول أن JVM هو مطبخنا. بعد ذلك ، ضع في اعتبارك بقية المشاركين:

منطقة الطريقة - كتاب الطبخ



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

خيط 1..N - فريق الطهاة



تتبع التدفقات بدقة الإرشادات التي يحددها لهم (منطقة الطريقة) ، ولهذا السبب لديهم PC Register و JVM Stack. يمكنك مقارنة كل تيار مع طاهي يقوم بالمهمة المعطاة له ، باتباع الوصفات بالضبط من كتاب الطهي.

سجل الكمبيوتر - ملاحظات ميدانية



برنامج عداد التسجيل - عداد أوامر دفقنا. يقوم بتخزين عنوان التعليمات التي يتم تنفيذها. في المطبخ ، ستكون هذه بعض الملاحظات على أي صفحة من كتاب الطبخ نحن الآن.

مكدس Jvm


كومة من الإطارات. يتم تخصيص إطار لكل وظيفة ، حيث يعمل مؤشر الترابط الحالي مع المتغيرات والمعاملات. كجزء من القياس مع إعداد المخللات ، يمكن أن تكون هذه مجموعة من العمليات المتداخلة:

-> -> -> ...

الإطار - سطح المكتب



يعمل الإطار كسطح مكتب الطباخ ، الذي يقع عليه لوح التقطيع والحاويات الموقعة.

المتغيرات المحلية - حاويات موقعة



هذه مجموعة من المتغيرات المحلية (جدول المتغيرات المحلية) ، والتي ، كما يوحي الاسم ، تخزن قيم المتغيرات المحلية ونوعها ونطاقها. هذا مشابه للحاويات الموقعة ، حيث يمكنك إضافة نتائج وسيطة للنشاط المهني.

مكدس معامل - لوح تقطيع



تكدس معامِلات وسيطات تعليمات JVM. على سبيل المثال ، القيم الصحيحة لعملية الإضافة ، والإشارات إلى كومة الذاكرة المؤقتة ، وما إلى ذلك.

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

كومة - جدول التوزيع



كجزء من العمل مع الإطار ، نعمل على ارتباطات الكائنات ؛ يتم تخزين الكائنات نفسها في كومة الذاكرة المؤقتة. هناك اختلاف مهم هو أن الإطار ينتمي إلى مؤشر ترابط واحد فقط ، والمتغيرات المحلية "مباشرة" أثناء تشغيل الإطار (يتم تنفيذ الوظيفة). ويمكن الوصول إلى كومة الذاكرة المؤقتة من مؤشرات الترابط الأخرى ، ويعيش حتى يتم تشغيل جامع القمامة. قياسا على المطبخ ، يمكننا إعطاء مثال على جدول توزيع ، وهو أمر شائع وحده. ويتم تنظيفه من قبل فريق منفصل من عمال النظافة.

مطبخ JVM. نظرة من الداخل. العمل باستخدام الإطار


لنبدأ بالوظيفة warmUp:

    /**
     *   
     *  @param duration -   
     */
    fun warmUp(duration: Int) {
        for (x in 1..duration)
            println("Warming...")
    }

وظيفة البايت كود المفككة:

  public final warmUp(I)V
    // annotable parameter count: 1 (visible)
    // annotable parameter count: 1 (invisible)
   L0
    LINENUMBER 19 L0
    ICONST_1
    ISTORE 2
    ILOAD 1
    ISTORE 3
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 20 L2
    LDC "Warming..."
    ASTORE 4
   L3
    ICONST_0
    ISTORE 5
   L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 4
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 19 L6
    ILOAD 2
    ILOAD 3
    IF_ICMPEQ L1
    IINC 2 1
   L7
    GOTO L2
   L1
    LINENUMBER 21 L1
    RETURN
   L8
    LOCALVARIABLE x I L2 L7 2
    LOCALVARIABLE this Lcom/company/Solenya; L0 L8 0
    LOCALVARIABLE duration I L0 L8 1
    MAXSTACK = 2
    MAXLOCALS = 6

تهيئة الإطار - إعداد مكان العمل


لتنفيذ هذه الوظيفة ، سيتم إنشاء إطار في تدفق مكدس JVM. دعني أذكرك أن المكدس يتكون من مجموعة من المتغيرات المحلية ومجموعة المكدس.

  1. حتى نتمكن من فهم مقدار الذاكرة المراد تخصيصها لهذا الإطار ، قدم المترجم معلومات تعريفية حول هذه الوظيفة (شرح في تعليق الكود):

        MAXSTACK = 2 //    2*32bit
        MAXLOCALS = 6 //    6*32bit
    
  2. لدينا أيضًا معلومات حول بعض عناصر مجموعة المتغيرات المحلية:

        LOCALVARIABLE x I L2 L7 2 //  x  Int(I),      L2-L7   2
        LOCALVARIABLE this Lcom/company/Solenya; L0 L8 0
        LOCALVARIABLE duration I L0 L8 1
    
  3. تدخل وسيطات الدالة عند تهيئة الإطار في المتغيرات المحلية. في هذا المثال ، ستتم كتابة قيمة المدة إلى الصفيف باستخدام الفهرس 1.

وبالتالي ، في البداية سيبدو الإطار كما يلي:


ابدأ تنفيذ التعليمات


لفهم كيفية عمل الإطار ، ما عليك سوى تسليح نفسك بقائمة من تعليمات JVM ( قوائم تعليمات Java bytecode ) وخطو التسمية L0:

   L0
    LINENUMBER 19 L0 //     
    ICONST_1
    ISTORE 2
    ILOAD 1
    ISTORE 3
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1

ICONST_1 - إضافة 1(الباحث) في كومة المعامل:



ISTORE 2 - قيمة سحب (نوع كثافة العمليات) من المكدس المعامل والكتابة إلى المتغيرات المحلية مع المؤشر 2:



يمكن أن تفسر هاتان العمليتان في جاوة رمز: int x = 1.

ILOAD 1 - تحميل قيمة المتغيرات المحلية مع مؤشر 1 في كومة المعامل:



ISTORE 3 - قيمة سحب (نوع كثافة العمليات) من المكدس المعامل ويكتب إلى المتغيرات المحلية مع فهرس من 3:



يمكن أن تفسر هاتان العمليتان في جاوة رمز: int var3 = duration.

ILOAD 2 - تحميل قيمة من المتغيرات المحلية مع فهرس 2 في كومة المعامل.

ILOAD 3 - قيمة الحمل من المتغيرات المحلية مع الفهرس 3 في حزمة المعامل:



IF_ICMPGT L1- تعليمات لمقارنة قيمتين صحيحتين من المكدس. إذا كانت القيمة "أقل" أكبر من القيمة "العليا" ، فانتقل إلى التصنيف L1. بعد تنفيذ هذه التعليمات ، ستصبح المكدس فارغة.

إليك ما ستبدو عليه خطوط Java bytecode:

      int x = 1;
      int var3 = duration;
      if (x > var3) {
         ....L1...

نقوم بفك الشفرة باستخدام Intellij IDEA على طول Kotlin -> مسار Java :

   public final void warmUp(int duration) {
      int x = 1;
      int var3 = duration;
      if (x <= duration) {
         while(true) {
            String var4 = "Warming";
            boolean var5 = false;
            System.out.println(var4);
            if (x == var3) {
               break;
            }
            ++x;
         }
      }
   }

هنا يمكنك رؤية المتغيرات غير المستخدمة ( var5) وغياب استدعاء دالة println(). لا تقلق ، هذا بسبب تفاصيل تجميع الدوال المضمنة ( println()) وتعابير لامدا. عمليا لن يكون هناك أي نفقات عامة لتنفيذ هذه التعليمات ، علاوة على ذلك ، سيتم حذف الرمز الميت بفضل JIT. هذا موضوع مثير للاهتمام ، يجب تخصيصه لمقال منفصل.

من خلال رسم القياس مع المطبخ ، يمكن وصف هذه الوظيفة بأنها مهمة للطاهي "لغلي الماء لمدة 10 دقائق". علاوة على ذلك ، المحترف لدينا في مجاله:

  1. يفتح كتاب طبخ (منطقة الطريقة) ؛
  2. يجد تعليمات حول كيفية غلي الماء ( warmUp()) ؛
  3. تحضير مكان العمل ، وتخصيص طبق ساخن (كومة المعامل) والحاويات (المتغيرات المحلية) للتخزين المؤقت للمنتجات.

مطبخ JVM. نظرة من الداخل. العمل مع كومة الذاكرة المؤقتة


خذ بعين الاعتبار الرمز:

val pickles = Any()

رمز بايت مفكك:

    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 3

NEW java / lang / Object - تخصيص الذاكرة لكائن فئة Objectمن كومة الذاكرة المؤقتة. لن يتم وضع الكائن نفسه على المكدس ، ولكن يوجد ارتباط إليه في كومة الذاكرة المؤقتة:


DUP - تكرار العنصر "العلوي" من المكدس. هناك حاجة إلى ارتباط واحد لتهيئة الكائن ، والثاني لحفظه في المتغيرات المحلية:


INVOKESPECIAL java / lang / Object. <init> () V - تهيئة كائن الفئة المقابلة ( Object) بواسطة الارتباط من المكدس:


ASTORE 3 هو الخطوة الأخيرة ، حفظ المرجع إلى الكائن في المتغيرات المحلية مع الفهرس 3.

رسم القياس مع المطبخ ، أقارن إنشاء كائن فئة مع الطبخ على طاولة مشتركة (كومة). للقيام بذلك ، تحتاج إلى تخصيص مساحة كافية لنفسك على طاولة التوزيع ، والعودة إلى مكان العمل ورمي ملاحظة بالعنوان (المرجع) في الحاوية المناسبة (المتغيرات المحلية). وفقط بعد ذلك ابدأ في إنشاء كائن من الفئة.

مطبخ JVM. نظرة من الداخل. تعدد


الآن فكر في هذا المثال:

    fun add(ingredient: Any) {
        ingredientsCount = ingredientsCount.inc()
        //- 
    }

هذا مثال كلاسيكي على مشكلة الترابط. لدينا عدد المكونات ingredientsCount. وظيفة add، بالإضافة إلى إضافة عنصر ، تؤدي زيادة ingredientsCount.

يبدو الرمز الثانوي المفكك كما يلي:

    ALOAD 0
    ALOAD 0
    GETFIELD com/company/Solenya.ingredientsCount : I
    ICONST_1
    IADD
    PUTFIELD com/company/Solenya.ingredientsCount : I

حالة المكدس الخاص بالمعامل عند تنفيذ التعليمات:


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


تم تنفيذ الوظيفة مرتين (مرة واحدة عن طريق كل مؤشر ترابط) ingredientsCountويجب أن تكون القيمة تساوي 2. ولكن في الواقع ، عمل أحد سلاسل العمليات بقيمة قديمة ingredientsCount، وبالتالي فإن النتيجة الفعلية هي 1 (مشكلة تحديث مفقودة).

يشبه الموقف العمل الموازي لفريق من الطهاة الذين يضيفون التوابل إلى الطبق. تخيل:

  1. هناك جدول توزيع يوضع عليه الطبق (كومة).
  2. يوجد طباخين في المطبخ (خيط 2 *).
  3. كل طباخ له طاولة قطع خاصة به ، حيث يعد مزيجًا من التوابل (JVM Stack * 2).
  4. المهمة: أضف حصتين من التوابل إلى الطبق.
  5. على طاولة التوزيع توجد قطعة من الورق يقرأون بها ويكتبون عليها الجزء الذي تمت إضافته ( ingredientsCount). ومن أجل حفظ البهارات:
    • قبل البدء في تحضير التوابل ، يجب أن يقرأ الطباخ على ورقة أن عدد التوابل المضافة غير كاف ؛
    • بعد إضافة التوابل ، يمكن للطاهي كتابة عدد ، حسب رأيه ، يتم إضافة التوابل إلى الطبق.

في ظل هذه الظروف ، قد تنشأ حالة:

  1. قراءة كوك رقم 1 أنه تمت إضافة 3 حصص من التوابل.
  2. قراءة كوك رقم 2 أنه تم إضافة 3 حصص من التوابل.
  3. يذهب كلاهما إلى مكاتبهما ويعدان مزيجًا من التوابل.
  4. يضيف كلا الطهاة البهارات (3 + 2) إلى الطبق.
  5. كتب Cook # 1 أنه تمت إضافة 4 حصص من البهارات.
  6. كتب Cook # 2 أنه تمت إضافة 4 حصص من البهارات.

خلاصة القول: كانت المنتجات مفقودة ، وتبين أن الطبق حار ، إلخ.

لتجنب مثل هذه المواقف ، هناك أدوات مختلفة مثل الأقفال ووظائف سلامة الخيط ، إلخ.

كي تختصر


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

وتجدر الإشارة إلى أن هذه الأجزاء بعيدة كل البعد عن جميع أجزاء JVM. هناك العديد من "الأشياء" الأكثر إثارة للاهتمام ، على سبيل المثال ، التجمع المستمر ، والتحقق من رمز البايت ، و JIT ، وذاكرة التخزين المؤقت للرمز ، وما إلى ذلك. ولكن لكي لا أفرط في تحميل المقالة ، ركزت فقط على العناصر الضرورية لفهم مشترك.

روابط مفيدة:


All Articles