Чи покращує збір сміття використання final для змінних у Java?


86

Сьогодні ми з колегами обговорюємо використання finalключового слова в Java для поліпшення збору сміття.

Наприклад, якщо ви пишете такий метод, як:

public Double doCalc(final Double value)
{
   final Double maxWeight = 1000.0;
   final Double totalWeight = maxWeight * value;
   return totalWeight;  
}

Оголошення змінних у методі finalдопоможе збиранню сміття очистити пам’ять від невикористаних змінних у методі після виходу методу.

Це правда?


насправді тут є дві речі. 1) при записі в локальне поле методу та екземпляр. Коли ви пишете в екземпляр, може бути користь.
Євген

Відповіді:


86

Ось дещо інший приклад, той із полями остаточного посилального типу, а не локальними змінними остаточного значення:

public class MyClass {

   public final MyOtherObject obj;

}

Кожного разу, коли ви створюєте екземпляр MyClass, ви будете створювати вихідне посилання на екземпляр MyOtherObject, і GC повинен буде перейти за цим посиланням, щоб шукати реальні об'єкти.

JVM використовує алгоритм розгортання позначок GC, який повинен перевірити всі реальні рецензії в "кореневих" місцях GC (як і всі об'єкти в поточному стеку викликів). Кожен живий об'єкт "позначений" як живий, а будь-який об'єкт, на який посилається живий об'єкт, також позначений як живий.

Після завершення етапу позначки GC проносить купу, звільняючи пам'ять для всіх не позначених об'єктів (і ущільнюючи пам'ять для решти об'єктів, що живуть).

Крім того, важливо визнати, що куча пам'яті Java розділена на "молоде покоління" і "старе покоління". Всі об'єкти спочатку виділяються молодому поколінню (іноді їх називають "дитячою"). Оскільки більшість об’єктів недовговічні, ГК більш агресивно ставиться до звільнення недавнього сміття від молодого покоління. Якщо об'єкт переживає цикл збору молодого покоління, він переходить у старе покоління (іноді його називають "щасливим поколінням"), яке обробляється рідше.

Отже, з кінця голови я збираюся сказати "ні, модифікатор" остаточний "не допомагає GC зменшити навантаження".

На мій погляд, найкраща стратегія для оптимізації управління пам’яттю в Java - це якнайшвидше усунення помилкових посилань. Ви можете зробити це, призначивши "null" посилання на об'єкт, як тільки ви закінчите його використовувати.

Або, ще краще, мінімізувати розмір кожної області декларації. Наприклад, якщо ви оголошуєте об'єкт на початку методу з 1000 рядків, і якщо об'єкт залишається живим до закінчення області дії цього методу (останньої фігурної фігурної дужки), тоді об'єкт може залишатися в живих набагато довше, ніж насправді необхідний.

Якщо ви використовуєте невеликі методи, що мають лише близько десятка рядків коду, тоді об’єкти, оголошені в цьому методі, швидше вийдуть за межі сфери дії, і GC зможе виконати більшу частину своєї роботи в рамках набагато ефективнішого молоде покоління. Ви не хочете, щоб об’єкти переносились у старше покоління, якщо це не є абсолютно необхідним.


Їжа для роздумів. Я завжди думав, що вбудований код є швидшим, але якщо jvm не вистачає пам'яті, він також буде повільним.
хммммм

1
Я тут просто здогадуюсь ... але я припускаю, що компілятор JIT може вбудовувати кінцеві примітивні значення (не об’єкти), для скромного підвищення продуктивності. Вставлення коду, навпаки, здатне забезпечити значну оптимізацію, але не має нічого спільного з кінцевими змінними.
benjismith

2
Неможливо було б присвоїти null вже створеному кінцевому об'єкту, тоді, можливо, final, а не допомога, може ускладнити ситуацію
Hernán Eche

1
Можна використовувати {}, щоб також обмежити обсяг у великому методі, замість того, щоб розбити його на кілька приватних методів, які можуть не мати відношення до інших методів класу.
ммм

Ви також можете використовувати локальні змінні замість полів, коли це можливо для покращення збору сміття в пам'яті та зменшення посилальних зв'язків.
sivi

37

Оголошення локальної змінної finalне вплине на збір сміття, це лише означає, що ви не можете змінити змінну. Ваш приклад вище , не повинен становити , як ви змінюєте змінну , totalWeightяка була зазначеними final. З іншого боку, оголошення примітивної ( doubleзамість Double) finalволі дозволяє вбудовувати цю змінну у викличний код, що може спричинити деяке покращення пам'яті та продуктивності. Це використовується, коли у вас є номер public static final Stringsв класі.

Загалом, компілятор та час виконання оптимізують, де це можливо. Найкраще писати код належним чином і не намагатися бути занадто хитрим. Використовуйте, finalколи ви не хочете, щоб змінну змінювали. Припустимо, що будь-які прості оптимізації будуть виконуватися компілятором, і якщо ви турбуєтесь про продуктивність чи використання пам'яті, використовуйте профайлер, щоб визначити справжню проблему.


26

Ні, це категорично неправда.

Пам'ятайте, що finalце не означає постійність, це просто означає, що ви не можете змінити посилання.

final MyObject o = new MyObject();
o.setValue("foo"); // Works just fine
o = new MyObject(); // Doesn't work.

Може бути якась невелика оптимізація, заснована на знанні того, що JVM ніколи не доведеться модифікувати посилання (наприклад, не мати перевірки, чи не змінилося воно), але це було б настільки незначно, щоб не турбуватися.

Final слід розглядати як корисні метадані для розробника, а не як оптимізацію компілятора.


17

Деякі моменти для роз’яснення:

  • Обнулення посилання не повинно допомогти GC. Якби це сталося, це означало б, що ваші змінні перевищені. Винятком є ​​випадок об’єктного кумівства.

  • Наразі в Java немає розподілу по стеку.

  • Оголошення змінної остаточним означає, що ви не можете (за звичайних умов) призначити нове значення для цієї змінної. Оскільки фінал нічого не говорить про сферу дії, він нічого не говорить про його вплив на ГХ.


У Java є виділення у стеку (примітивів та посилань на об’єкти в купі): stackoverflow.com/a/8061692/32453 Java вимагає, щоб об’єкти в тому самому закритті, що і анонімний клас / лямбда, були остаточними, але повертається це просто для зменшення "потрібної пам'яті / фрейму" / зменшення плутанини, тому не пов'язано зі збиранням ...
rogerdpack

11

Ну, я не знаю ні про використання "остаточного" модифікатора в цьому випадку, ні про його вплив на GC.

Але я можу сказати вам наступне: використання вкладених значень замість примітивів (наприклад, Double замість double) виділить ці об'єкти в купі, а не стеку, і призведе до непотрібного сміття, яке GC доведеться очистити.

Я використовую примітивні елементи в коробці лише тоді, коли це вимагає існуючий API, або коли мені потрібні примитиви, що дозволяють обнулювати.


1
Ти правий. Мені потрібен був лише швидкий приклад, щоб пояснити своє запитання.
Горан Мартініч,

5

Кінцеві змінні не можуть бути змінені після початкового призначення (накладеного компілятором).

Це не змінює поведінку збору сміття як такого. Єдине, що ці змінні не можуть бути обнулені, коли вони більше не використовуються (що може допомогти збору сміття в ситуаціях, де немає пам'яті).

Ви повинні знати, що final дозволяє компілятору робити припущення щодо того, що оптимізувати. Вбудований код та не включаючи код, про який відомо, що недоступний.

final boolean debug = false;

......

if (debug) {
  System.out.println("DEBUG INFO!");
}

Println не буде включено до байтового коду.


@Eugene Залежить від вашого менеджера безпеки та від того, чи компілятор вказав змінну чи ні.
Thorbjørn Ravn Andersen

правильно, я просто був педантичним; нічого більше; також дав відповідь
Євген

4

Існує не дуже відомий випадок з генераторськими збирачами сміття. (Для короткого опису прочитайте відповідь Бенджісміта, щоб глибше зрозуміти статті - наприкінці).

Ідея поколінь ГК полягає в тому, що здебільшого потрібно враховувати лише молоді покоління. Місцеположення кореня сканується на наявність посилань, а потім скануються об’єкти молодого покоління. Під час цієї частішої розгортки не перевіряється жоден об'єкт старого покоління.

Тепер проблема походить від того, що об’єкту заборонено мати посилання на молодші об’єкти. Коли довгоживучий (старе покоління) об'єкт отримує посилання на новий об'єкт, це посилання має явно відстежуватися збирачем сміття (див. Статтю від IBM про колектор JVM точки доступу ), фактично впливаючи на продуктивність GC.

Причина, через яку старий об'єкт не може посилатися на молодший, полягає в тому, що, оскільки старий об'єкт не перевіряється в незначних колекціях, якщо єдине посилання на об'єкт зберігається у старому об'єкті, він не буде позначений і буде помилковим звільнено під час розгортки.

Звичайно, як зазначають багато хто, ключове слово final не впливає на збирач сміття, але гарантує, що посилання ніколи не буде змінено на молодший об'єкт, якщо цей об'єкт переживе незначні колекції та перенесе його на стару купу.

Статті:

IBM про збір сміття: історія , у JVM точки доступу та продуктивність . Вони можуть перестати бути повноправними, оскільки це датується 2003/04 роком, але вони дають легке для читання розуміння ГК.

НД на налаштування збору сміття


3

GC діє на недосяжні посилання. Це не має нічого спільного з "остаточним", що є лише твердженням про одноразове призначення. Чи можливо, що деякі віртуальні машини GC можуть використовувати "final"? Я не розумію, як і чому.


3

finalдля локальних змінних та параметрів не має різниці у створених файлах класів, тому не може впливати на продуктивність виконання. Якщо у класі немає підкласів, HotSpot розглядає цей клас так, ніби він остаточний (він може бути скасований пізніше, якщо завантажений клас, який порушує це припущення). Я вважаю, що finalметоди дуже схожі на класи. finalу статичному полі може дозволити інтерпретувати змінну як "константу часу компіляції" та оптимізацію виконувати javac на цій основі. finalon fields дозволяє JVM певну свободу ігнорувати відносини, що трапляються раніше .


2

Здається, є багато відповідей, які блукають здогадками. Правда в тому, що остаточного модифікатора для локальних змінних на рівні байт-коду не існує. Віртуальна машина ніколи не дізнається, що ваші локальні змінні були визначені як остаточні чи ні.

Відповідь на ваше запитання - рішуче "ні".


Це може бути правдою, але компілятор все одно може використовувати остаточну інформацію під час аналізу потоку даних.

@WernerVanBelle компілятор вже знає, що змінна встановлюється лише один раз. Він вже повинен провести аналіз потоку даних, щоб знати, що змінна може бути нульовою, не ініціалізується перед використанням і т. Д. Отже, місцеві фінали не пропонують нову інформацію компілятору.
Matt Quigley

Це не так. Аналіз потоку даних може вивести багато речей, але можна мати повну програму Тьюрінга всередині блоку, яка буде або не встановлюватиме локальну змінну. Компілятор не може заздалегідь знати, чи буде змінна записана і чи буде вона незмінною. Тому компілятор без ключового слова final не може гарантувати, що змінна є кінцевою чи ні.

@WernerVanBelle Я щиро заінтригований, ти можеш навести приклад? Я не розумію, як може бути остаточне присвоєння неофіційній змінній, про яку компілятор не знає. Компілятор знає, що якщо у вас є неініціалізована змінна, він не дозволить вам її використовувати. Якщо ви спробуєте призначити кінцеву змінну всередині циклу, компілятор вам не дозволить. Який приклад, коли змінна, яку МОЖЕ оголосити остаточною, але НЕ, і компілятор не може гарантувати, що вона остаточна? Я підозрюю, що будь-який приклад був би змінною, яку не можна було б оголосити остаточною як спочатку.
Matt Quigley

Ах, перечитавши ваш коментар, я бачу, що ваш приклад - це змінна, ініціалізована всередині умовного блоку. Ці змінні не можуть бути остаточними, перш за все, якщо всі шляхи умов не ініціалізують змінну один раз. Отже, компілятор знає про такі оголошення - саме так він дозволяє компілювати кінцеву змінну, ініціалізовану у двох різних місцях (подумайте final int x; if (cond) x=1; else x=2;). Таким чином, компілятор без остаточного ключового слова може гарантувати, що змінна є кінцевою чи ні.
Matt Quigley

1

Всі методи та змінні можуть бути замінені за замовчуванням у підкласах. Якщо ми хочемо зберегти підкласи від перевизначення членів суперкласу, ми можемо оголосити їх як остаточні за допомогою ключового слова final. Наприклад, - final int a=10; final void display(){......} Зробити метод остаточним гарантує, що функціональність, визначена в суперкласі, ні в якому разі не буде змінена. Так само значення кінцевої змінної ніколи не може бути змінено. Кінцеві змінні поводяться як змінні класу.


1

Строго кажучи про поля екземплярів , це final може незначно покращити продуктивність, якщо конкретний GC хоче використати це. Коли GCвідбувається одночасне (це означає, що ваша програма все ще працює, поки GC триває ), дивіться це для більш широкого пояснення , GC повинні використовувати певні бар'єри, коли записуються та / або читаються. Посилання, яке я вам дав, багато в чому це пояснює, але, щоб зробити це по-справжньому коротким: коли a GCвиконує якусь одночасну роботу, все читання та запис у купу (поки цей GC триває), «перехоплюється» і застосовується пізніше; так що паралельна фаза ГХ може закінчити свою роботу.

Для finalполів примірника, так як вони не можуть бути змінені (якщо відображення), ці бар'єри можуть бути опущені. І це не просто чиста теорія.

Shenandoah GCмає їх на практиці (хоча і недовго ), і ви можете зробити, наприклад:

-XX:+UnlockExperimentalVMOptions  
-XX:+UseShenandoahGC  
-XX:+ShenandoahOptimizeInstanceFinals

І будуть оптимізовані алгоритми GC, що зробить це трохи швидшим. Це тому, що не буде перешкод для перехоплення final, оскільки ніхто ніколи не повинен їх змінювати. Навіть не через відображення чи JNI.


0

Єдине, що я можу придумати, так це те, що компілятор може оптимізувати кінцеві змінні та вбудувати їх як константи в код, таким чином, у вас не буде виділено пам'яті.


0

абсолютно, поки скорочується життя об'єкта, що приносить велику користь від управління пам'яттю, нещодавно ми розглянули функціональність експорту, що має змінні екземпляра в одному тесті, а інший тест має локальну змінну рівня методу. під час тестування навантаження JVM викидає пам'ятьпомилки під час першого тесту, і JVM зупиняється. але в другому тесті вдалося отримати звіт завдяки кращому керуванню пам’яттю.


0

Єдиний раз, коли я вважаю за краще оголошувати локальні змінні як остаточні, це коли:

  • Я повинен зробити їх остаточними, щоб ними можна було поділитися з яким-небудь анонімним класом (наприклад: створення потоку демона і надання йому доступу до якогось значення з методу вкладання)

  • Я хочу зробити їх остаточними (наприклад: якесь значення, яке не повинно / не буде замінено помилково)

Чи допомагають вони у швидкому вивозі сміття?
AFAIK об'єкт стає кандидатом на колекцію GC, якщо у нього немає нульових сильних посилань на нього, і в цьому випадку також немає гарантії, що вони будуть негайно зібрані сміття. Взагалі кажуть, що сильне посилання відмирає, коли воно виходить за межі обсягу або користувач явно перепризначає його до нульового посилання, таким чином, оголошуючи їх остаточним, означає, що посилання буде існувати доти, доки метод не існує (якщо область його використання явно не звужена до конкретний внутрішній блок {}), оскільки ви не можете перепризначити кінцеві змінні (тобто не можете перепризначити нульові значення). Тому я думаю, що „остаточне” збору сміття може призвести до небажаної можливої ​​затримки, тому слід бути обережним у визначенні обсягу, який визначає, коли вони стануть кандидатами на GC.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.