java ويندوز - لماذا من أي وقت مضى تنفيذ الصيغة النهائية()؟




برنامج شرح (17)

لست متأكدًا مما يمكنك فعله ، لكن ...

[email protected] ~/jdk1.6.0_02/src/
$ find . -name "*.java" | xargs grep "void finalize()" | wc -l
41

لذا أعتقد أن الشمس وجدت بعض الحالات التي (يعتقدون) أنها يجب أن تستخدم.

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

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

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

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


لقد كنت أفعل جافا مهنيا منذ عام 1998 ، ولم أقم أبدا بتنفيذ اللمسات الأخيرة (). ليس مرة واحدة.


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


قاعدة بسيطة: لا تستخدم أبدًا finalizers. الحقيقة وحدها أن كائن يحتوي على finalizer (بغض النظر عن التعليمات البرمجية التي ينفذها) كافية لتسبب مقدار كبير من البيانات المهملة لجمع البيانات المهملة.

من article بقلم بريان جويتز:

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


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

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


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


class MyObject {
    Test main;

    public MyObject(Test t) {    
        main = t; 
    }

    protected void finalize() {
        main.ref = this; // let instance become reachable again
        System.out.println("This is finalize"); //test finalize run only once
    }
}

class Test {
    MyObject ref;

    public static void main(String[] args) {
        Test test = new Test();
        test.ref = new MyObject(test);
        test.ref = null; //MyObject become unreachable,finalize will be invoked
        System.gc(); 
        if (test.ref != null) System.out.println("MyObject still alive!");  
    }
}

====================================

نتيجة:

هذا هو وضع اللمسات الأخيرة

MyObject لا يزال على قيد الحياة!

=====================================

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


ﻗﻮاﺋﻢ اﻹﺟﺎﺑﺔ اﻟﻤﻘﺒﻮﻟﺔ ﻳﻤﻜﻦ أن ﻳﺘﻢ إﻏﻼق اﻟﻤﺼﺪر أﺛﻨﺎء اﻻﻧﺘﻬﺎء.

ومع ذلك ، تُظهر هذه الإجابة أنه على الأقل في java8 مع برنامج التحويل البرمجي JIT ، فإنك تواجه مشكلات غير متوقعة حيث يتم استدعاء finalizer أحيانًا حتى قبل الانتهاء من القراءة من دفق تم الاحتفاظ به بواسطة الكائن.

لذلك ، حتى في هذا الموقف ، لن نوصيك بالوصول إلى الصيغة النهائية.


أنا شخصياً لا استخدم أبداً finalize() إلا في ظرف نادر: قمت بعمل مجموعة مخصصة عامة ، وقمت بكتابة طريقة finalize() مخصصة تقوم بما يلي:

public void finalize() throws Throwable {
    super.finalize();
    if (destructiveFinalize) {
        T item;
        for (int i = 0, l = length(); i < l; i++) {
            item = get(i);
            if (item == null) {
                continue;
            }
            if (item instanceof Window) {
                ((Window) get(i)).dispose();
            }
            if (item instanceof CompleteObject) {
                ((CompleteObject) get(i)).finalize();
            }
            set(i, null);
        }
    }
}

( CompleteObject هي واجهة قمت بها والتي تتيح لك تحديد أنك قمت بتطبيق طرق Object نادر التطبيق مثل #finalize() و #hashCode() و #clone() )

لذا ، باستخدام #setDestructivelyFinalizes(boolean) ، فإن البرنامج الذي يستخدم مجموعتي يمكنه (المساعدة) ضمان تدمير أي مرجع لهذه المجموعة يدمر أيضًا إشارات إلى محتوياته ويتخلص من أي نوافذ قد تبقي JVM على قيد الحياة بدون قصد. اعتبرت أيضًا أن أوقف أي خيوط ، لكن ذلك فتح علبة جديدة كاملة من الديدان.


finalize () هو تلميح إلى JVM أنه قد يكون من الجيد تنفيذ التعليمات البرمجية الخاصة بك في وقت غير محدد. هذا جيد عندما تريد رمز فشل في تشغيل غامضة.

كما أن القيام بأي شيء مهم في الرسائل النهائية (أي شيء باستثناء التسجيل) جيد أيضًا في ثلاث حالات:

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

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

في أحيان أخرى ، تحتاج إلى المورد الذي تعتقد أنه قادر على أن يكون أكثر قوة. على سبيل المثال ، لماذا تحتاج إلى إغلاق هذا الاتصال؟ يجب أن يعتمد في النهاية على نوع من I / O يوفره النظام (مقبس ، ملف ، أيا كان) ، فلماذا لا يمكنك الاعتماد على النظام لإغلاقه لك عندما يتم اقتطاف أدنى مستوى من الموارد؟ إذا كان الخادم في الطرف الآخر يتطلب منك تمامًا إغلاق الاتصال بشكل صحيح بدلاً من إسقاط المقبس ، فما الذي سيحدث عند قيام شخص ما بالسير فوق كابل الطاقة الخاص بالجهاز الذي تعمل عليه الكود ، أم تنتهي الشبكة المتداخلة؟

تنويه: لقد عملت على تنفيذ JVM في الماضي. أنا أكره finalizers.


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

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

انظر إلى فصول "المرجع". مرجع ضعيف ، إلخ.

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

شيء آخر احترس من أجل. في أي وقت تشاهد فيه أي شيء مثل هذا في أي مكان في الكود (ليس فقط في وضع اللمسات الأخيرة ، ولكن هذا هو المكان الذي من المرجح أن تراه فيه):

public void finalize() {
  ref1 = null;
  ref2 = null;
  othercrap = null;
}

إنها علامة على أن شخصًا ما لم يكن يعرف ما يفعله. "تنظيف" مثل هذا هو في الواقع لا حاجة إليه. عندما يكون الفصل هو GC'd ، يتم ذلك تلقائيًا.

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

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

لا ينبغي أبدا أن تستخدم لتنظيف الأشياء "أسرع".


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


يمكن أن يكون مفيدًا لإزالة الأشياء التي تمت إضافتها إلى مكان عالمي / ثابت (بدون الحاجة) ، ويجب إزالتها عند إزالة الكائن. على سبيل المثال:

    private void addGlobalClickListener() {
        weakAwtEventListener = new WeakAWTEventListener(this);

        Toolkit.getDefaultToolkit().addAWTEventListener(weakAwtEventListener, AWTEvent.MOUSE_EVENT_MASK);
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        if(weakAwtEventListener != null) {
            Toolkit.getDefaultToolkit().removeAWTEventListener(weakAwtEventListener);
        }
    }

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

من الأفضل تجنب ذلك ، مع استثناء محتمل لتشخيصات test-env.

يحتوي Eckel Thinking في Java على قسم جيد في هذا.


يجب إغلاق الموارد (الملف ، المقبس ، الدفق وما إلى ذلك) بمجرد الانتهاء من استخدامها. لديهم بشكل عام أسلوب close() الذي ندعوه بشكل عام في الجزء finally من عبارات try-catch . في بعض الأحيان يمكن استخدام finalize() من قِبل عدد قليل من المطورين ، ولكن المنظمة البحرية الدولية ليست طريقة مناسبة لعدم وجود ضمان بأن الاستدعاء النهائي سوف يكون دائمًا.

في Java 7 ، لدينا بيان try-with-resources التي يمكن استخدامها مثل:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
  // Processing and other logic here.
} catch (Exception e) {
  // log exception
} finally {
  // Just in case we need to do some stuff here.
}

في المثال أعلاه ، سيؤدي try-with-resource تلقائيًا إلى إغلاق المورد BufferedReader أسلوب close() . إذا أردنا أن نتمكن أيضًا من تنفيذ نظام Closeable في Closeable الخاصة واستخدامه بطريقة مماثلة. المنظمة البحرية الدولية يبدو أكثر أنيق وبسيط لفهم.


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

كان هذا مع JDK 1.5 والذي لا يزال قيد الاستخدام على نطاق واسع.

لم نكتشف أن شيئًا ما قد حدث حتى وقت متأخر ، ولكن في النهاية كان الجاني دائمًا هو الأسلوب النهائي () الذي يستخدم مكالمات JNI.


أجريت اختبارات على Eclipse vs Netbeans 8.0.2 ، كلاهما مع الإصدار 1.8 من Java ؛ اعتدت System.nanoTime() للقياسات.

كسوف:

حصلت على نفس الوقت في الحالتين - حوالي 1.564 ثانية .

نتبيانس:

  • باستخدام "#": 1.536 ثانية
  • باستخدام "ب": 44.164 ثانية

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

بعد المزيد من البحث ، أدركت أن المشكلة هي line-wrapping الحد الأقصى لمخزون Netbeans (لا يقتصر الأمر على System.out.println ) ، كما هو موضح في هذا الكود:

for (int i = 0; i < 1000; i++) {
    long t1 = System.nanoTime();
    System.out.print("BBB......BBB"); \\<-contain 1000 "B"
    long t2 = System.nanoTime();
    System.out.println(t2-t1);
    System.out.println("");
}

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

BBB...31744
BBB...31744
BBB...31744
BBB...31744
BBB...226365807
BBB...31744
BBB...31744
BBB...31744
BBB...31744
BBB...226365807
.
.
.

وما إلى ذلك وهلم جرا..

ملخص:

  1. يعمل Eclipse بشكل مثالي مع "B"
  2. لدى Netbeans مشكلة التفاف خط يمكن حلها (لأن المشكلة لا تحدث في الكسوف) (بدون إضافة مسافة بعد B ("B")).




java jvm