Използването на final за променливи в Java подобрява ли събирането на боклука?

Днес моите колеги и аз имаме дискусия относно използването на ключовата дума final в Java за подобряване на събирането на отпадъци.

Например, ако напишете метод като:

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

Декларирането на променливите в метода final би помогнало на събирането на боклук да изчисти паметта от неизползваните променливи в метода, след като методът излезе.

Това истина ли е?


person Goran Martinic    schedule 20.11.2008    source източник
comment
всъщност има две неща тук. 1) когато пишете в локално поле на метод и екземпляр. Когато пишете на екземпляр, може да има полза.   -  person Eugene    schedule 18.12.2019


Отговори (15)


Ето един малко по-различен пример, такъв с крайни полета от референтен тип, а не локални променливи от окончателен тип стойност:

public class MyClass {

   public final MyOtherObject obj;

}

Всеки път, когато създавате екземпляр на MyClass, вие ще създавате изходяща препратка към екземпляр на MyOtherObject и GC ще трябва да следва тази връзка, за да търси живи обекти.

JVM използва GC алгоритъм за маркиране, който трябва да изследва всички референции на живо в "коренните" местоположения на GC (като всички обекти в текущия стек за извикване). Всеки жив обект е "маркиран" като жив и всеки обект, посочен от жив обект, също е маркиран като жив.

След завършване на фазата на маркиране, GC преминава през купчината, освобождавайки памет за всички немаркирани обекти (и уплътнявайки паметта за останалите живи обекти).

Освен това е важно да се признае, че паметта на Java heap е разделена на „младо поколение“ и „старо поколение“. Всички обекти първоначално се разпределят в младото поколение (понякога наричано "детската стая"). Тъй като повечето обекти са краткотрайни, GC е по-агресивен за освобождаване на скорошен боклук от младото поколение. Ако даден обект оцелее в цикъл на събиране на младото поколение, той се премества в старото поколение (понякога наричано "генерирано поколение"), което се обработва по-рядко.

И така, отгоре наум ще кажа „не, „окончателният“ модификатор не помага на GC да намали натоварването си“.

По мое мнение най-добрата стратегия за оптимизиране на управлението на вашата памет в Java е да елиминирате фалшивите препратки възможно най-бързо. Можете да направите това, като присвоите "null" на препратка към обект веднага щом приключите с използването му.

Или, още по-добре, минимизирайте размера на обхвата на всяка декларация. Например, ако декларирате обект в началото на метод от 1000 реда и ако обектът остане жив до затварянето на обхвата на този метод (последната затваряща фигурна скоба), тогава обектът може да остане жив много по-дълго от действителното необходимо.

Ако използвате малки методи, само с дузина редове код, тогава обектите, декларирани в рамките на този метод, ще изпаднат от обхвата по-бързо и GC ще може да върши по-голямата част от работата си в рамките на много по-ефективния младо поколение. Не искате обекти да се преместват в по-старото поколение, освен ако не е абсолютно необходимо.

person benjismith    schedule 20.11.2008
comment
Храна за размисъл. Винаги съм смятал, че вграденият код е по-бърз, но ако jvm не разполага с памет, тогава той също ще се забави. хммммм... - person WolfmanDragon; 10.12.2008
comment
Тук само предполагам... но предполагам, че JIT компилаторът може да вгради крайни примитивни стойности (не обекти), за скромни увеличения на производителността. Вграждането на код, от друга страна, е способно да произведе значителни оптимизации, но няма нищо общо с крайните променливи. - person benjismith; 10.12.2008
comment
Не би било възможно да се присвои null на вече създаден краен обект, тогава може би final вместо помощ може да направи нещата по-трудни - person Hernán Eche; 15.12.2011
comment
Човек може да използва {}, за да ограничи обхвата и в голям метод, вместо да го разделя на няколко частни метода, които може да не са подходящи за други методи на клас. - person mmm; 10.12.2012
comment
Можете също така да използвате локални променливи вместо полета, когато е възможно, за да подобрите събирането на отпадъци от паметта и да намалите референтните връзки. - person sivi; 10.03.2014
comment
може да намали натоварването. както мнозина казаха, локалните финални променливи са наистина безполезна информация за GC; но екземплярите не са. - person Eugene; 18.12.2019

Декларирането на локална променлива final няма да повлияе на събирането на отпадъци, това само означава, че не можете да модифицирате променливата. Вашият пример по-горе не трябва да се компилира, тъй като модифицирате променливата totalWeight, която е маркирана с final. От друга страна, декларирането на примитив (double вместо Double) final ще позволи тази променлива да бъде вградена в извикващия код, така че това може да доведе до известно подобрение на паметта и производителността. Това се използва, когато имате число public static final Strings в клас.

Като цяло, компилаторът и времето за изпълнение ще оптимизират, където могат. Най-добре е да напишете кода по подходящ начин и да не се опитвате да бъдете твърде хитри. Използвайте final, когато не искате променливата да бъде модифицирана. Да предположим, че всички лесни оптимизации ще бъдат извършени от компилатора и ако се притеснявате за производителността или използването на паметта, използвайте профилиращ инструмент, за да определите истинския проблем.

person Aaron    schedule 20.11.2008

Не, категорично не е вярно.

Не забравяйте, че final не означава константа, това просто означава, че не можете да промените препратката.

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

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

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

person SCdF    schedule 20.11.2008

Някои точки за изясняване:

  • Нулирането на препратка не би трябвало да помогне на GC. Ако беше така, това би означавало, че вашите променливи са над обхват. Едно изключение е случаят на непотизъм на обекта.

  • В Java все още няма разпределение в стека.

  • Декларирането на променлива като final означава, че не можете (при нормални условия) да присвоите нова стойност на тази променлива. Тъй като final не казва нищо за обхвата, той не казва нищо за ефекта му върху GC.

person Kirk    schedule 19.06.2011
comment
Има разпределение в стека (на примитиви и препратки към обекти в купчината) в java: stackoverflow.com/a/8061692/32453 Java изисква обекти в същото затваряне като анонимен клас/ламбда също да бъдат окончателни, но се оказва, че това е само за намаляване на необходимата памет/рамка/намаляване на объркването, така че не е свързано със събирането му... - person rogerdpack; 03.11.2017

Е, не знам за използването на "крайния" модификатор в този случай или ефекта му върху GC.

Но мога да ви кажа следното: използването на стойности в кутия вместо примитиви (напр. Double вместо double) ще разпредели тези обекти в купчината, а не в стека, и ще произведе ненужен боклук, който GC ще трябва да почисти.

Използвам примитиви в кутия само когато се изисква от съществуващ API или когато имам нужда от примитиви с възможност за нула.

person benjismith    schedule 20.11.2008
comment
Ти си прав. Имах нужда само от бърз пример, за да обясня въпроса си. - person Goran Martinic; 21.11.2008

Крайните променливи не могат да бъдат променяни след първоначалното присвояване (наложено от компилатора).

Това не променя поведението на събирането на боклук като такова. Единственото нещо е, че тези променливи не могат да бъдат нулирани, когато не се използват повече (което може да помогне на събирането на боклук в ситуации с ограничена памет).

Трябва да знаете, че final позволява на компилатора да прави предположения какво да оптимизира. Вграждане на код и невключване на код, за който е известно, че не е достъпен.

final boolean debug = false;

......

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

Println няма да бъде включен в байтовия код.

person Thorbjørn Ravn Andersen    schedule 30.08.2009
comment
@Eugene Зависи от вашия мениджър по сигурността и дали компилаторът е вградил променливата или не. - person Thorbjørn Ravn Andersen; 18.12.2019
comment
правилно, просто бях педантичен; нищо повече; също също предостави отговор - person Eugene; 18.12.2019

Има един не толкова известен ъглов случай с генерационни боклукчии. (За кратко описание прочетете отговора от benjismith за по-задълбочена представа прочетете статиите в края).

Идеята в поколенията GC е, че през повечето време трябва да се вземат предвид само младите поколения. Основното местоположение се сканира за препратки и след това обектите от младото поколение се сканират. По време на тези по-чести проверки не се проверяват обекти от старото поколение.

Сега проблемът идва от факта, че на даден обект не е позволено да има препратки към по-млади обекти. Когато дълготраен (старо поколение) обект получи препратка към нов обект, тази препратка трябва да бъде изрично проследена от събирача на отпадъци (вижте статия от IBM на hotspot JVM колектор), което всъщност засяга производителността на GC.

Причината, поради която стар обект не може да препраща към по-млад е, че тъй като старият обект не се проверява във второстепенни колекции, ако единствената препратка към обекта се пази в стария обект, той няма да бъде маркиран и би бил грешен освободени по време на етапа на почистване.

Разбира се, както се посочва от мнозина, ключовата дума final не засяга реално събирача на отпадъци, но гарантира, че препратката никога няма да бъде променена в по-млад обект, ако този обект оцелее след второстепенните колекции и стигне до по-старата купчина.

Статии:

IBM относно събирането на отпадъци: история, в JVM с гореща точка и производителност. Те може вече да не са напълно валидни, тъй като датират от 2003/04 г., но дават лесна за четене представа за GC.

Sun on Настройка на събирането на отпадъци

person David Rodríguez - dribeas    schedule 20.11.2008

GC действа при недостижими реф. Това няма нищо общо с „окончателно“, което е просто твърдение за еднократно възлагане. Възможно ли е GC на някои VM да използва "final"? Не виждам как и защо.

person dongilmore    schedule 20.11.2008

final на локални променливи и параметри няма значение за произведените клас файлове, така че не може да повлияе на производителността по време на изпълнение. Ако даден клас няма подкласове, HotSpot третира този клас така, сякаш е окончателен (може да отмени по-късно, ако се зареди клас, който нарушава това предположение). Вярвам, че final на методите е почти същото като класовете. final на статично поле може да позволи променливата да бъде интерпретирана като "константа по време на компилиране" и оптимизацията да се извърши от javac на тази основа. final на полета позволява на JVM известна свобода да игнорира отношенията happens-before.

person Tom Hawtin - tackline    schedule 25.11.2008

Изглежда има много отговори, които са блуждаещи предположения. Истината е, че няма окончателен модификатор за локални променливи на ниво байт код. Виртуалната машина никога няма да разбере дали вашите локални променливи са дефинирани като окончателни или не.

Отговорът на вашия въпрос е категорично не.

person Matt Quigley    schedule 17.04.2013
comment
Това може да е вярно, но компилаторът все още може да използва крайната информация по време на анализа на потока от данни. - person ; 18.07.2014
comment
@WernerVanBelle компилаторът вече знае, че една променлива се задава само веднъж. Той вече трябва да направи анализ на потока от данни, за да знае, че дадена променлива може да е нулева, да не е инициализирана преди употреба и т.н. Така че локалните финали не предлагат никаква нова информация на компилатора. - person Matt Quigley; 20.07.2014
comment
Не става. Анализът на потока от данни може да изведе много неща, но е възможно да има пълна програма за туринг вътре в блок, който ще зададе или не локална променлива. Компилаторът не може да знае предварително дали променливата ще бъде записана и изобщо ще бъде постоянна. Следователно компилаторът, без ключовата дума final, не може да гарантира, че дадена променлива е final или не. - person ; 21.07.2014
comment
@WernerVanBelle Искрено съм заинтригуван, можеш ли да дадеш пример? Не виждам как може да има окончателно присвояване на нефинална променлива, за която компилаторът не знае. Компилаторът знае, че ако имате неинициализирана променлива, той няма да ви позволи да я използвате. Ако се опитате да присвоите крайна променлива вътре в цикъл, компилаторът няма да ви позволи. Какъв е примерът, при който променлива, която МОЖЕ да бъде декларирана като окончателна, но НЕ е и компилаторът не може да гарантира, че е окончателна? Подозирам, че всеки пример би бил променлива, която не може да бъде декларирана като окончателна на първо място. - person Matt Quigley; 22.07.2014
comment
А, след като прочетох отново коментара ви, виждам, че вашият пример е променлива, инициализирана вътре в условен блок. Тези променливи не могат да бъдат окончателни на първо място, освен ако всички пътища на условие не инициализират променливата веднъж. И така, компилаторът знае за такива декларации - така ви позволява да компилирате крайна променлива, инициализирана на две различни места (помислете final int x; if (cond) x=1; else x=2;). Следователно компилаторът, без ключовата дума final, може да гарантира, че дадена променлива е final или не. - person Matt Quigley; 22.07.2014
comment
Това, което се опитах да кажа е, че без final компилаторът не знае, че променливата всъщност е константа (въпреки че може да е константа). С финал, това е сигурно. Това означава, че финалът позволява повече оптимизации, отколкото без. - person ; 22.07.2014
comment
Хм, ще трябва да дадете пример. Все още съм сигурен, че компилаторът знае със или без ключовата дума дали дадена променлива е постоянна. Представете си това: компилаторът се преструва, че всяка локална променлива е обявена за окончателна. Ако може да компилира с крайната променлива, сега знае, че може да бъде окончателна и без ключовата дума. Ако не може да компилира, той обратно знае, че променливата не може да бъде окончателна. Това дали някой е добавил или не ключовата дума в текста няма никакво значение. - person Matt Quigley; 23.07.2014
comment
Случай, при който една променлива е постоянна, но компилаторът няма да може да я открие. int a=1; докато (a‹100) a*=2; [в тази позиция 'a' винаги е 128]. Въпреки това разбирам проблема ви тук, защото този код не може да бъде написан с final. Интересен пример би бил случай, в който потенциално бихте могли да добавите final и където това всъщност би помогнало на компилатора. - person ; 23.07.2014
comment
Примерът за „Turing complete“ е: { int a = 3; if (complexFalse()) a = 4; } където complexFalse() е метод, който компилаторът не може да докаже, че винаги ще бъде false (вж. halting-problem). В този случай не можете да декларирате final, без да преобразувате оператора if в троичен израз --- в който момент потокът от данни работи отново така или иначе. IOW, макар и тривиално правилно, изключението не е полезно на практика. - person Recurse; 10.12.2014
comment
@Recurse Този пример обаче не е валиден; a във вашия пример не може да бъде деклариран като окончателен. {final int a = 3; if (complexFalse()) a = 4} няма да се компилира в Java, така че включването или отсъствието на ключовата дума final не се прилага. Въпросът е, че никога няма нищо, което човек може да направи с крайния модификатор (в Java), което да каже на компилатора нещо, което той вече не знае. - person Matt Quigley; 11.12.2014

Всички методи и променливи могат да бъдат заменени по подразбиране в подкласове. Ако искаме да предпазим подкласовете от презаписване на членовете на суперкласа, можем да ги декларираме като окончателни, като използваме ключовата дума final. Например- final int a=10; final void display(){......} Създаването на окончателен метод гарантира, че функционалността, дефинирана в суперкласа, така или иначе никога няма да бъде променена. По същия начин стойността на крайната променлива никога не може да бъде променена. Финалните променливи се държат като класови променливи.

person Debasish Pakhira    schedule 07.02.2012

Строго казано за полетата за екземпляр, final може да подобри леко производителността, ако определен GC иска да използва това. Когато се случи едновременно GC (това означава, че вашето приложение все още работи, докато GC е в ход), вижте това за по-широко обяснение, GC трябва да използват определени бариери, когато се извършва запис и/или четене. Връзката, която ви дадох, до голяма степен обяснява това, но за да го направя наистина накратко: когато GC върши някаква едновременна работа, всички четения и записи в купчината (докато този GC е в ход), се „прихващат“ и се прилагат по-късно във времето ; така че паралелната GC фаза да може да завърши работата си.

За полетата на екземпляр final, тъй като те не могат да бъдат модифицирани (освен ако не са отражение), тези бариери могат да бъдат пропуснати. И това не е просто чиста теория.

Shenandoah GC ги има на практика (въпреки че не за дълго), и можете да направите, например:

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

И ще има оптимизации в алгоритъма на GC, които ще го направят малко по-бърз. Това е така, защото няма да има бариери, пресичащи final, тъй като никой никога не трябва да ги променя. Дори не чрез отражение или JNI.

person Eugene    schedule 18.12.2019

Единственото нещо, за което мога да се сетя, е, че компилаторът може да оптимизира крайните променливи и да ги вгради като константи в кода, като по този начин в крайна сметка нямате разпределена памет.

person Sijin    schedule 20.11.2008

абсолютно, стига да направим живота на обекта по-кратък, което води до голяма полза от управлението на паметта, наскоро изследвахме функционалността за експортиране с променливи на екземпляр на един тест и друг тест с локална променлива на ниво метод. по време на тестване на натоварване, JVM изхвърля грешка outofmemory при първия тест и JVM е спрян. но във втория тест, успешно успя да получи отчета поради по-добро управление на паметта.

person santhosh kumar    schedule 27.04.2012

Единственият път, когато предпочитам да декларирам локални променливи като окончателни, е когато:

  • Трябва да ги направя окончателни, така че да могат да бъдат споделени с някакъв анонимен клас (например: създаване на нишка на демон и позволяване на достъпа до някаква стойност от метода за обхващане)

  • Искам да ги направя окончателни (например: някаква стойност, която не трябва/не се отменя по погрешка)

Помагат ли за бързото събиране на боклук?
AFAIK обект става кандидат за събиране на GC, ако няма нулеви силни препратки към него и в този случай също няма гаранция, че те ще бъдат незабавно събрани на боклука. По принцип се казва, че една силна препратка умира, когато излезе извън обхвата или потребителят изрично я преназначи на нулева препратка, като по този начин декларирането им като окончателна означава, че препратката ще продължи да съществува, докато методът съществува (освен ако обхватът му не е изрично стеснен до конкретен вътрешен блок {}), защото не можете да преназначите крайни променливи (т.е. не можете да преназначите на null). Така че мисля, че w.r.t Garbage Collection 'final' може да въведе нежелано възможно забавяне, така че човек трябва да бъде малко внимателен при определянето на обхвата, тъй като това контролира кога ще станат кандидати за GC.

person sactiw    schedule 16.09.2014