English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

فهم شامل لعملية تحميل Java Class

عملية تحميل الصف الكامل في Java

عندما يمر ملف Java من التحميل إلى التخلص، يجب أن يمر خلال أربع مراحل:

تحميل -> اتصال (تحقق + إعداد + تحليل) -> التأهيل (الإعداد قبل الاستخدام) -> الاستخدام -> التخلص

عملية التحميل (باستثناء التحميل المخصص) + الاتصال هي مسؤولية JVM بشكل كامل، متى يجب على JVM إجراء عملية التأهيل للصف (عملية التحميل + الاتصال قد اكتملت بالفعل)، هناك قواعد صارمة (أربعة حالات):

1. عند وجود تعليمات البايتكود new،getstatic،putstatic،invokestatic هذه الأربعة، إذا لم يتم تعيين الصف بعد، يتم إجراء عملية التأهيل على الفور. إنه في الواقع ثلاثة حالات: عند إنشاء مثيل لصف باستخدام new، عند قراءة أو تعيين الحقول الثابتة للصف (تتضمن الحقول الثابتة المعدلة بـ final، لأنها تم إضافتها إلى خزانة القيم الدائمة)، وعند تنفيذ الطريقة الثابتة.

2. عند استدعاء الفئة باستخدام طرق java.lang.reflect.*، إذا لم يتم تفعيل الفئة بعد، يتم تفعيلها على الفور.

3. عند تفعيل فئة، إذا لم يتم تفعيل الفئة الأم بعد، يتم تفعيل الفئة الأم أولاً.

4. عند بدء تشغيل JVM، يجب على المستخدم تحديد الفئة الرئيسية التي يجب تنفيذها (تتضمن الفئة التي تحتوي على static void main(String[] args) )، حيث ستبدأ JVM في تفعيل هذه الفئة.

تسمى الـ 4 سيناريوهات المسبقة بـ "الاستدعاء النشط" لأي فئة، والسيناريوهات الأخرى التي ليست لها علاقة بالاستدعاء النشط، تسمى "الاستدعاء غير النشط"، ولاتفعيل تفعيل الفئة.

/** 
 * سيناريو الاستدعاء غير النشط 1 
 * لا يتم تفعيل تفعيل الفئة عند استدعاء الحقول الثابتة للفئة الأم من خلال الفئة البنائية 
 * @author volador 
 * 
 */ 
class SuperClass{ 
  static{ 
    System.out.println("super class init."); 
  } 
  public static int value=123; 
} 
class SubClass extends SuperClass{ 
  static{ 
    System.out.println("sub class init."); 
  } 
} 
public class test{ 
  public static void main(String[]args){ 
    System.out.println(SubClass.value); 
  } 
} 

نتيجة الإخراج: super class init.

/** 
 * سيناريو الاستدعاء غير النشط 2 
 * لا يتم تفعيل تفعيل الفئة عند استدعاء الفئة من خلال مصفوفة 
 * @author volador 
 * 
 */ 
public class test{ 
  public static void main(String[] args){ 
    SuperClass s_list=new SuperClass[10]; 
  } 
} 

نتيجة الإخراج: لا يوجد إخراج

/** 
 * سيناريو الاستدعاء غير النشط 3 
 * يتم حفظ الكميات في خزان الكميات الثابتة للمحتويات التي يتم استدعاؤها في مرحلة التجميع، لا يتم استدعاء الفئة التي تعريف الكمية، لذا لن يتم تفعيل تفعيل الفئة التي تعريف الكمية. 
 * @author root 
 * 
 */ 
class ConstClass{ 
  static{ 
    System.out.println("ConstClass init."); 
  } 
  public final static String value="hello"; 
} 
public class test{ 
  public static void main(String[] args){ 
    System.out.println(ConstClass.value); 
  } 
} 

نتيجة الإخراج: hello (نصيحة: عند التجميع، تم تحويل ConstClass.value إلى كمية ثابتة وتوضع في خزان الكميات الثابتة لمحتويات الفئة test)

الآن، بالنسبة للتدريب على الفئة، يجب أيضًا تدريب الواجهات، وتبدو عملية التدريب على الواجهات مختلفة قليلاً عن التدريب على الفئة:

كل هذه الأكواد تستخدم static{} لإخراج معلومات التدريب، لا يمكن للواجهات القيام بذلك، ولكن يظل المترجم يصنع بناءً <clinit>() للواجهة عند التدريب على الواجهة، يستخدمه لتحديث المتغيرات الأعضاء في الواجهة، ويتم ذلك أيضًا في التدريب على الفئة. الفرق الحقيقي يكمن في النقطة الثالثة، حيث يتطلب التدريب على الفئة أن يتم تدريب الأب قبل أن يبدأ التدريب، ولكن يبدو أن التدريب على الواجهة لا يهم تدريب الواجهة الأبوية، مما يعني أن التدريب على الواجهة الفرعية لا يتطلب أن يتم تدريب الواجهة الأبوية قبل أن يتم تدريبها، ولكن فقط عند استخدام الواجهة الأبوية فعليًا (مثل استخدام النصوص الثابتة للواجهة).

دعنا نقسم عملية تحميل فئة واحدة بالكامل: تحميل -> التحقق -> التحضير -> التحليل -> التدريب

أولاً: التحميل:

    هذه المناطق يجب أن يكملها محرك JVM 3 مهام:

        1. الحصول على تدفق البيانات الثنائية المحدد للفئة من خلال اسمها الكامل.

        2. تحويل هيكل التخزين الثابت الذي يمثله هذا التدفق الثنائي إلى هيكل البيانات المكتسبة في الوقت الفعلي في منطقة الطريقة.

        3. إنشاء代表 هذه الفئة في heap java.lang.Class كمدخل للوصول إلى هذه البيانات في منطقة الطريقة.

في البحث الأول، يكون هذا مرنًا للغاية، والعديد من التقنيات تبدأ هنا، لأنها لا تقييد من حيث أين يأتي تدفق البيانات الثنائية:

من ملف class -> تحميل عادي للملفات

من حزمة ZIP -> تحميل الفئات من jar

من الإنترنت -> Applet

...

على عكس مراحل أخرى من عملية التحميل، تكون قابلية التحكم في مرحلة التحميل أقوى، لأن محميل الفئة يمكن أن يكون من النظام أو مكتوبًا بنفسك، يمكن للمبرمج كتابة محميل للفئة الخاص بك لتحكم في الحصول على تدفقات البيانات.

بعد الحصول على تدفق ثنائي، سيتم حفظه وفقًا للطريقة التي يحتاجها JVM في منطقة الطريقة، وسيتم إنشاء عنصر java.lang.Class في堆 Java وسيتم ربطه بالبيانات في ال堆.

بعد إكمال التحميل، يجب البدء في التحقق من تلك تدفقات البيانات (في الواقع، بعض الخطوات تتداخل مع بعضها البعض، مثل التحقق من صيغة الملف):

غرض التحقق: التأكد من أن معلومات تدفق البيانات لملف class تتوافق مع نكهة JVM وليس لجعل JVM يشعر بعدم الارتياح. إذا كان ملف class مكون من برمجة Java النقية، فمن الطبيعي ألا يحدث أي مشاكل غير صحية مثل انقطاع الحزم أو التحول إلى أجزاء غير موجودة، لأنه إذا حدث ذلك، سيرفض المترجم الترميز. ولكن، كما ذكرنا من قبل، لا يكون تدفق ملف Class بالضرورة من ترميز ملف Java المصدر، بل قد يكون من الإنترنت أو من مكان آخر، بل يمكنك حتى كتابة 16 بت بنفسك، إذا لم يقم JVM بتشغيل فحص هذه البيانات، فقد تكون تدفقات البيانات الضارة تؤدي إلى إنهيار JVM بشكل كامل.

يتم التحقق من خلال عدة خطوات: تحقق تنسيق الملف -> تحقق البيانات الوصفية -> تحقق بايت كود -> تحقق الاستدلالات الرمزية

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

تحقق البيانات الوصفية: تحليل المعلومات الموصوفة في بايت كود، والتأكد من أنها تتوافق مع معايير لغة Java.

تحقق البايت كود: الأكثر تعقيدًا، لتحقق من محتويات جسم الطريقة، والتأكد من أنه لن يحدث أي شيء غير متوقع أثناء تشغيله.

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

مرحلة التحقق مهمة، ولكن ليست ضرورية، إذا كان بعض الكود يستخدم بشكل متكرر وتحقق من ثقته، يمكن في هذه الحالة محاولة استخدام ميزة -Xverify:none لإغلاق معظم تدابير التحقق للكلاسيكيات، مما يقلل من وقت تحميل الكلاسيكيات.

بعد اكمال الخطوات السابقة، سيتم الانتقال إلى مرحلة التحضير:

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

public static int value=123;

في هذه المرحلة، سيكون قيمة value 0 وليس 123، لأنه لم يبدأ تنفيذ أي كود Java بعد، و123 غير مرئي، والتعليمات التي نراها لتخصيص 123 لـ value هي تعليمات موجودة في <clinit>() بعد تجميع البرنامج، لذا، تخصيص value بـ 123 سيتم تنفيذه فقط عند التدريب.

هناك استثناءً:

public static final int value=123;

في هذه المرحلة، سيتم تعيين قيمة value إلى 123. هذا يعني، في وقت التجميع، سيقوم javac بإنشاء ConstantValue الخاص بهذا القيمة الخاصة، وسيقوم jm بتخصيص هذه القيمة في مرحلة التحضير.

بعد اكمال الخطوة السابقة، يجب البدء في التحليل. يبدو أن التحليل هو تحويل الحقول، الطرق وما إلى ذلك من الفئات، وهو يتعلق بتنسيق محتويات ملف Class، وليس هناك تعمق فيه.

عملية التكوين هي الخطوة الأخيرة في عملية تحميل الكلاس:}

في عملية تحميل الكلاس السابقة، بالإضافة إلى أن المستخدم يمكن أن يشارك في المرحلة التحميل من خلال محول كلاس مخصص، يتم تنفيذ جميع العمليات الأخرى تحت إشراف JVM، ولكن عندما نصل إلى التكوين، يبدأ التنفيذ الفعلي للكود في Java.

سيتم تنفيذ بعض العمليات المسبقة، وينبغي التمييز بين المرحلة التحضيرية، حيث تم تنفيذ تخصيص متغيرات الكلاس مرة واحدة بالفعل.

في الواقع، هذه الخطوة هي عملية تنفيذ <clinit>(); في برنامج. دعونا ندرس <clinit>() الآن:

يُدعى <clinit>() بناءر تكوين الكلاس، ويُجمع فيه جميع عمليات تخصيص متغيرات الكلاس والجمل في الكود الثابت الثابت ويوضع في نفس الترتيب الذي تم فيه ترتيبه في ملف المصدر.

يختلف <clinit>(); عن بناءر الكلاس، حيث لا يتطلب دعوة <clinit>(); للكلاس الأب بشكل مباشر، ويضمن المحول أن يتم تنفيذ <clinit>(); للكلاس الفرعي قبل تنفيذه، أي أن أول <clinit>() يتم تنفيذه في المحول هو java.lang.Object.

دعونا نأخذ مثالاً لشرح ذلك:

static class Parent{ 
  public static int A=1; 
  static{ 
    A=2; 
  } 
} 
static class Sub extends Parent{ 
  public static int B=A; 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

أولاً، تم الاستعانة بالبيانات الثابتة في Sub.B، ويجب على كلاس Sub التكوين. في نفس الوقت، يجب على كلاس الأب Parent تنفيذ عملية التكوين أولاً. بعد تكوين Parent، سيكون A=2، لذا سيكون B=2؛ العملية السابقة تشبه:

static class Parent{ 
  <clinit>(){ 
    public static int A=1; 
    static{ 
      A=2; 
    } 
  } 
} 
static class Sub extends Parent{ 
  <clinit>(){ // jvm سيفحص أولاً هذا النوع من الكود في الكلاس الأب قبل تنفيذه هنا 
  public static int B=A; 
  } 
} 
public static void main(String[] args){ 
  System.out.println(Sub.B); 
} 

ليس من الضروري أن يكون <clinit>(); ضروري للكلاس أو الواجهة، إذا لم يتم تخصيص متغيرات الكلاس أو لم يتم وجود كود ثابت ثابت، فإن عملية <clinit>() لن يتم إنشاؤها من قبل المبرمج.

بسبب أن لا يمكن وجود كود ثابت مثل static{} في واجهة، ولكن يمكن أن تكون هناك عمليات تخصيص المتغيرات عند بدء التكوين، لذا يتم إنشاء بناءر <clinit>() في واجهة. ولكن يختلف هذا عن الكلاس، حيث لا يتطلب تنفيذ <clinit>(); للاختبار الفرعي قبل تنفيذ <clinit>(); للكلاس الأب، ويتم تخصيص الكلاس الأب فقط عند استخدام المتغيرات المحددة في الكلاس الأب.

بالإضافة إلى ذلك، لن يتم تنفيذ <clinit>()؛ لمبدأ التحقق من الكائن في وقت التحميل.}

بالإضافة إلى ذلك، ستحقق JVM من أن كلاً من <clinit>(); لكل كائن يتم تنفيذه بشكل صحيح في بيئة متعددة الأنماط.<بسبب أن التحميل سيتم مرة واحدة فقط>.

سأوضح ذلك باستخدام مثال:

public class DeadLoopClass { 
  static{ 
    if(true){ 
    System.out.println("سيتم تحميل ["+Thread.currentThread()+"]، وستبدأ الآن بمرحلة الحلقة الدورانية غير المحدودة"); 
    while(treu){}   
    } 
  } 
  /** 
   * @param args 
   */ 
  public static void main(String[] args) { 
    // TODO Auto-generated method stub 
    System.out.println("toplaile"); 
    Runnable run=new Runnable(){ 
      @Override 
      public void run() { 
        // TODO Auto-generated method stub 
        System.out.println("["+Thread.currentThread()+"] سيتم إنشاء كائن من هذا الكائن"); 
        DeadLoopClass d=new DeadLoopClass(); 
        System.out.println("["+Thread.currentThread()+"] تم الانتهاء من تحميل الكائن"); 
      }}; 
      new Thread(run).start(); 
      new Thread(run).start(); 
  } 
} 

في هذا السياق، ستلاحظ ظاهرة الحجب عند تشغيلها.

شكرًا على قراءتك، آمل أن تكون قد ساعدتك، شكرًا لدعمك لموقعنا!

أنت قد تعجبك