۲۹ شهریور ۱۴۰۳

Techboy

اخبار و اطلاعات روز تکنولوژی

نحوه رسیدگی به خطاهای جاوا و پاکسازی بدون نهایی کردن

روش نهایی جاوا در جاوا 18 منسوخ خواهد شد و در نسخه بعدی به طور کامل حذف خواهد شد. بیایید به جایگزین ها نگاه کنیم.

روش نهایی جاوا در جاوا ۱۸ منسوخ خواهد شد و در نسخه بعدی به طور کامل حذف خواهد شد. بیایید به جایگزین ها نگاه کنیم.

پس از چندین سال سر و صدا، جاوا در حال آماده شدن برای منسوخ کردن روش finalize در JDK 18 است. این مورد تحت پوشش پیشنهاد ارتقای JDK 421 است، که نهایی شدن را به عنوان منسوخ علامت گذاری می کند و اجازه می دهد برای تست خاموش شد به طور پیش فرض فعال باقی می ماند. در نسخه بعدی به طور کامل حذف خواهد شد. در این مناسبت، بیایید نگاهی بیندازیم به معنای پایان نهایی کردن چیست و چگونه باید خطاها و پاکسازی منابع را اکنون مدیریت کنیم.

نهایی شدن چیست؟

قبل از اینکه بفهمیم چرا نهایی کردن حذف می‌شود و به جای آن از چه چیزی استفاده کنیم، بیایید بفهمیم نهایی‌سازی چیست یا بود.

ایده اصلی این است که به شما امکان می‌دهد روشی را بر روی اشیاء خود تعریف کنید که زمانی اجرا می‌شود که شی برای جمع‌آوری زباله آماده است. از نظر فنی، وقتی یک شیء به فانتوم قابل دسترسی، به این معنی که هیچ مرجع قوی یا ضعیفی در JVM باقی نمانده است.

در آن لحظه، ایده این است که JVM متد object.finalize() را اجرا کند و کدهای خاص برنامه، هر منبعی مانند جریان‌های I/O یا دسته‌ها را پاکسازی می‌کند. انبارهای داده.

کلاس Object ریشه در جاوا دارای یک متد finalize()، همراه با روش های دیگر مانند equals() و hashCode(). این برای فعال کردن هر شیء در هر برنامه جاوا است که تا به حال در این مکانیسم ساده برای جلوگیری از نشت منابع مشارکت داشته باشد.

توجه داشته باشید که این مورد همچنین به مواردی می‌پردازد که در آن یک استثنا پرتاب می‌شود و ممکن است دیگر کد پاک‌سازی نادیده گرفته شود: شی همچنان برای جمع‌آوری زباله علامت‌گذاری می‌شود و روش نهایی آن در نهایت فراخوانی می‌شود. مشکل حل شد، درست است؟ فقط روش finalize() را روی اشیاء مصرف کننده منابع خود نادیده بگیرید.

مشکلات نهایی کردن

این ایده است، اما واقعیت چیز دیگری است. تعدادی از کاستی‌ها در Finalize وجود دارد که مانع از تحقق آرمان‌شهر پاکسازی می‌شود. (این وضعیت شبیه serialize() است، روش دیگری که روی کاغذ خوب به نظر می رسید اما در عمل مشکل ساز شد.)

در میان مشکلات نهایی کردن:

  • Finalize می‌تواند به روش‌های غیرمنتظره اجرا شود. گاهی اوقات GC تشخیص می دهد که شی شما هیچ ارجاع زنده ای به آن ندارد قبل از اینکه فکر کنید. یک به این پاسخ نسبتاً ترسناک Stack Overflow نگاه کنید.
  • Finalize ممکن است هرگز اجرا نشود یا پس از تأخیر طولانی اجرا شود. در طرف دیگر طیف، روش نهایی شما ممکن است هرگز اجرا نشود. همانطور که JEP 421 RFC بیان می کند، "GC معمولاً فقط در مواقع ضروری برای برآورده کردن درخواست های تخصیص حافظه عمل می کند." بنابراین شما در هوس هوس GC هستید.
  • Finalize می‌تواند کلاس‌های مرده را دوباره زنده کند. گاهی اوقات، یک شی استثنایی را ایجاد می کند که آن را واجد شرایط GC می کند. با این حال، روش finalize() در ابتدا شانس اجرا را پیدا می‌کند و آن متد می‌تواند هر کاری از جمله برقراری مجدد ارجاعات زنده به شی را انجام دهد. این یک منبع نشت بالقوه و خطر امنیتی است.
  • اجرای صحیح نهایی کردن مشکل است. نوشتن مستقیم یک روش نهایی جامد که کاربردی و بدون خطا باشد آنقدرها هم که به نظر می رسد آسان نیست. به طور خاص، هیچ تضمینی در مورد پیامدهای نخی نهایی وجود ندارد. نهایی‌کننده‌ها می‌توانند روی هر رشته‌ای اجرا شوند و شرایط خطا را معرفی کنند که اشکال‌زدایی آن‌ها بسیار سخت است. فراموش کردن تماس با finalize() می تواند منجر به مشکلاتی شود که به سختی قابل کشف هستند.
  • عملکرد. با توجه به غیرقابل اعتماد بودن نهایی کردن در انجام هدف اعلام شده، هزینه سربار JVM برای پشتیبانی از آن مستحق نیست.
  • Finalize باعث ایجاد برنامه های کاربردی در مقیاس بزرگ شکننده تر می شود. نتیجه نهایی، همانطور که تحقیقات نشان داده است، این است که نرم‌افزار در مقیاس بزرگ که از نهایی‌سازی استفاده می‌کند، به احتمال زیاد شکننده‌تر است و با شرایط خطای بازتولید سختی که تحت بارهای سنگین ایجاد می‌شود، مواجه می‌شود.

زندگی پس از نهایی شدن

در حال حاضر راههای مناسب برای رسیدگی به خطاها و پاکسازی چیست؟ ما در اینجا به سه گزینه نگاه خواهیم کرد: try-catch-finally blocks، try-with-resource statements و cleaners. هر کدام نقاط مثبت و منفی خود را دارند.

Try-catch-finally blocks

روش قدیمی مدیریت انتشار منابع از طریق بلوک‌های try-catch است. این در بسیاری از موقعیت ها قابل اجرا است، اما از خطا و پرمخاطب بودن رنج می برد. به عنوان مثال، برای ثبت کامل شرایط خطای تودرتو (یعنی زمانی که بستن منبع یک استثنا نیز ایجاد می کند)، به چیزی مانند فهرست ۱ نیاز دارید. 

ممکن است بیش از حد به نظر برسد، اما در یک سیستم طولانی‌مدت و به شدت مورد استفاده، این نوع شرایط می‌تواند منجر به نشت منابع شود که یک برنامه را از بین می‌برد. بنابراین، افسوس که پرحرفی باید در پایه کد تکرار شود. این چیزها برای شکستن جریان کد بدنام هستند.

فهرست ۱. مدیریت بسته شدن منبع با try-catch-finally

FileOutputStream outStream = null;
try {
  outStream = new FileOutputStream("output.file");
  ObjectOutputStream stream = new ObjectOutputStream(outStream);
  stream.write //…
  stream.close();
} catch(FileNotFoundException ffe) {
  throw new RuntimeException("Could not open file for writing", ffee);
} catch(IOException ioe) {
  System.err.println("Error writing to file");
} finally {
  if (outStream != null) {
    try {
      outStream.close();
    } catch (Exception e) {
      System.err.println(“Failed to close stream”, e);
    }
  }
}

تمام کاری که می‌خواهید در فهرست ۱ انجام دهید این است که یک جریان را باز کنید، چند بایت در آن بنویسید، و مطمئن شوید که بسته می‌شود، صرف نظر از اینکه چه استثنایی وجود دارد. برای انجام این کار، باید تماس‌ها را در یک بلوک try بپیچید و اگر استثناهای علامت‌گذاری شده وجود داشت، با آن‌ها مقابله کنید (یا با بالا بردن یک استثنای زمان اجرا پیچیده یا با چاپ استثنا در گزارش).

سپس باید یک بلوک نهایی اضافه کنید که جریان را دوبار بررسی کند. این برای اطمینان از این است که یک استثنا مانع از بسته شدن نمی شود. اما شما نمی توانید فقط جریان را ببندید. باید آن را در یک بلوک آزمایشی دیگر بپیچید تا مطمئن شوید که بسته شدن به خودی خود خطا نمی کند.

این کار برای یک نیاز ساده و معمولی کار و اختلال زیادی است.

عبارات Try-with-resource

یک عبارت try-with-resource که در جاوا ۷ معرفی شده است به شما امکان می دهد یک یا چند شی منبع را به عنوان بخشی از اعلان try مشخص کنید. این منابع تضمین می شود که پس از تکمیل بلوک try بسته می شوند.

به طور خاص، هر کلاسی که java.lang.AutoCloseable را پیاده سازی می کند، می تواند به try-with-resource عرضه شود. این تقریباً تمام منابع رایج مورد استفاده را که در اکوسیستم جاوا پیدا می کنید پوشش می دهد.

بیایید فهرست ۱ را بازنویسی کنیم تا از عبارت try-with-resource استفاده کنیم، همانطور که در فهرست ۲ مشاهده می شود.

فهرست ۲. مدیریت بسته شدن منبع با try-with-resource

try (FileOutputStream outStream = new ObjectOutputStream(outStream)) {
  ObjectOutputStream stream = new ObjectOutputStream(outStream);
  stream.write //…
  stream.close();
} catch(FileNotFoundException ffe) {
  throw new RuntimeException("Could not open file for writing", ffee);
} catch(IOException ioe) {
  System.err.println("Error writing to file");
}

می‌توانید ببینید که در اینجا چندین مزیت وجود دارد که منجر به ردپای کد کوچک‌تر می‌شود، اما بزرگ‌ترین خوبی این است که وقتی استریم (یا هر چیزی که استفاده می‌کنید) را با اعلام آن در داخل بلوک try به ماشین مجازی واگذار می‌کنید. پرانتز، دیگر لازم نیست نگران آن باشید. برای شما بسته خواهد شد. هیچ منبعی درز ندارد.

ما نیاز به مسدود کردن نهایی یا هرگونه تماسی را برای نهایی کردن حذف کردیم. این مشکل اصلی را برای اکثر موارد استفاده برطرف می کند (اگرچه پرحرفی رسیدگی به خطاهای بررسی شده باقی می ماند).

موقعیت‌هایی وجود دارد که در آن‌ها به راه‌حل دقیق‌تر و قوی‌تری نیاز است، زمانی که کارها بسیار پیچیده‌تر از آن هستند که بتوان آن‌ها را در یک بلوک واحد مانند این مدیریت کرد. برای آن مواقع، توسعه‌دهنده جاوا به چیزی قوی‌تر نیاز دارد. برای آن شرایط، به یک پاک کننده نیاز دارید.

پاک کننده ها

کلاس Cleaner در جاوا ۹ معرفی شد. پاک کننده ها به شما این امکان را می دهند که اقدامات پاکسازی را برای گروه هایی از مراجع تعریف کنید. پاک کننده ها یک پیاده سازی Cleanable تولید می کنند که رابط آن از Runnable می آید. هر Cleanable در یک رشته اختصاصی اجرا می شود که استثناها را نادیده می گیرد.

ایده در اینجا جدا کردن روال پاکسازی از کدی است که از اشیاء نیاز به تمیز کردن استفاده می کند. بیایید با مثالی که Oracle در مستندات ارائه می‌کند، که در فهرست ۳ نشان داده شده است، این موضوع را ملموس‌تر کنیم.

فهرست ۳. مثال ساده پاک کننده

public class CleaningExample implements AutoCloseable {
  // A cleaner, preferably one shared within a library
  private static final Cleaner cleaner = <cleaner>;
  static class State implements Runnable {
    State(...) {
      // initialize State needed for cleaning action
    }
    public void run() {
      // cleanup action accessing State, executed at most once
    }
  }
  private final State;
  private final Cleaner.Cleanable cleanable
  public CleaningExample() {
    this.state = new State(...);
    this.cleanable = cleaner.register(this, state);
  }
  public void close() {
    cleanable.clean();
  }
}

برای شروع، و شاید مهمتر از همه، شما می توانید به صراحت از روش close() برای پاکسازی مراجع خود استفاده کنید. این با finalize() متمایز است، که کاملاً به فراخوانی (نامعین) از جمع‌آورنده زباله وابسته است.

اگر فراخوانی close() به طور صریح انجام نشود، زمانی که شیء ارسال شده به عنوان اولین آرگومان cleaner.register() تبدیل شود، سیستم آن را برای شما اجرا خواهد کرد. فانتوم قابل دسترس با این حال، اگر قبلاً به صراحت توسط شما، توسعه‌دهنده، اجرا شده باشد، سیستم نمی‌خواندclose().

(توجه داشته باشید که مثال کد در لیست ۳ یک شی با قابلیت بسته شدن خودکار تولید می کند. این بدان معناست که می توان آن را به آرگومان یک دستور try-with-resource منتقل کرد.)

اکنون برای یک هشدار: در روش اجرای پاک کننده خود به اشیاء پاک شده ارجاعی ایجاد نکنید، زیرا این امر به طور بالقوه یک شی زامبی ایجاد می کند (یعنی دوباره شیء را به عنوان زنده ایجاد می کند). بعید است که در قالب مثال داده شده این اتفاق بیفتد، اما اگر آن را به صورت لامبدا (که به محدوده محصور آن دسترسی دارد) اجرا کنید، احتمال بیشتری دارد.

در مرحله بعد، این نظر را در نظر بگیرید، "یک پاک کننده، ترجیحاً یک پاک کننده به اشتراک گذاشته شده در یک کتابخانه." چرا اینطور است؟ به این دلیل است که هر پاک کننده یک نخ ایجاد می کند، بنابراین اشتراک پاک کننده ها باعث کاهش هزینه های اضافی برای برنامه در حال اجرا می شود.

در نهایت (جناسی در نظر گرفته شده)، توجه کنید که شیئی که نظارت می شود از کد (در مثال، State) که کار پاکسازی را انجام می دهد جدا شده است.

برای بررسی عمیق تر پاک کننده ها، این مقاله را ببینید . بینش خوبی ارائه می دهد، به ویژه در مورد موارد استفاده از آنها (در این مورد، برای دفع منابع بومی گران قیمت).

خداحافظ، نهایی کنید

جاوا همچنان در حال تکامل است. این خبر خوبی برای ما که آن را دوست داریم و استفاده می کنیم است. منسوخ شدن finalize() و افزودن رویکردهای جدید همه نشانه های خوبی از این تعهد به آینده است.