Analiza i rešenja 9 uobičajenih problema sa CMS GC u Javi
1.1 Uvod
Otkako je Sun objavio Java jezik, počelo se sa korišćenjem GC tehnologije za automatsko upravljanje memorijom, čime se izbegava problem visećih pokazivača (Dangling Pointer) koji prati ručno upravljanje, u velikoj meri se povećava efikasnost razvoja, a GC tehnologija time stiče slavu. GC ima veoma dugu istoriju — još 1960. godine John McCarthy, poznat kao „otac Lisp-a" i „otac veštačke inteligencije", objavio je GC algoritam u svom radu. Tokom proteklih 60 godina razvoj GC tehnologije napredovao je skokovito, ali ma koliko napredni sakupljači bili, oni se oslanjaju na kombinaciju ili primenu tri osnovna algoritma — drugim rečima, fundamentalni problem koji GC rešava svih ovih godina nije se promenio. Autor smatra da ni u skorijoj budućnosti GC tehnologija neće zastareti; u poređenju sa novim tehnologijama koje se iz dana u dan menjaju, klasična tehnologija poput GC-a više vredi učenja.
Dakle, može li se sposobnost rešavanja GC problema savladati sistematski? Kako analizirati uticajne faktore koji su uzajamno uzročno-posledično povezani? Na primer, RT (vreme odziva) nekog servisa naglo poraste, a pojave se četiri simptoma: povećanje GC vremena, povećanje broja blokiranih niti, povećanje broja sporih upita i visoko opterećenje CPU-a — koji je od njih stvarni uzrok? Kako proceniti da li GC ima problem? Koja su uobičajena pitanja pri korišćenju CMS-a? Kako utvrditi šta je koreni uzrok? Kako rešiti ili izbeći te probleme? Nakon čitanja ovog članka, poverujem da ćete imati sistemsko razumevanje rešavanja CMS GC problema i moći ćete da ih rešavate sa lakoćom — krenimo! Ako u tekstu ima grešaka, slobodno ih ukažite.
1.2 Pregled
Da biste sistematski savladali rešavanje GC problema, autor ovde daje jednu putanju učenja; i okvir celog članka je izložen prema toj strukturi, uglavnom u četiri velika koraka.
Izgradnja sistema znanja: od memorijske strukture JVM-a do algoritama i sakupljača smeća, učite osnove GC-a i savladajte neke uobičajene alate za analizu GC problema.
Određivanje metrika evaluacije: upoznajte se sa osnovnim metodama evaluacije GC-a, saznajte kako postaviti metrike za samostalni sistem, kao i sa sredstvima za ocenu da li GC predstavlja problem u poslovnom scenariju.
Praksa podešavanja u različitim scenarijima: primenom savladanog znanja i sistemskih metrika evaluacije analizirajte i rešite devet uobičajenih GC scenarija u CMS-u.
Sumiranje iskustava u optimizaciji: sumirajte ceo proces i navedite nekoliko autorovih saveta, a stečena iskustva unesite u sistem znanja.
2. Osnove GC-a
Pre nego što zvanično krenemo, ukratko ćemo postaviti osnovu i predstaviti uobičajene pojmove poput podele memorije JVM-a, algoritama sakupljanja i sakupljača. Čitaoci sa boljim osnovama mogu slobodno preskočiti ovaj deo.
2.1 Osnovni pojmovi
GC: sam GC ima tri značenja; u nastavku, prema konkretnom kontekstu, koristi se odgovarajuće značenje:
Garbage Collection: tehnologija sakupljanja smeća, imenica.
Garbage Collector: sakupljač smeća, imenica.
Garbage Collecting: radnja sakupljanja smeća, glagol.
Mutator: uloga koja proizvodi smeće, odnosno naša aplikacija — stvaralac smeća, koja preko Allocator-a vrši allocate i free.
TLAB: skraćenica od Thread Local Allocation Buffer; na CAS zasnovane ekskluzivne niti (Mutator Threads) mogu prioritetno da dodeljuju objekte u jedan blok memorije unutar Eden-a. Pošto je to memorijsko područje koje Java nit koristi isključivo, nema konkurentnosti za brave, pa je dodela brža; svaki TLAB je namenjen jednoj niti.
Card Table: uglavnom služi za obeležavanje stanja kartičnih stranica (card page); svaka stavka tabele karata odgovara jednoj kartičnoj stranici. Kada se nad referencom objekta unutar kartične stranice obavi operacija pisanja, write barrier će obeležiti stanje tabele karata tog objekta kao dirty. Suština tabele karata jeste rešavanje problema referenci između generacija. Kako se to tačno rešava možete pogledati na ovo pitanje na StackOverflow-u how-actually-card-table-and-writer-barrier-works, ili proučiti izvorni kod u cardTableRS.app.
2.2 Podela memorije JVM-a
Sa zvaničnog sajta JCP (Java Community Process) vidi se da je najnovija verzija Jave trenutno Java 16; buduća Java 17, kao i sadašnje Java 11 i Java 8, LTS su verzije. Specifikacija JVM-a se takođe menja sa iteracijama; pošto ovaj članak uglavnom razmatra CMS, ovde prikazujemo memorijsku strukturu Jave 8.

GC uglavnom radi u Heap oblasti i MetaSpace oblasti (plavi deo na gornjoj slici). U Direct Memory-u, ako se koristi DirectByteBuffer, onda se pri nedovoljnoj memoriji za dodelu GC indirektno njome upravlja preko Cleaner#clean.
Koraci sa kojima se suočava svaki sistem za automatsko upravljanje memorijom su: dodeliti prostor novom objektu, a zatim prikupiti prostor koji zauzimaju objekti-smeće. U nastavku ćemo detaljnije predstaviti te osnove.
2.3 Dodela objekata
U Javi se operacije nad adresama objekata uglavnom obavljaju preko Unsafe, koji poziva dva C metoda: allocate i free. Postoje dva nacina dodele:
Slobodna lista (free list): dodatnim skladistem se beleze slobodne adrese, cime se slucajni IO pretvara u sekvencijalni IO, ali se unosi dodatni prostorni trosak.
Bump pointer (pokazivac sudara): pomoci jednog pokazivaca kao granicne tacke; kada treba dodeliti memoriju, dovoljno je pomeriti pokazivac ka slobodnom kraju za rastojanje jednako velicini objekta. Dodela je prilicno efikasna, ali je polje primene ograniceno.
2.4 Prikupljanje objekata
2.4.1 Prepoznavanje smeca
Metod brojanja referenci (Reference Counting): vodi se brojac referenci za svaki objekat; svaki put kada ga neko mesto referencira, brojac se uvecava za 1, a kada referenca prestane da vazi, smanjuje se za 1. Brojac referenci se nalazi u zaglavlju objekta; objekti sa brojacem vecim od 0 smatraju se zivim. Iako se problem kruznih referenci moze resiti Recycler algoritmom, u visenitnom okruzenju promena brojaca referenci zahteva i skupe operacije sinhronizacije, pa su performanse nize; rani programski jezici su koristili ovaj algoritam.
Analiza dostiznosti, poznata i kao metod referentnog lanca (Tracing GC): polazeci od GC Root-a vrsi se pretraga objekata; objekti koji mogu biti pronadjeni su dostizni objekti. To ipak jos uvek nije dovoljno za ocenu da li je objekat ziv ili mrtav — potrebno je visekratno obelezavanje da bi se preciznije utvrdilo, a objekti izvan celog povezanog grafa mogu se prikupiti kao smece. Savremene virtuelne masine u Javi uglavnom koriste ovaj algoritam.
Napomena: metod brojanja referenci ipak moze da obradi problem kruznih referenci — ne govorite ovako na sledecem intervjuu! ~ ~
2.4.2 Algoritmi prikupljanja
Ovi algoritmi prikupljanja postoje jos od pojave automatskog upravljanja memorijom; razliciti sakuplaci ih kombinuju u razlicitim scenarijima.
Mark-Sweep (obelezi-pocisti): proces prikupljanja se deli u dve faze. Prva faza je faza pracenja (Tracing) — polazi se od GC Root-a, obilazi graf objekata i obelezava (Mark) se svaki susretnuti objekat. Druga faza je faza ciscenja (Sweep) — sakuplac proverava svaki objekat na hipu i prikuplja sve neobelezene objekte; tokom celog procesa ne dolazi do pomeranja objekata. U razlicitim implementacijama algoritam koristi tehnike poput trobojne apstrakcije (Tricolour Abstraction), bitmap obelezavanja (BitMap) itd. radi povecanja efikasnosti; prilicno je efikasan kada je mnogo zivih objekata.
Mark-Compact (obelezi-sabij): glavni cilj ovog algoritma jeste resavanje problema fragmentacije koji postoji u nepomerajucim sakuplacima. Takode se deli u dve faze: prva je slicna Mark-Sweep-u, a u drugoj se zivi objekti sabijaju prema redosledu sabijanja (Compaction Order). Glavne implementacije ukljucuju Two-Finger algoritam, Lisp2 (sliding) algoritam i Threaded Compaction algoritam.
Copying (kopiranje): prostor se deli na dva podjednako velika polu-regiona, From i To; u jednom trenutku koristi se samo jedan. Pri svakom prikupljanju zivi objekti iz jednog polu-regiona kopiranjem se premestaju u drugi. Postoje rekurzivni (predlozili Robert R. Fenichel i Jerome C. Yochelson) i iterativni (Cheney) algoritam, kao i algoritam priblizno-prioritetne pretrage koji resava probleme rekurzivnog steka i kes linija prethodna dva. Kopirajuci algoritam moze brzo da dodeljuje memoriju putem bump pointera, ali mu je slabost niska iskoriscenost prostora; pored toga, kada su zivi objekti veliki, trosak kopiranja je visok.
Uporedjujuci tri algoritma po pitanju pomeranja objekata, prostora i vremena, uz pretpostavku da je broj zivih objekata L, a velicina hipa H:
| Algoritam | Pomeranje objekata | Trošak prostora | Trošak vremena |
|---|---|---|---|
| Mark-Sweep | ne | fragmentacija | mark ~ O(W), sweep ~ O(H) |
| Mark-Sweep-Compact | da | bez fragmentacije | + kompaktiranje |
| Mark-Copy | da | 2× prostor preživelih | kopiranje preživelih |
Kada se trajanja mark, sweep, compaction i copying radnji posmatraju zajedno, grubo vazi sledeci odnos:

Iako i compaction i copying ukljucuju pomeranje objekata, zavisno od konkretnog algoritma, compaction mozda mora prvo da izracuna ciljnu adresu objekta, zatim ispravi pokazivace i na kraju premesti objekat. Copying te poslove moze da objedini, pa je nesto brzi. Takode, treba imati na umu da rezija koju GC donosi ne zavisi samo od trajanja Collector-a, vec i od Allocator-a. Ako se garantuje da nema fragmentacije memorije, dodela se moze obaviti pointer bumping-om — dovoljno je pomeriti jedan pokazivac i dodela je gotova, veoma brzo. Ako memorija ima fragmente, mora se upravljati freelist-om i slicnim nacinima, pa je dodela obicno sporija.
2.5 Sakuplaci
Trenutno u HotSpot VM-u uglavnom postoje dve velike kategorije: generacijsko i regionalno prikupljanje; detalje mozete videti na sledecoj slici, mada se buducnost postepeno krece ka regionalnom prikupljanju. U Meituan-u je deo posla probao ZGC (zainteresovani citaoci mogu procitati clanak «Istrazivanje i praksa nove generacije sakuplaca smeca ZGC»), dok ostatak uglavnom ostaje na CMS-u i G1. Pored toga, od JDK-a 11 pruza se Epsilon (A No-Op Garbage Collector), sakuplac koji ne obavlja nikakvo prikupljanje smeca, namenjen analizi performansi. Jos jedan je Azul-ov Zing JVM, ciji C4 (Concurrent Continuously Compacting Collector) sakuplac takode ima izvestan uticaj u industriji.

Napomena: vredi pomenuti da je RednaxelaFX, jedan od domacih promotora GC tehnologije u ranim godinama (u krugovima poznat kao «Veliki R»), nekada radio u Azul-u; deo materijala u ovom clanku takode se oslanja na njegove tekstove.
2.5.1 Generacijski sakuplaci
ParNew: visenitni sakuplac koji koristi kopirajuci algoritam, uglavnom radi u Young oblasti, a broj niti za prikupljanje se moze kontrolisati parametrom
-XX:ParallelGCThreads; ceo proces je STW i cesto se koristi u kombinaciji sa CMS-om.CMS: cilj mu je postizanje najkraceg vremena pauze prikupljanja; koristi «obelezi-pocisti» algoritam i prikuplja smece u 4 velika koraka, pri cemu inicijalno obelezavanje i ponovno obelezavanje izazivaju STW. Vecinom se primenjuje na serverima veb-sajtova ili B/S sistema; u JDK-u 9 oznacen je kao zastareo, a u JDK-u 14 uklonjen — detalje vidite u JEP 363.
2.5.2 Regionalni sakuplaci
G1: serverski sakuplac smeca, namenjen viseprocesorskim okruzenjima i velikim kolicinama memorije; pored visoke propusnosti, nastoji da zadovolji zahteve za vreme pauze prikupljanja smeca.
ZGC: sakuplac niskog kasnjenja predstavljen u JDK-u 11, pogodan za upravljanje memorijom i prikupljanje kod servisa sa velikom memorijom i niskim kasnjenjem. U SPECjbb 2015 benchmark-u, pri hipu od 128G, maksimalno vreme pauze iznosi samo 1.68 ms — daleko bolje od G1 i CMS-a.
Shenandoah: razvija ga tim iz Red Hat-a; slicno G1, to je sakuplac zasnovan na Region-u, ali ne zahteva Remember Set niti Card Table za belezenje referenci izmedju Regiona, pa vreme pauze nema nikakve veze sa velicinom hipa. Vreme pauze mu je blisko ZGC-u — u javno dostupnim benchmark-upima Shenandoah i ZGC drže pauze u milisekundama, dok CMS i G1 pri velikim hipovima dostizu desetine pa i stotinu milisekundi.
2.5.3 Uobičajeni sakupljači
Trenutno se najviše koriste CMS i G1 sakupljači; oba imaju pojam generacija, a glavna memorijska struktura je prikazana ispod:

2.5.4 Ostali sakupljači
Gore su navedeni samo uobičajeni sakupljači; pored njih ih ima još mnogo — real-time sakupljači poput Metronome, Stopless, Staccato, Chicken, Clover; konkurentni kopirajući/sabijajući sakupljači poput Sapphire, Compressor, Pauseless; te mark-compact sakupljači poput Doligez-Leroy-Conthier. Zbog ograničenja prostora nećemo ih ovde sve navoditi.
2.6 Uobičajeni alati
Da bi se dobro obavio posao, prvo treba pripremiti alate. Ovde su navedeni neki alati koje autor često koristi; slobodno ih birajte prema potrebi — svi problemi u ovom članku locirani su i analizirani pomoću ovih alata.
2.6.1 Komandna linija
Standardna terminalna klasa: jps, jinfo, jstat, jstack, jmap
Klasa integrisanih funkcija: jcmd, vjtools, arthas, greys
2.6.2 Grafičko okruženje
Jednostavni: JConsole, JVisualvm, HA, GCHisto, GCViewer
Napredni: MAT, JProfiler
Sa komandne linice preporucujemo arthas, a od grafickih okruzenja JProfiler. Pored toga postoje i onlajn platforme gceasy, heaphero, fastthread; u Meituan-u se takode cesto koristi Scalpel (interni alat za dijagnostiku JVM problema, trenutno nije otvorenog koda).
3. Ocena GC problema
Pre nego što se pozabavimo pronalaženjem i optimizacijom GC problema, moramo razjasniti da li je problem zaista neposredno izazvan GC-om, ili je neka anomalija u GC-u posledica koda aplikacije koja na kraju dovodi do problema.
3.1 Kako oceniti da li GC ima problem?
3.1.1 Određivanje kriterijuma evaluacije
Dve ključne metrike za ocenu GC-a:
Latencija (Latency): može se shvatiti i kao maksimalno vreme pauze, odnosno najduže trajanje jednog STW-a tokom prikupljanja smeća — što kraće, to bolje; do izvesne mere prihvatljiv je i veći broj pauza; glavni pravac razvoja GC tehnologije.
Propusnost (Throughput): tokom životnog veka sistema aplikacije, pošto GC niti zauzimaju CPU takt cycles trenutno dostupne Mutator-u, propusnost je procenat efektivno utrošenog vremena Mutator-a u ukupnom vremenu rada sistema. Na primer, ako sistem radi 100 min, a GC troši 1 min, propusnost sistema je 99%; sakupljači orijentisani na propusnost mogu prihvatiti i duže pauze.
Trenutno sistemi velikih internetskih kompanija uglavnom teže nižoj latenciji, izbegavajući da predugo trajanje jedne GC pauze naruši korisničko iskustvo. Merne metrike treba kombinovati sa SLA-om aplikacijskog servisa; uglavnom se ocenjuje prema sledeće dve tačke:
Uslov za „4 devetke" (99.99% vremena bez GC pauze): prosečno trajanje pojedinačne pauze
t_pausei ukupno vreme pauza u periodu moraju zadovoljiti da udeo vremena provedenog u GC bude manji od 0.01%.
Ukratko: vreme jedne pauze ne treba da premaši TP9999 aplikacijskog servisa, a propusnost GC-a ne treba da bude manja od 99.99%. Na primer, pretpostavimo da je TP9999 servisa A 80 ms, a prosečna GC pauza 30 ms — onda je najbolje da maksimalno vreme pauze tog servisa ne premaši 80 ms, a da se učestalost GC-a održava na jedno prikupljanje u više od 5 min. Ako se to ne može postići, potrebno je podešavanje ili paralelna redundansa kroz više resursa. (Možete zastati ovde i pogledati metriku gc.meantime na nivou minuta na platformi za nadzor — ako prelazi 6 ms, propusnost GC-a na jednoj mašini ne dostiže «četiri devetke».)
Napomena: pored ove dve metrike postoje i Footprint (mera veličine resursa), brzina reakcije itd.; internetski sistemi realnog vremena teže niskoj latenciji, dok mnogi ugrađeni sistemi teže manjem Footprint-u.
3.1.2 Razumevanje GC Cause-a
Kada dobijemo GC log, možemo prosto analizirati stanje GC-a; pomoću nekih alata možemo prilično intuitivno videti raspodelu Cause-a, kao na grafikonu ispod koji je nacrtan pomoću gceasy:

Kao što se vidi na gornjoj slici, jasno možemo znati koji je razlog izazvao GC i koliko vremena je potrošeno svaki put. Ali da bismo analizirali GC problem, prvo moramo razumeti GC Cause, odnosno pod kojim uslovima JVM bira da izvrši GC operaciju. Konkretnu klasifikaciju Cause-a možete pogledati u HotSpot izvornom kodu: src/share/vm/gc/shared/gcCause.hpp i src/share/vm/gc/shared/gcCause.cpp.
const char* GCCause::to_string(GCCause::Cause cause) {
switch (cause) {
case _java_lang_system_gc:
return "System.gc()";
case _full_gc_alot:
return "FullGCAlot";
case _scavenge_alot:
return "ScavengeAlot";
case _allocation_profiler:
return "Allocation Profiler";
case _jvmti_force_gc:
return "JvmtiEnv ForceGarbageCollection";
case _gc_locker:
return "GCLocker Initiated GC";
case _heap_inspection:
return "Heap Inspection Initiated GC";
case _heap_dump:
return "Heap Dump Initiated GC";
case _wb_young_gc:
return "WhiteBox Initiated Young GC";
case _wb_conc_mark:
return "WhiteBox Initiated Concurrent Mark";
case _wb_full_gc:
return "WhiteBox Initiated Full GC";
case _no_gc:
return "No GC";
case _allocation_failure:
return "Allocation Failure";
case _tenured_generation_full:
return "Tenured Generation Full";
case _metadata_GC_threshold:
return "Metadata GC Threshold";
case _metadata_GC_clear_soft_refs:
return "Metadata GC Clear Soft References";
case _cms_generation_full:
return "CMS Generation Full";
case _cms_initial_mark:
return "CMS Initial Mark";
case _cms_final_remark:
return "CMS Final Remark";
case _cms_concurrent_mark:
return "CMS Concurrent Mark";
case _old_generation_expanded_on_last_scavenge:
return "Old Generation Expanded On Last Scavenge";
case _old_generation_too_full_to_scavenge:
return "Old Generation Too Full To Scavenge";
case _adaptive_size_policy:
return "Ergonomics";
case _g1_inc_collection_pause:
return "G1 Evacuation Pause";
case _g1_humongous_allocation:
return "G1 Humongous Allocation";
case _dcmd_gc_run:
return "Diagnostic Command";
case _last_gc_cause:
return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";
default:
return "unknown GCCause";
}
ShouldNotReachHere();
}Nekoliko GC Cause-a na koje treba obratiti pažnju:
System.gc(): ručno pokrenuta GC operacija.
CMS: neke od radnji tokom izvršavanja CMS GC-a; posebno obratite pažnju na dva STW segmenta — CMS Initial Mark i CMS Final Remark.
Promotion Failure: Old oblast nema dovoljno prostora za dodelu objektima koji se iz Young oblasti unapređuju (čak i kada je ukupna dostupna memorija dovoljno velika).
Concurrent Mode Failure: tokom rada CMS GC-a, prostor rezervisan u Old oblasti nedovoljan je za dodelu novim objektima; tada sakupljač degradira, što ozbiljno utiče na GC performanse — jedan od donjih primera upravo je takav scenario.
GCLocker Initiated GC: ako nit izvršava u JNI kritičnoj sekciji baš kada je GC potrebno, GC Locker će sprečiti nastanak GC-a i sprečiti druge niti da uđu u JNI kritičnu sekciju, sve dok poslednja nit ne izađe iz kritične sekcije, kada se pokreće jedan GC.
U kom se trenutku prikupljanje pokreće pomoću ovih Cause-a možete pogledati u kodu CMS-a; ovde to nećemo razmatrati — konkretan kod nalazi se u /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp.
shouldConcurrentCollect
bool CMSCollector::shouldConcurrentCollect() {
LogTarget(Trace, gc) log;
if (_full_gc_requested) {
log.print("CMSCollector: collect because of explicit gc request (or GCLocker)");
return true;
}
FreelistLocker x(this);
// ------------------------------------------------------------------
// Print out lots of information which affects the initiation of
// a collection.
if (log.is_enabled() && stats().valid()) {
log.print("CMSCollector shouldConcurrentCollect: ");
LogStream out(log);
stats().print_on(&out);
log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full());
log.print("free=" SIZE_FORMAT, _cmsGen->free());
log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available());
log.print("promotion_rate=%g", stats().promotion_rate());
log.print("cms_allocation_rate=%g", stats().cms_allocation_rate());
log.print("occupancy=%3.7f", _cmsGen->occupancy());
log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end());
log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect());
}
// ------------------------------------------------------------------
// If the estimated time to complete a cms collection (cms_duration())
// is less than the estimated time remaining until the cms generation
// is full, start a collection.
if (!UseCMSInitiatingOccupancyOnly) {
if (stats().valid()) {
if (stats().time_until_cms_start() == 0.0) {
return true;
}
} else {
if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f",
_cmsGen->occupancy(), _bootstrap_occupancy);
return true;
}
}
}
if (_cmsGen->should_concurrent_collect()) {
log.print("CMS old gen initiated");
return true;
}
CMSHeap* heap = CMSHeap::heap();
if (heap->incremental_collection_will_fail(true /* consult_young */)) {
log.print("CMSCollector: collect because incremental collection will fail ");
return true;
}
if (MetaspaceGC::should_concurrent_collect()) {
log.print("CMSCollector: collect for metadata allocation ");
return true;
}
// CMSTriggerInterval starts a CMS cycle if enough time has passed.
if (CMSTriggerInterval >= 0) {
if (CMSTriggerInterval == 0) {
// Trigger always
return true;
}
// Check the CMS time since begin (we do not check the stats validity
// as we want to be able to trigger the first CMS cycle as well)
if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
if (stats().valid()) {
log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
stats().cms_time_since_begin());
} else {
log.print("CMSCollector: collect because of trigger interval (first collection)");
}
return true;
}
}
return false;
}3.2 Ocena da li je problem izazvan GC-om?
Da li je nešto posledica (pojava) ili uzrok — tokom rešavanja jednog GC problema, kako oceniti da li je kvar izazvan GC-om ili je sam sistem izazvao GC problem? Nastavljamo sa primerom pomenutim na početku članka: «Četiri simptoma — povećanje GC vremena, povećanje broja blokiranih niti, povećanje broja sporih upita i visoko opterećenje CPU-a — kako oceniti koji je koreni uzrok?». Autor je ovde, na osnovu svog iskustva, grubo prikupio četiri metoda ocenjivanja kao referencu:
Analiza vremenskog slede: događaji koji se dese ranije verovatnije su koreni uzrok; sredstvima nadzora analiziraju se vremenske tačke anomalija svake metrike i rekonstruiše vremenska linija događaja. Na primer, ako se prvo uoči visoko opterećenje CPU-a (uz dovoljan vremenski razmak), lanac uticaja problema mogao bi biti: visoko opterećenje CPU-a -> povećanje sporih upita -> povećanje GC vremena -> povećanje blokiranih niti -> porast RT-a.
Verovatnocna analiza: koristi statisticku verovatnocu i kombinuje je sa iskustvom iz istorijskih problema, analizirajuci po tipu od skorosnjeg ka starijem. Na primer, ako su u proslosti bili cesce problemi sa sporim upitima, lanac uticaja mogao bi biti: povecanje sporih upita -> povecanje GC vremena -> visoko opterecenje CPU-a -> povecanje blokiranih niti -> porast RT-a.
Eksperimentalna analiza: simulacijom kvara i slicnim nacinima se rekonstruise mesto problema, pokrecu se neki od uslova (jedan ili vise) i posmatra da li ce problem nastati. Na primer, ako se problem pojavi vec pri pokretanju samo blokade niti, lanac uticaja mogao bi biti: povecanje blokiranih niti -> visoko opterecenje CPU-a -> povecanje sporih upita -> povecanje GC vremena -> porast RT-a.
Analiza protivu-dokaza: za neku od pojava radi se analiza protivu-dokaza, odnosno ocenjuje se da li postoji korelacija izmedju nastanka te pojave i rezultata. Na primer, ako iz ugla celog klastera uocimo da su na nekim cvorovima i spori upiti i CPU normalni, ali je problem ipak nastao, lanac uticaja mogao bi biti: povecanje GC vremena -> povecanje blokiranih niti -> porast RT-a.
Za različite korene uzroke, naknadni metodi analize potpuno se razlikuju. Ako je opterećenje CPU-a visoko, možda treba pogledati flame graph radi identifikacije hotspot-ova; ako raste broj sporih upita, možda treba proveriti stanje baze; ako problem izaziva blokada niti, možda treba proveriti situaciju sa konkurencijom za brave. Na kraju, ako se dokaže da sve pojave nemaju problema, onda GC zaista možda ima problem i može se nastaviti analiza GC problema.
3.3 Vodič za klasifikaciju problema
3.3.1 Tipovi Mutator-a
Prema grafikonu odnosa vremena preživljavanja objekata, tipovi Mutator-a uglavnom se dele u dve vrste; slično se pominje i u slaboj generacijskoj hipotezi. Na donjoj slici «Survival Time» označava vreme preživljavanja objekta, a «Rate» odnos dodele objekata:
IO-interaktivni: većina današnjih servisa na internetu pripada ovom tipu — npr. distribuirani RPC, MQ, HTTP gateway servisi; memorijski zahtevi nisu veliki, a većina objekata ugine unutar TP9999 vremena — što veća Young oblast, to bolje.
MEM-računarski: uglavnom distribuirano obrada podataka Hadoop, distribuirano skladištenje HBase, Cassandra, interni distribuirani keš itd.; memorijski zahtevi su veliki, objekti dugo žive — što veća Old oblast, to bolje.
Naravno, pored te dve, postoje i scenariji između njih; ovaj članak uglavnom razmatra prvi slučaj. Distribucija Survival Time-a objekata ima veoma važan vodeći značaj za postavljanje GC parametara — na primer, na osnovu donje slike može se prosto izračunati granica generacija.

3.3.2 Klasifikacija GC problema
Autor je odabrao devet različitih tipova GC problema koji pokrivaju većinu scenarija; ako imate bolji scenario, slobodno ga navedite u komentarima.
Unexpected GC: GC koji se desi neočekivano, a zapravo ne bi ni trebalo da se desi; možemo ga izbeći određenim sredstvima.
Space Shock: problem prostornog osciliranja; vidi «Scenario 1: Prostorno osciliranje izazvano dinamičkim proširenjem».
Explicit GC: problem eksplicitnog GC-a; vidi «Scenario 2: Zadržati ili ne eksplicitni GC».
Partial GC: GC koji obavlja delimično prikupljanje, prikupljajući samo neke generacije/regione.
CMS: učestao Old GC; vidi «Scenario 5: Učestali CMS Old GC».
CMS: Old GC nije učestao, ali jedno prikupljanje dugo traje; vidi «Scenario 6: Dugo trajanje jednog CMS Old GC-a».
ParNew: učestao Young GC; vidi «Scenario 4: Prevelika promocija».
Young GC: prikupljanje Young oblasti u okviru generacijskog prikupljanja, poznato i kao Minor GC.
Old GC: prikupljanje Old oblasti u okviru generacijskog prikupljanja, poznato i kao Major GC; neki ga zovu i Full GC, mada ta oznaka nije standardna — Full GC je tek kada CMS ima Foreground GC, a parametar CMSScavengeBeforeRemark samo pokreće jedan Young GC pre Remark-a.
Full GC: GC potpunog prikupljanja, prikuplja ceo hip; STW vreme je prilično dugo, a uticaj veliki kada se desi; može se zvati i Major GC; vidi «Scenario 7: Memorijska fragmentacija i degradacija sakupljača».
MetaSpace: problem izazvan prikupljanjem u MetaSpace-u; vidi «Scenario 3: OOM u MetaSpace oblasti».
Direct Memory: problem izazvan prikupljanjem direktne memorije (poznate i kao memorija van hipa); vidi «Scenario 8: OOM van hipa».
JNI: problem izazvan lokalnim Native metodama; vidi «Scenario 9: GC problem izazvan JNI-jem».
3.3.3 Težina pronalaženja uzroka
Težina rešavanja problema obrnuto je srazmerna njegovoj učestalosti — u većini slučajeva razne pretraživače možemo koristiti da pronađemo slične probleme i iste metode isprobati u rešavanju. Kada se problem ne može pronaći na raznim sajtovima, moguće su dve situacije: ili to nije problem, ili je u pitanju problem koji je duboko skriven, pa tada treba ući na nivo izvornog koda radi otklanjanja. Za GC scenarije ispod, težina pronalaženja uzroka raste odozgo nadole.
4. Analiza i rešenja uobičajenih scenarija
4.1 Scenario 1: Prostorno osciliranje izazvano dinamičkim proširenjem
4.1.1 Pojava
Servis pri pokretanju ima više GC-a; iako preostaje mnogo maksimalnog prostora, GC se i dalje dešava. To možemo uočiti posmatranjem GC loga ili, putem alata za nadzor, praćenjem promena prostora hipa. GC Cause je obično Allocation Failure, a u GC logu se vidi da posle jednog GC-a veličine pojedinih prostora unutar hipa bivaju prilagođene, kao na slici ispod:

4.1.2 Uzrok
U parametrima JVM-a, -Xms i -Xmx su postavljeni na razlicite vrednosti; pri inicijalizaciji se inicijalno rezervise samo prostor velicine -Xms za cuvanje podataka, a kad god prostora ponestane, trazi se jos od operativnog sistema — tada GC nuzno mora da se izvrsi. Konkretno, nova velicina prostora izracunava se metodom ConcurrentMarkSweepGeneration::compute_new_size():
ConcurrentMarkSweepGeneration::compute_new_size()
void ConcurrentMarkSweepGeneration::compute_new_size() {
assert_locked_or_safepoint(Heap_lock);
// If incremental collection failed, we just want to expand
// to the limit.
if (incremental_collection_failed()) {
clear_incremental_collection_failed();
grow_to_reserved();
return;
}
// The heap has been compacted but not reset yet.
// Any metric such as free() or used() will be incorrect.
CardGeneration::compute_new_size();
// Reset again after a possible resizing
if (did_compact()) {
cmsSpace()->reset_after_compaction();
}
}Pored toga, kada preostane mnogo prostora vrsi se i smanjenje. JVM preko -XX:MinHeapFreeRatio i -XX:MaxHeapFreeRatio kontrolise odnos prosirenja i smanjenja; podesavanjem te dve vrednosti moze se kontrolisati i trenutac sirenja/smanjenja. Na primer, prosirenje se obavlja pomoci GenCollectedHeap::expand_heap_and_allocate(), ciji je kod prikazan ispod:
GenCollectedHeap::expand_heap_and_allocate()
HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool is_tlab) {
HeapWord* result = NULL;
if (_old_gen->should_allocate(size, is_tlab)) {
result = _old_gen->expand_and_allocate(size, is_tlab);
}
if (result == NULL) {
if (_young_gen->should_allocate(size, is_tlab)) {
result = _young_gen->expand_and_allocate(size, is_tlab);
}
}
assert(result == NULL || is_in_reserved(result), "result not in heap");
return result;
}Celokupni model širenja/smanjenja može se razumeti preko ove slike: kada committed veličina prostora premaši niski/visoki nivo vode, capacity se takođe prilagođava:
max (maksimum)
──────────── gornja granica
┬ vodeni talas (visoki vodostaj) — pokreće GC
┬
┬ niski vodostaj — ciljna granica nakon GC
min (minimum)GC se pokreće kada zauzeće pređe visoki vodostaj; cilj je spustiti zauzeće ispod niskog.
4.1.3 Strategija
Lociranje: posmatrajte da li je committed odnos Old/MetaSpace oblasti u trenutku okidanja CMS GC-a neka fiksna vrednost, ili — kao što je pomenuto — posmatrajte ukupnu iskorišćenost memorije.
Resenje: nastojte parametre velicine prostora koji se javljaju u parovima postaviti na fiksne vrednosti, kao sto su -Xms i -Xmx, -XX:MaxNewSize i -XX:NewSize, -XX:MetaSpaceSize i -XX:MaxMetaSpaceSize itd.
4.1.4 Rezime
Uopsteno, treba da obezbedimo stabilan hip Java virtuelne masine — uverimo se da su -Xms i -Xmx postavljeni na istu vrednost (odnosno da su inicijalna i maksimalna vrednost iste), cime dobijamo stabilan hip; slicno vazi i za MetaSpace oblast. Medjutim, kada se ne tezi kratkom vremenu pauze, osciliranje prostora moze biti i korisno — moze se dinamicki siriti i suzavati radi ustede prostora, npr. kod Java aplikacija kao bogati klijenti.
Iako je ovaj problem osnovni, verovatnoća njegovog nastanka nije mala, naročito tamo gde standardi nisu dobro razvijeni.
4.2 Scenario 2: Zadržati ili ne eksplicitni GC
4.2.1 Pojava
Osim sto prosirenje i smanjenje mogu pokrenuti CMS GC, postoji jos nekoliko okidaca: Old oblast dostigne prag prikupljanja, nedostatak prostora u MetaSpace-u, neuspeh promocije iz Young oblasti, neuspeh garancije za velike objekte itd. Ako se GC pokrene a nijedan od tih uslova nije ispunjen? Verovatno je u kodu rucno pozvan metod System.gc; to mozete potvrditi pregledom GC Cause u GC logu. Da li ovakav GC zaista predstavlja problem? Pregledom izvora na internetu, neki kazu da se moze dodati parametar -XX:+DisableExplicitGC da bi se izbegao, dok drugi kazu da se ne sme dodati jer utice na prikupljanje Native Memory. Najpre zakljucak: autor ovde preporucuje zadrzavanje System.gc — zasto? Hajde da analiziramo.
4.2.2 Uzrok
U izvornom kodu HotSpot-a se vidi da se, nakon dodavanja parametra -XX:+DisableExplicitGC, ovaj metod pretvara u prazan metod; ako parametar nije dodat, poziva se Universe::heap()::collect. Pracenjem tog metoda otkrivamo da System.gc izaziva STW Full GC koji prikuplja ceo hip.
DisableExplicitGC
JVM_ENTRY_NO_ENV(void, JVM_GC(void))
JVMWrapper("JVM_GC");
if (!DisableExplicitGC) {
Universe::heap()->collect(GCCause::_java_lang_system_gc);
}
JVM_ENDGenCollectedHeap::collect()
void GenCollectedHeap::collect(GCCause::Cause cause) {
if (cause == GCCause::_wb_young_gc) {
// Young collection for the WhiteBox API.
collect(cause, YoungGen);
} else {
#ifdef ASSERT
if (cause == GCCause::_scavenge_alot) {
// Young collection only.
collect(cause, YoungGen);
} else {
// Stop-the-world full collection.
collect(cause, OldGen);
}
#else
// Stop-the-world full collection.
collect(cause, OldGen);
#endif
}
}Zadržati System.gc
Ovde dodajemo jednu činjenicu: CMS GC se deli na Background i Foreground režim. Prvi je konkurentno prikupljanje u uobičajenom smislu i ne utiče na normalan rad poslovnih niti, dok se Foreground Collector znatno razlikuje — on obavlja jedan kompresioni GC. Taj kompresioni GC koristi isti LISP2 algoritam kao Serial Old GC, odnosno Mark-Compact za Full GC, i obično se naziva MSC (Mark-Sweep-Compact); obuhvata Young i Old oblast Java hipa kao i MetaSpace. Iz gorenjeg poglavlja o algoritmima znamo da je cena compact-a ogromna, pa upotreba Foreground Collector-a donosi veoma dugačak STW. Ako se u aplikaciji System.gc često poziva, to je veoma opasno.
Ukloniti System.gc
Ako se onemoguci, pojavice se drugi problem — curenje memorije. Tada moramo pomenuti DirectByteBuffer, koji ima osobine poput zero-copy-ja, koriste ga razni NIO okviri poput Netty-ja, i pritom zauzima memoriju van hipa. Memorijom hipa upravlja sam JVM, dok memoriju van hipa mora rucno oslobadjati; DirectByteBuffer nema Finalizer, a ciscenje njegovog Native Memory-a obavlja se automatski preko sun.misc.Cleaner — alatke za ciscenje zasnovane na PhantomReference, lakse od obicnog Finalizer-a.
Tokom dodele prostora za DirectByteBuffer izričito se poziva System.gc, u nadi da će Full GC primorati već beskorisne DirectByteBuffer objekte da oslobode memoriju van hipa koja je s njima povezana. Implementacija je prikazana ispod:
reserveMemory
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size) {
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
if (size <= maxMemory - reservedMemory) {
reservedMemory += size;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (reservedMemory + size > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
}
}HotSpot VM obavlja Reference Processing nad objektima u Old oblasti samo tokom Old GC-a, a tokom Young GC-a ga obavlja samo nad objektima u Young oblasti. DirectByteBuffer objekti u Young oblasti obradjuju se tokom Young GC-a — drugim recima, CMS GC obavlja Reference Processing nad Old oblasti i time moze pokrenuti Cleaner nad vec mrtvim DirectByteBuffer objektima. Medjutim, ako se duze vreme nije radio GC ili se radio samo Young GC, Cleaner se nece pokrenuti u Old oblasti, pa Native Memory povezan sa DirectByteBuffer objektima koji su vec mrtvi ali unapredjeni u Old mozda nece biti pravovremeno oslobodjen. Te osobine implementacije cine da oslobadjanje DirectByteMemory oslanja na pokretanje GC-a putem System.gc. Ako je ukljuceno -XX:+DisableExplicitGC, ciscenje mozda nece biti pravovremeno zavrseno, pa moze doci do OOM u Direct Memory-ju.
4.2.3 Strategija
Iz gornje analize se vidi da i zadrzavanje i uklanjanje nose izvesne rizike, ali posto danasnja RPC komunikacija na internetu velikim delom koristi NIO, autor ovde preporucuje zadrzavanje. Pored toga, JVM pruza parametre -XX:+ExplicitGCInvokesConcurrent i -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses kojima se tip okidaca System.gc prebacuje iz Foreground u Background rezim; i Background obavlja Reference Processing, cime se znatno smanjuje STW rezija, a pritom ne nastaje NIO Direct Memory OOM.
4.2.4 Rezime
Ne samo CMS — u G1 ili ZGC-u, ukljucivanjem rezima ExplicitGCInvokesConcurrent takode se koristi visoko-perfomantno konkurentno prikupljanje. Ipak, preporucuje se i dobro definisati kodne standarde i urediti koriscenje System.gc.
P.S. HotSpot System.gc tretira posebno — najvažnije se vidi u tome da li jedan System.gc, kao i običan GC, ažurira statističke/podatke praga GC-a. Mnogi GC algoritmi u HotSpot-u imaju adaptivne funkcije koje prema ranijoj efikasnosti prikupljanja određuju parametre za naredni GC, ali System.gc podrazumevano ne ažurira te statistike, čime se izbegava ometanje tih adaptivnih funkcija prinudnim GC-jem korisnika (vidi parametar -XX:+UseAdaptiveSizePolicyWithSystemGC, podrazumevano false).
4.3 Scenario 3: OOM u MetaSpace oblasti
4.3.1 Pojava
Nakon pokretanja JVM-a, ili od nekog trenutka, iskorišćenost MetaSpace-a neprestano raste, a ni jedan GC je ne može osloboditi; čak ni povećanje prostora MetaSpace-a ne rešava problem trajno.
4.3.2 Uzrok
Pre nego sto razmotrimo zasto nastaje OOM, pogledajmo koje se podatke cuvaju u ovoj oblasti. Pre Jave 7, bazen konstanti stringova nalazio se u Perm oblasti, i svi intern-ovani String-ovi cuvali su se ovde; posto String.intern nije kontrolisan, vrednost -XX:MaxPermSize se tesko podesava, pa se cesto javljao izuzetak java.lang.OutOfMemoryError: PermGen space. Zato su od Jave 7 bazen konstanti i drugi literali (Literal), staticke promenljive klase (Class Static), simbolicke reference (Symbols Reference) itd. premesteni u Heap. A od Jave 8 uklonjen je i PermGen, a zamenio ga je MetaSpace.
Na najnizem nivou, JVM preko mmap interfejsa trazi od operativnog sistema memorijsko mapiranje, po 2MB prostora u svakom zahtevu. Ovo je mapiranje virtuelne memorije — ne trosi stvarno 2MB glavne memorije odmah; memorija se stvarno trosi tek kasnije, pri upotrebi. Ta memorija se smesta u povezanu listu VirtualSpaceList, kao jedan njen cvor.
Na visem nivou, MetaSpace se uglavnom sastoji od dva velika dela: Klass Metaspace i NoKlass Metaspace.
Klass MetaSpace: sluzi za cuvanje Klass-a, odnosno runtime strukture podataka Class fajla u JVM-u. Ovaj deo se podrazumevano nalazi u Compressed Class Pointer Space-u, sto je jedna neprekidna memorijska oblast, neposredno uz Heap. Compressed Class Pointer Space nije obavezan — ako je postavljeno
-XX:-UseCompressedClassPointers, ili je-Xmxveci od 32 G, ove memorije nece biti; u tom slucaju se Klass cuva u NoKlass Metaspace-u.NoKlass MetaSpace: namenjen cuvanju drugog sadrzaja vezanog za Klass, poput Method, ConstantPool itd.; moze se sastojati od vise nepovezanih memorijskih blokova. Iako se zove NoKlass Metaspace, u njemu se zapravo moze cuvati i sadrzaj Klass-a, cemu smo vec doprineli gore.
Konkretne definicije se mogu naci u izvornom kodu shared/vm/memory/metaspace.hpp:
MetaSpace
class Metaspace : public AllStatic {
friend class MetaspaceShared;
public:
enum MetadataType {
ClassType,
NonClassType,
MetadataTypeCount
};
enum MetaspaceType {
ZeroMetaspaceType = 0,
StandardMetaspaceType = ZeroMetaspaceType,
BootMetaspaceType = StandardMetaspaceType + 1,
AnonymousMetaspaceType = BootMetaspaceType + 1,
ReflectionMetaspaceType = AnonymousMetaspaceType + 1,
MetaspaceTypeCount
};
private:
// Align up the word size to the allocation word size
static size_t align_word_size_up(size_t);
// Aligned size of the metaspace.
static size_t _compressed_class_space_size;
static size_t compressed_class_space_size() {
return _compressed_class_space_size;
}
static void set_compressed_class_space_size(size_t size) {
_compressed_class_space_size = size;
}
static size_t _first_chunk_word_size;
static size_t _first_class_chunk_word_size;
static size_t _commit_alignment;
static size_t _reserve_alignment;
DEBUG_ONLY(static bool _frozen;)
// Virtual Space lists for both classes and other metadata
static metaspace::VirtualSpaceList* _space_list;
static metaspace::VirtualSpaceList* _class_space_list;
static metaspace::ChunkManager* _chunk_manager_metadata;
static metaspace::ChunkManager* _chunk_manager_class;
static const MetaspaceTracer* _tracer;
}Zašto objekti u MetaSpace-u ne mogu da se oslobode — pogledajmo dve tačke:
Upravljanje memorijom MetaSpace-a: zivotni vek klase i njenih metapodataka isti je kao i njen odgovarajuci classloader; sve dok je classloader klase ziv, i metapodaci klase u Metaspace-u su zivi i ne mogu biti prikupljeni. Svaki loader ima poseban prostor za skladistenje, a ClassLoaderMetaspace upravlja pokazivacem na SpaceManager*, medjusobno izolovano.
Elasticno sirenje MetaSpace-a: posto prostor MetaSpace-a nije zajedno sa Heap-om, ovaj prostor se ne mora postavljati ili se moze postaviti posebno. Da bi se izbeglo da MetaSpace iscrpi memoriju VM-a, obicno se postavlja MaxMetaSpaceSize. Tokom rada, ako je stvarna velicina manja od te vrednosti, JVM preko parametara
-XX:MinMetaspaceFreeRatioi-XX:MaxMetaspaceFreeRatiodinamicki kontrolise velicinu celog MetaSpace-a; konkretan prikaz se vidi u metodiMetaSpaceGC::compute_new_size()(kod ispod), koja se poziva tokom GC-a u nekoliko sakuplaca poput CMSCollector-a i G1CollectorHeap-a. U njoj se na osnovuused_after_gc,MinMetaspaceFreeRatioiMaxMetaspaceFreeRatioizracunava nova vrednost_capacity_until_GC(nivo vode). Zatim se, prema stvarnoj vrednosti_capacity_until_GC, pomociMetaspaceGC::inc_capacity_until_GC()iMetaspaceGC::dec_capacity_until_GC()obavlja expand ili shrink; taj proces se moze razumeti i preko modela sirenja/smanjenja iz scenarija 1.
MetaspaceGC::compute_new_size()
void MetaspaceGC::compute_new_size() {
assert(_shrink_factor <= 100, "invalid shrink factor");
uint current_shrink_factor = _shrink_factor;
_shrink_factor = 0;
const size_t used_after_gc = MetaspaceUtils::committed_bytes();
const size_t capacity_until_GC = MetaspaceGC::capacity_until_GC();
const double minimum_free_percentage = MinMetaspaceFreeRatio / 100.0;
const double maximum_used_percentage = 1.0 - minimum_free_percentage;
const double min_tmp = used_after_gc / maximum_used_percentage;
size_t minimum_desired_capacity =
(size_t)MIN2(min_tmp, double(max_uintx));
// Don't shrink less than the initial generation size
minimum_desired_capacity = MAX2(minimum_desired_capacity,
MetaspaceSize);
log_trace(gc, metaspace)("MetaspaceGC::compute_new_size: ");
log_trace(gc, metaspace)(" minimum_free_percentage: %6.2f maximum_used_percentage: %6.2f",
minimum_free_percentage, maximum_used_percentage);
log_trace(gc, metaspace)(" used_after_gc : %6.1fKB", used_after_gc / (double) K);
size_t shrink_bytes = 0;
if (capacity_until_GC < minimum_desired_capacity) {
// If we have less capacity below the metaspace HWM, then
// increment the HWM.
size_t expand_bytes = minimum_desired_capacity - capacity_until_GC;
expand_bytes = align_up(expand_bytes, Metaspace::commit_alignment());
// Don't expand unless it's significant
if (expand_bytes >= MinMetaspaceExpansion) {
size_t new_capacity_until_GC = 0;
bool succeeded = MetaspaceGC::inc_capacity_until_GC(expand_bytes, &new_capacity_until_GC);
assert(succeeded, "Should always succesfully increment HWM when at safepoint");
Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
new_capacity_until_GC,
MetaspaceGCThresholdUpdater::ComputeNewSize);
log_trace(gc, metaspace)(" expanding: minimum_desired_capacity: %6.1fKB expand_bytes: %6.1fKB MinMetaspaceExpansion: %6.1fKB new metaspace HWM: %6.1fKB",
minimum_desired_capacity / (double) K,
expand_bytes / (double) K,
MinMetaspaceExpansion / (double) K,
new_capacity_until_GC / (double) K);
}
return;
}
// No expansion, now see if we want to shrink
// We would never want to shrink more than this
assert(capacity_until_GC >= minimum_desired_capacity,
SIZE_FORMAT " >= " SIZE_FORMAT,
capacity_until_GC, minimum_desired_capacity);
size_t max_shrink_bytes = capacity_until_GC - minimum_desired_capacity;
// Should shrinking be considered?
if (MaxMetaspaceFreeRatio < 100) {
const double maximum_free_percentage = MaxMetaspaceFreeRatio / 100.0;
const double minimum_used_percentage = 1.0 - maximum_free_percentage;
const double max_tmp = used_after_gc / minimum_used_percentage;
size_t maximum_desired_capacity = (size_t)MIN2(max_tmp, double(max_uintx));
maximum_desired_capacity = MAX2(maximum_desired_capacity,
MetaspaceSize);
log_trace(gc, metaspace)(" maximum_free_percentage: %6.2f minimum_used_percentage: %6.2f",
maximum_free_percentage, minimum_used_percentage);
log_trace(gc, metaspace)(" minimum_desired_capacity: %6.1fKB maximum_desired_capacity: %6.1fKB",
minimum_desired_capacity / (double) K, maximum_desired_capacity / (double) K);
assert(minimum_desired_capacity <= maximum_desired_capacity,
"sanity check");
if (capacity_until_GC > maximum_desired_capacity) {
// Capacity too large, compute shrinking size
shrink_bytes = capacity_until_GC - maximum_desired_capacity;
shrink_bytes = shrink_bytes / 100 * current_shrink_factor;
shrink_bytes = align_down(shrink_bytes, Metaspace::commit_alignment());
assert(shrink_bytes <= max_shrink_bytes,
"invalid shrink size " SIZE_FORMAT " not <= " SIZE_FORMAT,
shrink_bytes, max_shrink_bytes);
if (current_shrink_factor == 0) {
_shrink_factor = 10;
} else {
_shrink_factor = MIN2(current_shrink_factor * 4, (uint) 100);
}
log_trace(gc, metaspace)(" shrinking: initThreshold: %.1fK maximum_desired_capacity: %.1fK",
MetaspaceSize / (double) K, maximum_desired_capacity / (double) K);
log_trace(gc, metaspace)(" shrink_bytes: %.1fK current_shrink_factor: %d new shrink factor: %d MinMetaspaceExpansion: %.1fK",
shrink_bytes / (double) K, current_shrink_factor, _shrink_factor, MinMetaspaceExpansion / (double) K);
}
}
// Don't shrink unless it's significant
if (shrink_bytes >= MinMetaspaceExpansion &&
((capacity_until_GC - shrink_bytes) >= MetaspaceSize)) {
size_t new_capacity_until_GC = MetaspaceGC::dec_capacity_until_GC(shrink_bytes);
Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
new_capacity_until_GC,
MetaspaceGCThresholdUpdater::ComputeNewSize);
}
}Iz scenarija 1 znamo da, radi izbegavanja dodatnog GC troska koji donosi elasticno sirenje, vrednosti -XX:MetaSpaceSize i -XX:MaxMetaSpaceSize postavljamo na fiksne. Medjutim, to takode znaci da pri nastasku prostora ne moze da se prosiri, pa se GC cesto pokrece i na kraju nastupa OOM. Dakle, kljucni uzrok jeste taj sto ClassLoader neprestano ucitava nove Class-ove u memoriju; takvi problemi obicno se javljaju pri dinamickom ucitavanju klasa i slicnim situacijama.
4.3.3 Strategija
Kada se grubo razume uzrok, lociranje i resavanje su jednostavni: moze se napraviti dump snimka, a zatim u JProfiler-u ili MAT-u posmatrati Histogram klasa, ili se moze locirati neposredno komandom — pokrenite jcmd nekoliko puta za Histogram i pogledajte pod kojim paketom je Class najvise porastao. Ponekad treba kombinovano posmatrati i metrike poput InstBytes, KlassBytes, Bytecodes, MethodAll. Na donjoj slici autor je pomoci jcmd-a otkrio problem sa Orika-om.
jcmd <PID> GC.class_stats|awk '{print$13}'|sed 's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1
Ako se ne moze locirati iz celokupnog ugla, mogu se dodati parametri -XX:+TraceClassLoading i -XX:+TraceClassUnLoading radi posmatranja detalja o ucitavanju i uklanjanju klasa.
4.3.4 Rezime
Razumevanje principa je prilicno slozeno, ali lociranje i resavanje problema su jednostavniji. Mesta koja cesto stvaraju probleme su classMap iz Orika-e, ASMSerializer iz JSON-a, dinamicko ucitavanje klasa u Groovy-ju itd. — uglavnom se svode na refleksiju, bytecode enhancement pomoci Javassist-a, CGLIB dinamicki proxy-ji, OSGi prilagodjene classloadere i slicne tehnike. Pored toga, na vreme dodajte nadzor stepena iskoriscenosti MetaSpace-a, kako biste ranije uocili promene metrike i resili problem.
**4.4 Scenario 4: Prevelika promocija * **
4.4.1 Pojava
Ovaj scenario se uglavnom desava kod generacijskih sakuplaca; strucni termin je «Premature Promotion». 90% objekata zivi kratko; tek nakon nekoliko GC ciklusa u Young oblasti unapredjuju se u Old oblast. Sa svakim GC-om, GC Age objekta se uvecava za 1, a maksimum se kontrolise preko -XX:MaxTenuringThreshold.
Prevelika promocija obicno ne utice neposredno na GC; uvek je prati plutajuce smece (floating garbage), neuspeh garancije za velike objekte itd., ali se ti problemi ne javljaju odmah. Mozemo posmatrati sledece pojave da bismo ocenili da li je nastupila prevelika promocija.
Brzina dodele priblizna je brzini promocije, a uzrast promocije objekata je mali.
U GC logu se pojave poruke poput «Desired survivor size 107347968 bytes, new threshold 1(max 6)», sto ukazuje da je dovoljan jedan GC da bi objekat zavrsio u Old oblasti.
Full GC je prilicno ucestao, a posle jednog GC-a odnos promena u Old oblasti je veoma veliki.
Na primer, prag prikupljanja Old oblasti je 80%, a posle jednog GC-a pada na 10% — to znaci da 70% objekata u Old oblasti zapravo zivi veoma kratko. Velicina Old oblasti se posle svakog GC-a smanjuje sa 2.1G na 300M:
Old pre Full GC: ████████████████████████ 2.1 G
Old posle Full GC: ███ 300 M (ceo Heap = 4 G)To znaci da je prikupljeno 1.8G smeca, a ostaje samo 300M aktivnih objekata. Ceo Heap je trenutno 4G, pa aktivni objekti cine manje od desetine.
Opasnosti prevelike promocije:
Young GC je ucestao, ukupna propusnost opada.
Full GC je ucestao, moguce su duge pauze.
4.4.2 Uzrok
Glavni uzroci su ove dve tačke:
Young/Eden oblast suvise mala: neposredna posledica je da se Eden brze napuni, pa objekti koji su trebali biti prikupljeni ucestvuju u GC-u i bivaju unapredjeni. Young GC koristi kopirajuci algoritam; iz osnovnog dela znamo da copying traje znatno duze od mark-a, odnosno da vreme Young GC-a u sustini jeste vreme kopiranja (osim kada CMS ima problem sa skeniranjem Card Table ili G1 sa Remember Set-om). Objekti koji nisu stigli da budu prikupljeni povecavaju cenu prikupljanja, pa vreme Young GC-a raste, a kako se prostor ne moze brzo osloboditi, raste i broj Young GC-a.
Brzina dodele prevelika: posmatrajte brzinu dodele Mutator-a pre i posle problema; ako postoje izrazeni skokovi, pokusajte posmatrati mrezni saobracaj, logove sporih upita kod storage middlewere itd., da vidite da li je velika kolicina podataka ucitana u memoriju.
Istovremeno, objekti koje GC ne moze da prikupi donose jos jedan problem — pokrecu dinamicko izracunavanje uzrasta: JVM preko parametra -XX:MaxTenuringThreshold kontrolise uzrast promocije; posle svakog GC-a uzrast se uvecava za jedan, a dostizanjem maksimalnog uzrasta objekat moze uci u Old oblast. Maksimum je 15 (jer JVM koristi 4 bita za predstavljanje uzrasta objekta). Postavljanjem fiksne vrednosti MaxTenuringThreshold kao uslova promocije:
Ako je MaxTenuringThreshold postavljen preveliko, objekti koji su trebali biti unapredjeni ostaju u Survivor oblasti dok se Survivor ne preliva; cim dodje do prelivanja, objekti iz Eden + Survivor vise se ne unapredjuju u Old oblast na osnovu uzrasta, cime mehanizam starenja objekata prestaje da vazi.
Ako je MaxTenuringThreshold postavljen premalo, dolazi do prevelike promocije — objekti se ne prikupljaju dovoljno u Young oblasti, veliki broj kratkozivecih objekata unapredjuje se u Old oblast, prostor Old oblasti brzo raste, izazivajuci ucestance Major GC-e; generacijsko prikupljanje gubi smisao i ozbiljno narusava GC performanse.
Ista aplikacija se u razlicito vreme ponasa drugacije; izvrsenje posebnih zadataka ili promena sastava saobracaja dovode do fluktuacija u distribuciji zivotnog veka objekata. Fiksno postavljen prag, posto ne moze dinamicki da se prilagodi promenama, izaziva gore navedene probleme, pa HotSpot koristi dinamicko izracunavanje za podesavanje praga promocije.
Konkretno dinamicko izracunavanje moze se videti u HotSpot izvornom kodu, u metodi compute_tenuring_threshold fajla /src/hotspot/share/gc/shared/ageTable.cpp:
compute_tenuring_threshold
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//TargetSurvivorRatio je podrazumevano 50, znaci: nakon prikupljanja zelimo da stepen zauzeca survivor oblasti dostigne ovaj odnos
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
assert(sizes[0] == 0, "no objects with age zero should be recorded");
while (age < table_size) {//table_size=16
total += sizes[age];
//ako nakon dodavanja velicine svih objekata tog uzrasta stepen zauzeca > ocekivane velicine, postavlja se age kao novi prag za promociju
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
if (PrintTenuringDistribution || UsePerfData) {
//stampa ocekivanu velicinu survivor-a, novi izracunati prag i postavljeni maksimalni prag
if (PrintTenuringDistribution) {
gclog_or_tty->cr();
gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)",
desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold);
}
total = 0;
age = 1;
while (age < table_size) {
total += sizes[age];
if (sizes[age] > 0) {
if (PrintTenuringDistribution) {
gclog_or_tty->print_cr("- age %3u: " SIZE_FORMAT_W(10) " bytes, " SIZE_FORMAT_W(10) " total",
age, sizes[age]*oopSize, total*oopSize);
}
}
if (UsePerfData) {
_perf_sizes[age]->set_value(sizes[age]*oopSize);
}
age++;
}
if (UsePerfData) {
SharedHeap* sh = SharedHeap::heap();
CollectorPolicy* policy = sh->collector_policy();
GCPolicyCounters* gc_counters = policy->counters();
gc_counters->tenuring_threshold()->set_value(result);
gc_counters->desired_survivor_size()->set_value(
desired_survivor_size*oopSize);
}
}
return result;
}Vidi se da HotSpot, obilazeci sve objekte, pocinje da sabira prostor koji zauzimaju svi objekti uzrasta 0; kada se posle dodavanja prostora svih objekata uzrasta n primeni uslovna vrednost Survivor oblasti (TargetSurvivorRatio / 100, podrazumevano 50) za ocenu, a ako je veca od te vrednosti, petlja se zavrsava. Zatim se n poredi sa MaxTenuringThreshold: ako je n manje, prag je n; ako je n vece, prag se moze postaviti samo na maksimum MaxTenuringThreshold. Nakon okidanja dinamickog uzrasta vise objekata ulazi u Old oblast, sto dovodi do rasipanja resursa.
4.4.3 Strategija
Kada znamo uzrok problema, imamo i smer resenja. Ako je Young/Eden oblast suvise mala, mozemo, pri nepromenjenoj ukupnoj memoriji Heap-a, umerno povecati Young oblast. Kako konkretno? Uopsteno, velicina Old-a treba da bude oko 2-3 puta veca od aktivnih objekata, a s obzirom na plutajuce smece najbolje oko 3 puta; ostatak se moze dodeliti Young oblasti.
Za jednu tipicnu optimizaciju prevelike promocije autora: prvobitna konfiguracija bila je Young 1.2G + Old 2.8G; posmatranjem CMS GC uoceno je da je aktivnih objekata oko 300-400M, pa je Old podesen na oko 1.5G, a preostalih 2.5G dodeljeno Young oblasti. Samo promenom parametra velicine Young oblasti (-Xmn), broj Young GC-a u jednom minutu pao je sa 26 na 11, bez povecanja vremena jednog prikupljanja; ukupno GC vreme palo je sa 1100ms na 500ms, a ucestalost CMS GC-a sa jednog u 40 minuta na jedan u 7 sati i 30 minuta.


Ako je brzina dodele prevelika:
Povremeno velika: memorijskim alatom za analizu pronadjite problematican kod i optimizujte poslovnu logiku.
Stalno velika: trenutni Collector vise ne zadovoljava ocekivanja Mutator-a; u tom slucaju ili prosirite VM Mutator-a, ili promenite tip GC sakuplaca ili povecate prostor.
4.4.4 Rezime
Problem prevelike promocije obicno nije narocito uocljiv, ali vremenom moze eskalirati u talas problema poput degradacije sakuplaca, pa ga treba izbeci unapred. Proverite da li se u vasem sistemu javljaju ove pojave; ako se podudaraju, pokusajte sa optimizacijom. ROI optimizacije jedne linije koda i dalje je veoma visok.
Ako pri posmatranju promene odnosa u Old oblasti pre i posle uocite da je procenat koji se moze prikupiti veoma mali — npr. sa 80% padne samo na 60% — to znaci da je vecina nasih objekata ziva, pa se prostor Old oblasti moze umerno povecati.
4.4.5 Dodatak
Kada se podesava odnos Young i Old, kako izabrati konkretnu vrednost NewRatio? Ovde se problem apstrahuje u model rezervoara, sa sledecim kljucnim metrikama; mozete izracunavati prema svom scenariju.


Vrednost NewRatio-a r u izvesnoj je funkcijskoj zavisnosti sa va, vp, vyc, voc, rs itd. (sto je rs manji to je r veci; sto je r manji to je vp manji ... Ranije smo pokusavali da u modelovanju pomognemo NN-om, ali konkretan formula jos uvek nije u potpunosti izveden; citaoci sa idejama neka svoj odgovor ostave u komentarima).
Ukupno vreme pauze T jeste zbir ukupnog vremena Young GC-a Tyc i ukupnog vremena Old GC-a Toc, pri cemu Tyc zavisi od vyc i vp, a Toc od voc.
Ako se zanemari GC vreme, vremenski razmak između dva Young GC-a treba da bude veći od TP9999 vremena, tako da se objekti, koliko je moguće, prikupe već u Eden oblasti, čime se smanjuje mnogo pauza.
**4.5 Scenario 5: Ucestali CMS Old GC ***
4.5.1 Pojava
Old oblast cesto radi CMS GC, ali svaki put vreme nije narocito dugo, a ukupno maksimalno STW je u prihvatljivim granicama; medjutim, zbog previse cestog GC-a propusnost znatno opada.
4.5.2 Uzrok
Ova situacija je prilicno cesta: nakon zavrsetka jednog Young GC-a, pozadinska nit concurrentMarkSweepThread, zaduzena za CMS GC, neprestano anketira, pomoci metoda shouldConcurrentCollect() obavlja jednu proveru i ocenjuje da li je ispunjen uslov za prikupljanje. Ako je ispunjen, pomoci collect_in_background() pokrece jedan GC u Background rezimu. Anketiranje se obavlja pomoci metoda sleepBeforeNextCycle(), a period je odredjen sa -XX:CMSWaitDuration, podrazumevano 2s.
Konkretan kod je u: src/hotspot/share/gc/cms/concurrentMarkSweepThread.cpp.
run_service()
void ConcurrentMarkSweepThread::run_service() {
assert(this == cmst(), "just checking");
if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) {
log_warning(gc)("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread);
}
while (!should_terminate()) {
sleepBeforeNextCycle();
if (should_terminate()) break;
GCIdMark gc_id_mark;
GCCause::Cause cause = _collector->_full_gc_requested ?
_collector->_full_gc_cause : GCCause::_cms_concurrent_mark;
_collector->collect_in_background(cause);
}
verify_ok_to_terminate();
}sleepBeforeNextCycle()
void ConcurrentMarkSweepThread::sleepBeforeNextCycle() {
while (!should_terminate()) {
if(CMSWaitDuration >= 0) {
// Wait until the next synchronous GC, a concurrent full gc
// request or a timeout, whichever is earlier.
wait_on_cms_lock_for_scavenge(CMSWaitDuration);
} else {
// Wait until any cms_lock event or check interval not to call shouldConcurrentCollect permanently
wait_on_cms_lock(CMSCheckInterval);
}
// Check if we should start a CMS collection cycle
if (_collector->shouldConcurrentCollect()) {
return;
}
// .. collection criterion not yet met, let's go back
// and wait some more
}
}Kod koji ocenjuje da li prikupljanje treba izvrsiti nalazi se u: /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp.
shouldConcurrentCollect()
bool CMSCollector::shouldConcurrentCollect() {
LogTarget(Trace, gc) log;
if (_full_gc_requested) {
log.print("CMSCollector: collect because of explicit gc request (or GCLocker)");
return true;
}
FreelistLocker x(this);
// ------------------------------------------------------------------
// Print out lots of information which affects the initiation of
// a collection.
if (log.is_enabled() && stats().valid()) {
log.print("CMSCollector shouldConcurrentCollect: ");
LogStream out(log);
stats().print_on(&out);
log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full());
log.print("free=" SIZE_FORMAT, _cmsGen->free());
log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available());
log.print("promotion_rate=%g", stats().promotion_rate());
log.print("cms_allocation_rate=%g", stats().cms_allocation_rate());
log.print("occupancy=%3.7f", _cmsGen->occupancy());
log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end());
log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect());
}
// ------------------------------------------------------------------
if (!UseCMSInitiatingOccupancyOnly) {
if (stats().valid()) {
if (stats().time_until_cms_start() == 0.0) {
return true;
}
} else {
if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f",
_cmsGen->occupancy(), _bootstrap_occupancy);
return true;
}
}
}
if (_cmsGen->should_concurrent_collect()) {
log.print("CMS old gen initiated");
return true;
}
// We start a collection if we believe an incremental collection may fail;
// this is not likely to be productive in practice because it's probably too
// late anyway.
CMSHeap* heap = CMSHeap::heap();
if (heap->incremental_collection_will_fail(true /* consult_young */)) {
log.print("CMSCollector: collect because incremental collection will fail ");
return true;
}
if (MetaspaceGC::should_concurrent_collect()) {
log.print("CMSCollector: collect for metadata allocation ");
return true;
}
// CMSTriggerInterval starts a CMS cycle if enough time has passed.
if (CMSTriggerInterval >= 0) {
if (CMSTriggerInterval == 0) {
// Trigger always
return true;
}
// Check the CMS time since begin (we do not check the stats validity
// as we want to be able to trigger the first CMS cycle as well)
if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
if (stats().valid()) {
log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
stats().cms_time_since_begin());
} else {
log.print("CMSCollector: collect because of trigger interval (first collection)");
}
return true;
}
}
return false;
}Analizom te logike ocenjujemo da li se GC pokrece; deli se na sledece situacije:
Okidanje CMS GC-a: pozivom
_collector->collect_in_background()pokrece se Background GC.CMS podrazumevano koristi statisticke podatke iz rada JVM-a da oceni da li treba pokrenuti CMS GC; ako zelite da ocenjujete na osnovu vrednosti
-XX:CMSInitiatingOccupancyFraction, potrebno je postaviti parametar-XX:+UseCMSInitiatingOccupancyOnly.Ako je ukljucen parametar
-XX:UseCMSInitiatingOccupancyOnly, ocenjuje se da li je trenutna iskoriscenost Old oblasti veca od praga, pa se pokrece CMS GC; taj prag se moze podesiti parametrom-XX:CMSInitiatingOccupancyFraction, a ako nije postavljen, podrazumevano je 92%.Ako je prethodni Young GC vec bio neuspesan, ili sledeci Young GC u Young oblasti mozda nece uspeti, u oba slucaja treba pokrenuti CMS GC.
CMS podrazumevano ne prikuplja smece u MetaSpace-u ili Perm-u; ako zelite da prikupljate smece u tim oblastima, potrebno je postaviti parametar
-XX:+CMSClassUnloadingEnabled.Okidanje Full GC-a: direktno se obavlja Full GC; ova situacija se detaljnije razmatra u scenariju 7.
Ako je
_full_gc_requestedtacno, to znaci da postoji izricita potreba za GC-jem, npr. poziv System.gc.Neuspeh dodele memorije u Eden oblasti za objekat ili TLAB izaziva jedan Young GC, a ocena se donosi u metodu
satisfy_failed_allocation()klaseGenCollectorPolicy.
Mozete pogledati stampanje logova u izvornom kodu; na osnovu logova prilicno jasno mozemo saznati konkretan razlog, a zatim pristupiti analizi.
4.5.3 Strategija
Ovde cemo uzeti najcesci scenario — dostizanje odnosa prikupljanja. Za razliku od prevelike promocije, ovi objekti zaista zive izvesno vreme; Survival Time prelazi TP9999 vreme, ali opet ne dostize dugozivece, kao sto su razne baze, mrezne veze, kesovi sa vremenom isticanja itd.
Resavanje ovakvog uobicajenog problema curenja memorije uglavnom sledi jednu ideju; glavni koraci su:

Dump Diff i Leak Suspects prilicno su intuitivni pa ih ne objasnjavamo; ovde cemo pomenci nekoliko drugih kljucnih tacaka:
Memorijski Dump: kada pomoci jmap, arthas i sl. pravite dump hipa za snimku, setite se da sklonite saobracaj, i napravite po jedan dump pre i posle CMS GC-a.
Analiza Top Component-a: setite se da posmatrate Histogram po vise dimenzija — objekat, klasa, classloader, paket — i da pomoci outgoing i incoming analizirate povezane objekte; takdje bacite pogled na Soft Reference, Weak Reference, Finalizer itd.
Analiza Unreachable: na ovo obratite posebnu paznju i pratite velicine Shallow i Retained. Primer iz jedne ranije GC optimizacije: u MAT-ovom pregledu Unreachable Objects vise je desetina MB objekata klase
Hystrix RollingNumberzadrzavano kliznim prozorom (rolling window), sto je na osnovu te liste odmah ukazalo na problem u Hystrix-u.
4.5.4 Rezime
Posle celog procesa problem se uglavnom moze locirati; medjutim, prilikom optimizacije setite se da koristite metod kontrolisane promenljive, kako neke izmene koje pogorsavaju problem ne bi bile prikrivene.
**4.6 Scenario 6: Dugo trajanje jednog CMS Old GC-a ***
4.6.1 Pojava
CMS GC pojedinacno STW maksimalno prelazi 1000ms i ne desava se cesto; kao na slici ispod, najduze je dostiglo 8000ms. U nekim scenarijima moze izazvati «efekat lavine», sto je veoma opasna situacija koju treba izbeci.

4.6.2 Uzrok
Tokom CMS prikupljanja, STW faze su uglavnom Init Mark i Final Remark — to je i najcesci uzrok dugog CMS Old GC-a. Pored toga, u nekim slucajevima cekanje da niti Mutator-a stignu do SafePoint pre STW-a takode moze produziti vreme, ali to je redje; ovde uglavnom razmatramo prvo. Scenariji degradacije sakuplaca ili sazimanja fragmenata opisani su u scenariju 7.
Da bismo razumeli zasto te dve faze trose vreme, prvo moramo videti sta one rade.
Kljucni kod se nalazi u /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp; interno, nit ConcurrentMarkSweepThread anketira i proverava, a detalji prikupljanja smeca u Old oblasti potpuno su enkapsulirani u CMSCollector. Ulaz za poziv je CMSCollector::collect_in_background, koga poziva ConcurrentMarkSweepThread, i metod CMSCollector::collect, koga poziva ConcurrentMarkSweepGeneration; ovde razmatramo collect_in_background za vecinu scenarija. U celom procesu STW uglavnom izazivaju initial Mark i Final Remark, a kljucni kod je u VM_CMS_Initial_Mark / VM_CMS_Final_Remark; pri izvrsavanju kontrola se prepusta VMThread-u.
- Koraci izvrsenja CMS Init Mark, implementirani u
CMSCollector::checkpointRootsInitialWork()iCMSParInitialMarkTask::work; ukupni koraci i kod su ispod:
CMSCollector::checkpointRootsInitialWork()
void CMSCollector::checkpointRootsInitialWork() {
assert(SafepointSynchronize::is_at_safepoint(), "world should be stopped");
assert(_collectorState == InitialMarking, "just checking");
// Already have locks.
assert_lock_strong(bitMapLock());
assert(_markBitMap.isAllClear(), "was reset at end of previous cycle");
// Setup the verification and class unloading state for this
// CMS collection cycle.
setup_cms_unloading_and_verification_state();
GCTraceTime(Trace, gc, phases) ts("checkpointRootsInitialWork", _gc_timer_cm);
// Reset all the PLAB chunk arrays if necessary.
if (_survivor_plab_array != NULL && !CMSPLABRecordAlways) {
reset_survivor_plab_arrays();
}
ResourceMark rm;
HandleMark hm;
MarkRefsIntoClosure notOlder(_span, &_markBitMap);
CMSHeap* heap = CMSHeap::heap();
verify_work_stacks_empty();
verify_overflow_empty();
heap->ensure_parsability(false); // fill TLABs, but no need to retire them
// Update the saved marks which may affect the root scans.
heap->save_marks();
// weak reference processing has not started yet.
ref_processor()->set_enqueuing_is_done(false);
// Need to remember all newly created CLDs,
// so that we can guarantee that the remark finds them.
ClassLoaderDataGraph::remember_new_clds(true);
// Whenever a CLD is found, it will be claimed before proceeding to mark
// the klasses. The claimed marks need to be cleared before marking starts.
ClassLoaderDataGraph::clear_claimed_marks();
print_eden_and_survivor_chunk_arrays();
{
if (CMSParallelInitialMarkEnabled) {
// The parallel version.
WorkGang* workers = heap->workers();
assert(workers != NULL, "Need parallel worker threads.");
uint n_workers = workers->active_workers();
StrongRootsScope srs(n_workers);
CMSParInitialMarkTask tsk(this, &srs, n_workers);
initialize_sequential_subtasks_for_young_gen_rescan(n_workers);
// If the total workers is greater than 1, then multiple workers
// may be used at some time and the initialization has been set
// such that the single threaded path cannot be used.
if (workers->total_workers() > 1) {
workers->run_task(&tsk);
} else {
tsk.work(0);
}
} else {
// The serial version.
CLDToOopClosure cld_closure(¬Older, true);
heap->rem_set()->prepare_for_younger_refs_iterate(false); // Not parallel.
StrongRootsScope srs(1);
heap->cms_process_roots(&srs,
true, // young gen as roots
GenCollectedHeap::ScanningOption(roots_scanning_options()),
should_unload_classes(),
¬Older,
&cld_closure);
}
}
// Clear mod-union table; it will be dirtied in the prologue of
// CMS generation per each young generation collection.
assert(_modUnionTable.isAllClear(),
"Was cleared in most recent final checkpoint phase"
" or no bits are set in the gc_prologue before the start of the next "
"subsequent marking phase.");
assert(_ct->cld_rem_set()->mod_union_is_clear(), "Must be");
// Save the end of the used_region of the constituent generations
// to be used to limit the extent of sweep in each generation.
save_sweep_limits();
verify_overflow_empty();
}
Ceo proces je prilicno jednostavan: polazi se od GC Root-a i obelezavaju se objekti u Old oblasti; po zavrsetku se pomoci BitMap obrade reference iz Young oblasti ka Old oblasti. Ceo proces je uglavnom brz i retko daje duze pauze.
- Koraci izvrsenja CMS Final Remark, implementirani u
CMSCollector::checkpointRootsFinalWork(); ukupni kod i koraci su ispod:
CMSCollector::checkpointRootsFinalWork()
void CMSCollector::checkpointRootsFinalWork() {
GCTraceTime(Trace, gc, phases) tm("checkpointRootsFinalWork", _gc_timer_cm);
assert(haveFreelistLocks(), "must have free list locks");
assert_lock_strong(bitMapLock());
ResourceMark rm;
HandleMark hm;
CMSHeap* heap = CMSHeap::heap();
if (should_unload_classes()) {
CodeCache::gc_prologue();
}
assert(haveFreelistLocks(), "must have free list locks");
assert_lock_strong(bitMapLock());
heap->ensure_parsability(false); // fill TLAB's, but no need to retire them
// Update the saved marks which may affect the root scans.
heap->save_marks();
print_eden_and_survivor_chunk_arrays();
{
if (CMSParallelRemarkEnabled) {
GCTraceTime(Debug, gc, phases) t("Rescan (parallel)", _gc_timer_cm);
do_remark_parallel();
} else {
GCTraceTime(Debug, gc, phases) t("Rescan (non-parallel)", _gc_timer_cm);
do_remark_non_parallel();
}
}
verify_work_stacks_empty();
verify_overflow_empty();
{
GCTraceTime(Trace, gc, phases) ts("refProcessingWork", _gc_timer_cm);
refProcessingWork();
}
verify_work_stacks_empty();
verify_overflow_empty();
if (should_unload_classes()) {
CodeCache::gc_epilogue();
}
JvmtiExport::gc_epilogue();
assert(_markStack.isEmpty(), "No grey objects");
size_t ser_ovflw = _ser_pmc_remark_ovflw + _ser_pmc_preclean_ovflw +
_ser_kac_ovflw + _ser_kac_preclean_ovflw;
if (ser_ovflw > 0) {
log_trace(gc)("Marking stack overflow (benign) (pmc_pc=" SIZE_FORMAT ", pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ", kac_preclean=" SIZE_FORMAT ")",
_ser_pmc_preclean_ovflw, _ser_pmc_remark_ovflw, _ser_kac_ovflw, _ser_kac_preclean_ovflw);
_markStack.expand();
_ser_pmc_remark_ovflw = 0;
_ser_pmc_preclean_ovflw = 0;
_ser_kac_preclean_ovflw = 0;
_ser_kac_ovflw = 0;
}
if (_par_pmc_remark_ovflw > 0 || _par_kac_ovflw > 0) {
log_trace(gc)("Work queue overflow (benign) (pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ")",
_par_pmc_remark_ovflw, _par_kac_ovflw);
_par_pmc_remark_ovflw = 0;
_par_kac_ovflw = 0;
}
if (_markStack._hit_limit > 0) {
log_trace(gc)(" (benign) Hit max stack size limit (" SIZE_FORMAT ")",
_markStack._hit_limit);
}
if (_markStack._failed_double > 0) {
log_trace(gc)(" (benign) Failed stack doubling (" SIZE_FORMAT "), current capacity " SIZE_FORMAT,
_markStack._failed_double, _markStack.capacity());
}
_markStack._hit_limit = 0;
_markStack._failed_double = 0;
if ((VerifyAfterGC || VerifyDuringGC) &&
CMSHeap::heap()->total_collections() >= VerifyGCStartAt) {
verify_after_remark();
}
_gc_tracer_cm->report_object_count_after_gc(&_is_alive_closure);
// Change under the freelistLocks.
_collectorState = Sweeping;
// Call isAllClear() under bitMapLock
assert(_modUnionTable.isAllClear(),
"Should be clear by end of the final marking");
assert(_ct->cld_rem_set()->mod_union_is_clear(),
"Should be clear by end of the final marking");
}
Final Remark je drugo, zavrsno obelezavanje; izvrsava se samo ako je Background GC obavio korak InitialMarking — ako je InitialMarking obavio Foreground GC, FinalRemark ne treba ponovo da se izvrsi. Pocetna faza Final Remark-a ista je kao kod Init Mark-a, ali kasnije sadrzi i obilazak Card Table, ciscenje instanci Reference i njihovo dodavanje u pend_list koju odrzava Reference; ako se prikupljaju podaci o metapodacima, ciste se i nekorisceni resursi u komponentama poput SystemDictionary, CodeCache, SymbolTable, StringTable.
4.6.3 Strategija
Kada znamo tok izvrsenja dva STW procesa, analiza i resavanje su jednostavniji. Po sto vecina problema nastaje u procesu Final Remark, i ovde cemo uzeti taj scenario kao primer; glavni koraci:
- [Smer] Posmatrajte detaljan GC log, pronadjite Final Remark log kada je problem nastao, analizirajte da li je realno vreme obrade Reference i metapodataka normalno; detaljne informacije zahtevaju ukljucivanje parametra
-XX:+PrintReferenceGC. Uglavnom se vec iz loga moze locirati okvirni smer problema; na ono sto traje preko 10% treba obratiti paznju.
2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs][class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs][Koreni uzrok] Sa konkretnim smerom mozemo preci na dublju analizu. Uglavnom se najlaksa mesta za problem nalaze u FinalReference unutar Reference i u fazi scrub symbol table obrade metapodataka; da biste pronasli konkretno problematican kod potrebni su vam memorijski alati MAT ili JProfiler, i setite se da dumpujete hip neposredno pre pocetka CMS GC-a. Pre koriscenja MAT-a i slicnih alata mozete i sa komandne linije pogledati Histogram objekata — mozda cete odmah locirati problem.
Analiza FinalReference uglavnom posmatra dominator tree objekta
java.lang.ref.Finalizeri trazi izvor curenja. Mesta koja cesto stvaraju probleme suSocksSocketImpliz Socket-a,ClientRuntimeiz Jersey-ja,ConnectionImpliz MySQL-a itd.scrub symbol table oznacava vreme ciscenja simbolickih referenci metapodataka; simbolicka referenca je oblik u kom se metod prikazuje u JVM-u kada se Java kod kompajlira u bajtkod, a zivotni vek joj je uglavnom isti kao i Class. Kada je
_should_unload_classespostavljeno na true, obradjuje se uCMSCollector::refProcessingWork()zajedno sa Class Unload i String Table.
CMSCollector::refProcessingWork()
if (should_unload_classes()) {
{
GCTraceTime(Debug, gc, phases) t("Class Unloading", _gc_timer_cm);
// Unload classes and purge the SystemDictionary.
bool purged_class = SystemDictionary::do_unloading(_gc_timer_cm);
// Unload nmethods.
CodeCache::do_unloading(&_is_alive_closure, purged_class);
// Prune dead klasses from subklass/sibling/implementor lists.
Klass::clean_weak_klass_links(purged_class);
}
{
GCTraceTime(Debug, gc, phases) t("Scrub Symbol Table", _gc_timer_cm);
// Clean up unreferenced symbols in symbol table.
SymbolTable::unlink();
}
{
GCTraceTime(Debug, gc, phases) t("Scrub String Table", _gc_timer_cm);
// Delete entries for dead interned strings.
StringTable::unlink(&_is_alive_closure);
}
}[Strategija] Kada se zna koreni uzrok GC vremena, lakse je postupati; ovakav problem ne izbija istovremeno na velikoj povrsini, ali cesto pojedinacno STW vreme jedne masine moze biti dugo. Ako je uticaj na posao veliki, na vreme sklonite saobracaj; konkretne naknadne strategije optimizacije su:
FinalReference: nakon lociranja izvora memorije resava se optimizacijom koda; ako se ne moze brzo locirati, moze se dodati
-XX:+ParallelRefProcEnabledza paralelnu obradu Reference.symbol table: posmatrajte istorijske vrhunce iskoriscenosti MetaSpace oblasti i stanje prikupljanja pre i posle svakog GC-a. Generalno, ako nema dinamickog ucitavanja klasa ili obrade DSL-a i sl., u iskoriscenosti MetaSpace-a nece biti promena; u tom slucaju se pomoci
-XX:-CMSClassUnloadingEnabledmoze izbeci obrada MetaSpace-a. JDK 8 podrazumevano ukljucuje CMSClassUnloadingEnabled, zbog cega CMS u fazi CMS-Remark pokusava izvrsiti istovar klasa.
4.6.4 Rezime
Background CMS GC u normalnim uslovima: problemi se uglavnom koncentrisu na obradu metapodataka poput Reference i Class. Sto se tice problema sa Reference — bilo FinalReference, SoftReference ili WeakReference — kljucno sredstvo jeste odabrati pravi trenutak za dump snimke, a zatim ih analizirati memorijskim alatom. Za obradu Class-a, trenutno nema nekog dobrog metoda osim iskljucivanja prekidaca za istovar klasa.
I u G1 postoji problem sa Reference; mozete posmatrati Ref Proc u logovima, a nacin obrade je slican kao kod CMS-a.
4.7 Scenario 7: Memorijska fragmentacija i degradacija sakuplaca
4.7.1 Pojava
Konkurentni CMS GC algoritam degradira u Foreground jednonitni serijski GC rezim, sa izuzetno dugim STW-om, ponekad i do desetinak sekundi. Nakon degradacije CMS sakuplaca, jednonitni serijski GC algoritam ima dve varijante:
Algoritam sa kompresijom, poznat kao MSC, koji smo gore opisali: koristi obelezi-pocisti-sabij, jednonitno uz punu pauzu, prikuplja smece na celom hipu — to je Full GC u pravom smislu, a vreme pauze duze je nego kod obicnog CMS-a.
Algoritam bez kompresije, koji prikuplja Old oblast; prilicno je slican obicnom CMS algoritmu, a vreme pauze mu je nesto krace nego kod MSC algoritma.
4.7.2 Uzrok
Degradacija sakuplaca u CMS-u uglavnom nastaje u sledecim situacijama.
Neuspeh promocije (Promotion Failed)
Ime kaze: neuspeh promocije znaci da prilikom Young GC-a Survivor ne moze da prihvti objekte, pa oni moraju u Old, ali tada ni Old nema mesta. Na prvi pogled cini se da se to cesto desava, ali zapravo, zbog postojanja concurrentMarkSweepThread-a i mehanizma garancije, uslovi nastanka su veoma strogi — osim ako se preostali prostor Old oblasti brzo ne popuni u kratkom vremenu, npr. prevelikom promocijom izazvanom dinamickom ocenom uzrasta (vidi neuspeh garancije inkrementalnog prikupljanja ispod). Postoji jos jedna situacija: Promotion Failed izazvan memorijskom fragmentacijom, gde Young GC misli da Old ima dovoljno prostora, ali pri dodeli unapredjeni veliki objekat ne moze naci neprekidan prostor.
Kada se CMS koristi kao GC sakuplac, Old oblast nakon izvesnog rada izgleda kao na slici ispod: algoritam ciscenja dovodi do vise neprekidnih segmenata memorije, pa nastaje velika kolicina memorijskih fragmenata.
+--------------------------------------+
| stara generacija (old) |
| ┌────────┬────────────┬──────────┐ |
| │ used │ free space│fragmen- │ |
| │ │ | tacija │ |
| └────────┴────────────┴──────────┘ |
+--------------------------------------+Fragmenti dovode do dva problema:
Niza efikasnost dodele prostora: gore je vec pomenuto da, ako je prostor neprekidan, JVM moze dodeljivati pomoci pointer bumping-a, dok se za slobodnu listu sa mnogo fragmenata mora pristupati stavci po stavci freelist-e da bi se pronasla adresa na kojoj moze stati novi objekat.
Niza efikasnost iskoriscenosti prostora: ako je velicina objekta koji se unapredjuje iz Young oblasti veca od velicine neprekidnog prostora, to ce okinuti Promotion Failed; cak i kad je kapacitet cele Old oblasti dovoljan, zbog neprekidnosti on ne moze prihvatiti novi objekat — sto je upravo problem o kome je rec u ovom tekstu.
Neuspeh garancije inkrementalnog prikupljanja
Posle neuspeha dodele memorije, ocenjuje se da li je zbir prosecne velicine promocije iz Young GC-a u Old i velicine trenutno iskoriscenog Young-a (odnosno maksimalne moguce velicine objekta za promociju) veci od preostalog prostora Old oblasti. Sve dok je preostali prostor CMS-a veci od bilo koje od te dve vrednosti, CMS smatra da je promocija jos uvek bezbedna; u suprotnom smatra da nije bezbedna, ne radi Young GC i direktno okida Full GC.
Eksplicitni GC
Ova situacija se vidi u scenariju 2.
Neuspeh konkurentnog rezima (Concurrent Mode Failure)
Poslednja situacija, ujedno i jedna od vecce verovatnoce: u GC logu se cesto moze videti kljucna rec Concurrent Mode Failure. Nastaje zato sto se konkurentni Background CMS GC izvrsava, a istovremeno objekti unapredjeni iz Young GC-a treba da se smeste u Old oblast, pri cemu Old oblast tada nema dovoljno prostora.
Zasto degradacija sakuplaca nastaje i dok se CMS GC izvrsava? uglavnom zbog toga sto CMS ne moze obraditi plutajuce smece (Floating Garbage). U konkurentnoj fazi ciscenja CMS-a Mutator i dalje radi, pa se neprestano stvara novo smece; to smece nije obuhvaceno obelezavanjem u ovom ciklusu ciscenja, pa ne moze biti prikupljeno u ovom GC-u — to je plutajuce smece. Pored toga, i objekti cija je referenca prekinuta pre Remark-a, a time izmakla kontroli read/write barrier-a, takode se racunaju kao plutajuce smece. Zato prag prikupljanja Old oblasti ne sme biti previsok, jer rezervisani memorijski prostor onda moze biti nedovoljan, sto dovodi do Concurrent Mode Failure.
4.7.3 Strategija
Kada analiziramo konkretan uzrok, mozemo ciljano resiti; ideja i dalje polazi od korenog uzroka. Konkretne strategije:
Memorijska fragmentacija: konfiguracijom
-XX:UseCMSCompactAtFullCollection=truekontrolise se da li se tokom Full GC-a obavlja sabijanje prostora (podrazumevano ukljuceno; napomena: u pitanju je Full GC, ne obican CMS GC), a-XX: CMSFullGCsBeforeCompaction=nkontrolise nakon koliko Full GC-a se obavlja jedna kompresija.Inkrementalno prikupljanje: sniziti prag za okidanje CMS GC-a, odnosno vrednost parametra
-XX:CMSInitiatingOccupancyFraction, da bi se CMS GC izvrsio sto ranije, cime se obezbedjuje dovoljno neprekidnog prostora i smanjuje iskoriscenost Old oblasti; takode treba koristiti-XX:+UseCMSInitiatingOccupancyOnlykao dopunu, jer u suprotnom JVM tu vrednost koristi samo prvi put, a kasnije je automatski podesava.Plutajuce smece: prema potrebi kontrolisite velicinu objekata koji se unapredjuju, ili skratite vreme svakog CMS GC-a; po potrebi podesite vrednost NewRatio. Pored toga, pomoci
-XX:+CMSScavengeBeforeRemarkmozete u toku procesa ranije pokrenuti jedan Young GC i time sprecaiti naknadno unapredjenje previse objekata.
4.7.4 Rezime
U normalnim uslovima CMS GC u konkurentnom rezimu ima veoma kratke pauze i malo utice na posao, ali nakon degradacije CMS GC-a uticaj je ogroman, pa se preporucuje da se cim se jednom otkrije problem resi do kraja. Dok god se locira konkretan uzrok nastanka — memorijska fragmentacija, plutajuce smece, inkrementalno prikupljanje — prilicno je dobro resiv. Sto se tice memorijske fragmentacije, ako je tesko izabrati vrednost -XX:CMSFullGCsBeforeCompaction, moze se koristiti -XX:PrintFLSStatistics za posmatranje stepena fragmentacije memorije, a zatim postaviti konkretnu vrednost.
Na kraju, i prilikom kodiranja treba izbegavati nastanak velikih objekata kojima je potreban neprekidan adresni prostor, poput predugih stringova, bajt-nizova za cuvanje priloga, serijalizaciju ili deserijalizaciju itd.; takode, problem prevelike promocije potrebno je izbeci pre nego sto eskalira.
4.8 Scenario 8: OOM van hipa
4.8.1 Pojava
Iskoriscenost memorije neprestano raste, pa se cak pocinje koristiti i SWAP memorija; istovremeno se mogu javiti skokovi GC vremena, blokada niti i slicno, a komandom top se uocava da RES Java procesa cak prelazi ****velicinu -Xmx. Pojavom tih pojava moze se sa sigurnoscu zakljuciti da je nastupilo curenje memorije van hipa.
4.8.2 Uzrok
Curenje memorije van hipa u JVM-u uglavnom ima dva uzroka:
Aktivno zatrazena memorija van hipa preko
UnSafe#allocateMemory,ByteBuffer#allocateDirectkoja nije oslobodjena; cesto kod komponenti poput NIO, Netty.Memorija koja je u kodu zatrazena pozivom Native Code-a preko JNI-ja, a nije oslobodjena.
4.8.3 Strategija
Koji uzrok je izazvao curenje memorije van hipa?
Prvo moramo utvrditi koji uzrok je doveo do curenja memorije van hipa. Ovde se za analizu moze koristiti NMT (NativeMemoryTracking). Nakon dodavanja JVM parametra -XX:NativeMemoryTracking=detail u projekat, ponovo pokrenite projekat (napomena: ukljucivanje NMT-a donosi pad performansi od 5%~10%). Komandom jcmd pid VM.native_memory detail pregledajte raspodelu memorije. Posmatrajte pre svega committed u total-u, jer memorija koju prikazuje jcmd ukljucuje memoriju u hipu, Code oblast i memoriju zatrazenu preko Unsafe.allocateMemory i DirectByteBuffer, ali ne ukljucuje memoriju van hipa koju zatrazuje ostali Native Code (C kod).
Ako su committed u total-u i RES iz top-a priblizno jednaki, onda je curenje izazvano neoslobodjenom memorijom van hipa koja je aktivno zatrazena; ako se znatno razlikuju, moze se sa sigurnoscu zakljuciti da je izazvano pozivima JNI-ja.
Uzrok 1: Aktivno zatrazeno, neoslobodjeno
JVM pomoci parametra -XX:MaxDirectMemorySize=size kontrolise maksimalnu vrednost memorije van hipa koja se moze zatraziti. U Javi 8, ako parametar nije konfigurisan, podrazumevano je jednak -Xmx.
I NIO i Netty uzimaju vrednost konfigurisanu u -XX:MaxDirectMemorySize da ogranicic velicu memorije van hipa koja se zatrazuje. U NIO i Netty postoji i polje brojaca koje racuna trenutnu zatrazenu velicinu memorije van hipa: u NIO-u java.nio.Bits#totalCapacity, u Netty-ju io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER.
Kada se zatrazuje memorija van hipa, NIO i Netty porede vrednost brojaca sa maksimalnom vrednoscu; ako vrednost brojaca prelazi ogranicenje maksimuma, bacice OOM izuzetak.
U NIO-u je: OutOfMemoryError: Direct buffer memory.
U Netty-ju je: OutOfDirectMemoryError: failed to allocate capacity bajt(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT ).
Mozemo proveriti kako kod koristi memoriju van hipa — NIO ili Netty — pa refleksijom doci do polja brojaca u odgovarajucoj komponenti i u projektu na vrsstiti vrednost tog polja, cime se ova memorija van hipa moze tacno nadzirati.
U ovom trenutku se Debug-om moze utvrditi da li su mesta koja koriste memoriju van hipa ispravno izvrsila kod za oslobadjanje memorije. Pored toga, proverite da li JVM parametri sadrze -XX:+DisableExplicitGC; ako sadrze, uklonite ga, jer taj parametar onesposobljava System.gc. (Scenario 2: Zadrzati ili ne eksplicitni GC)
Uzrok 2: Memorija koju je zatrazio Native Code pozvan preko JNI-ja nije oslobodjena
Ova situacija je teze za otklanjanje; mozemo pomoci alata poput Google perftools + Btrace analizirati gde se nalazi problematican kod.
gperftools je veoma koristan skup alata koji je razvio Google; princip mu je da, dok Java aplikacija radi, pri pozivu malloc-a zameni svojom libtcmalloc.so, cime moze voditi statistiku o dodeli memorije. Mi koristimo gperftools da pratimo komande dodele memorije. Kao na slici ispod, gperftools pokazuje da je Java_java_util_zip_Inflater_init sumnjiv.

Zatim se moze koristiti Btrace za pokusaj lociranja konkretnog pozivnog steka. Btrace je alat za pracenje i nadzor Jave koji je izdao Sun, a moze nadzirati Java program u produkciji bez zaustavljanja. Kao na slici ispod, Btrace je locirao da ZipHelper u projektu cesto poziva GZIPInputStream i dodeljuje objekte u memoriji van hipa.

Na kraju je locirano da je u pitanju neispravno koriscenje GIPInputStream u projektu — nije pravilno zatvoren close(), pa je svaka instanca zadrzavala nativni bafer van hipa.
Osim uzroka u samom projektu, curenje mogu izazvati i spoljasnje zavisnosti, poput Netty-ja i Spring Boot-a; detalje mozete nauciti iz ova dva clanka: «Traganje za uzrokom: Beleska o otklanjanju curenja memorije u Spring Boot-u», «Gozba otklanjanja curenja memorije van hipa u Netty-ju».
4.8.4 Rezime
Prvo se moze pomoci NMT + jcmd analizirati gde je procurela memorija van hipa zatrazena; po utvrdivanju uzroka, razlicitim sredstvima se vrsi lociranje uzroka:
# ukljuci Native Memory Tracking pri pokretanju: -XX:NativeMemoryTracking=summary
$ jcmd <pid> VM.native_memory summary
Total: reserved=...KB, committed=...KB
- Java Heap (reserved=..., committed=...)
- Class (reserved=..., committed=...) # metapodatci klasa
- Thread (reserved=..., committed=...) # stekovi niti
- Code (reserved=..., committed=...) # JIT kod
- GC (reserved=..., committed=...)
...
$ jcmd <pid> VM.native_memory diff # razlika u odnosu na baseline4.9 Scenario 9: GC problem izazvan JNI-jem
4.9.1 Pojava
U GC logu se kao GC Cause pojavljuje GCLocker Initiated GC.
2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs]
2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]4.9.2 Uzrok
JNI (Java Native Interface) znaci Java lokalni poziv; omogucava Java kodu da interaguje sa Native kodom pisanim u drugim jezicima.
Ako JNI treba da preuzme String ili niz iz JVM-a, postoje dva nacina:
Prenos kopiranjem.
Deljena referenca (pokazivac), sa vecim performansama.
Posto Native kod direktno koristi pokazivace na Heap oblast JVM-a, ako tada dodje do GC-a, dovodi do greske u podacima. Zato se, prilikom takvih JNI poziva, zabranjuje nastanak GC-a i sprecava druge niti da udu u JNI kritican sektor, sve dok poslednja nit ne izadje iz kritican sektora, kada se okida jedan GC.
Eksperiment sa GC Locker-om:
public class GCLockerTest {
static final int ITERS = 100;
static final int ARR_SIZE = 10000;
static final int WINDOW = 10000000;
static native void acquire(int[] arr);
static native void release(int[] arr);
static final Object[] window = new Object[WINDOW];
public static void main(String... args) throws Throwable {
System.loadLibrary("GCLockerTest");
int[] arr = new int[ARR_SIZE];
for (int i = 0; i < ITERS; i++) {
acquire(arr);
System.out.println("Acquired");
try {
for (int c = 0; c < WINDOW; c++) {
window[c] = new Object();
}
} catch (Throwable t) {
// omit
} finally {
System.out.println("Releasing");
release(arr);
}
}
}
}#include <jni.h>
#include "GCLockerTest.h"
static jbyte* sink;
JNIEXPORT void JNICALL Java_GCLockerTest_acquire(JNIEnv* env, jclass klass, jintArray arr) {
sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
}
JNIEXPORT void JNICALL Java_GCLockerTest_release(JNIEnv* env, jclass klass, jintArray arr) {
(*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0);
}Pokretanjem tog JNI programa vidi se da su sevi GC koji nastanu svi GCLocker Initiated GC, i napomenimo da izmedju «Acquired» i «Released» ne moze doci do GC-a.

Nezeljene posledice koje GC Locker moze izazvati:
Ako je tada Young oblast nestašicna, pa je GC izazvan Allocation Failure-om, posto se ne moze obaviti Young GC, objekti se dodeljuju direktno u Old oblast.
Ako ni Old oblast vise nema prostora, ceka se oslobadjanje brave, sto dovodi do blokade niti.
Moze se okinuti i visak, nepotreban Young GC. Postoji jedan Bug u JDK-u sa izvesnom verovatnocom: umesto da se okine samo jedan Young GC kao GCLocker Initiated GC, u stvari se desi jedan Allocation Failure GC, a odmah zatim jos jedan GCLocker Initiated GC; razlog je sto je svojstvo GCLocker Initiated GC-a postavljeno na full, pa se dva GC-a ne mogu konvergovati.
4.9.3 Strategija
- Dodavanjem parametra
-XX+PrintJNIGCStallsmogu se odstampati niti pri JNI pozivima, nakon cega se moze analizom naci JNI poziv koji izaziva problem. - Oprezno sa JNI pozivima — ne moraju nuzno da podignu performanse, a mogu naprotiv izazvati GC probleme.
- Nadgradite JDK na verziju 14, cime se izbegava ponavljani GC izazvan sa JDK-8048556.

4.9.4 Rezime
GC problemi koje stvara JNI teze su za otklanjanje, pa je potrebno koristiti ga oprezno.
5. Zakljucak
Ovde cemo sazeti sadrzaj celog clanka, da bi ga lakse celovito razumeli i ponovili.
5.1 Tok obrade (SOP)
Slika ispod prikazuje opsti tok obrade GC problema; kljucna mesta se posebno oznacavaju ispod, dok je ostalo uglavnom standardni tok obrade koji ovde necemo ponavljati. Na kraju, kad se ceo problem resi, preporucuje se da se, ako su uslovi prisutni, odrzi retrospektiva.
Uspostavljanje standarda: ova sadrzina je zapravo veoma vazna, ali je u vecini sistema nedostaje. Od ucenika koje je autor intervjuisao, manje od desetine je moglo dati konkretan izgled GC standarda svog sistema; ostali koriste uniformne sablone metrike, sto ostavlja nedostatak predvidljivosti. Konkretne metrike mogu se formirati prema sadrzaju u 3.1, potrebno ih je kombinovati sa TP9999 vremenom, latencijom, propusnoscu i sl. aplikacijskog sistema, umesto da budu vođene problemom.
Ocuvanje mesta problema: danas su servisi u produkciji uglavnom distribuirani; kad na jednom cvoru nastane problem, ako uslovi dozvoljavaju, nikako ne vracajte se odmah na restart, rollback i sl.; prvenstveno oporavite se sklanjanjem saobracaja, cime cemo sacuvati kljucne podatke poput hipa, steka, GC loga. U suprotnom se propusta trenutak za lociranje korenog uzroka, a naknadna resavanja znatno otezavaju. Pored toga, logovi aplikacije, logovi middlewere, kernel logovi, razne Metrics metrike itd. takode mnogo pomazu u analizi problema.
Analiza uzroka i posledice: ocenite uzrocno-posledicnu vezu izmedju GC anomalije i anomalija drugih sistemskih metrika; mozete se osloniti na cetiri metoda analize uzroka i posledice koje je autor predstavio u 3.2 — analiza vremenskog slede, verovatnocna, eksperimentalna i analiza protivu-dokaza — kako biste izbegli zablude tokom otklanjanja.
Analiza korenog uzroka: po utvrdivanju da je zaista problem u GC-u, moze se pomoci gore pomenutih alata, a zatim primeniti 5 Why analiza korenog uzroka i redom uporedivati sa devet uobicajenih scenarija iz treceg poglavlja, ili neposredno koristiti donji graf riblje kosti korenog uzroka, pronaci koreni uzrok nastanka problema i tek na kraju izabrati sredstvo optimizacije.
5.2 Graf riblje kosti korenog uzroka
Evo strukture analize korenog uzroka („graf riblje kosti") za GC probleme. Uopsteno, kada obradjujemo GC problem, dovoljno je locirati «zariste» problema i delovati ciljano — to vec znaci resiti 80%; ako u nekim scenarijima nije lako locirati, mozete se pomoci ovakvom strukturom i locirati metodom iskljucivanja.
5.3 Predlozi za optimizaciju
Trade Off: kao sto CAP po pravilu ostaje bez jednog ugla, i GC optimizacija zahteva balansiranje izmedju latencije (Latency), propusnosti (Throughput) i kapaciteta (Capacity).
Krajnje sredstvo: kada nastane GC problem, ne mora se nuzno podesavati JVM-ovi GC parametri; u vecini slucajeva se na osnovu stanja GC-a otkrivaju poslovni problemi. Setite se da ne krecite odmah od podesavanja GC parametara — naravno, osim u slucajevima sa jasnom greskom u konfiguraciji.
Kontrolisana promenljiva: metod kontrolisane promenljive je tehnika za smanjenje varijanse u Monte Carlo metodi; trudite se da ga koristite i prilikom optimizacije — u svakom koraku podesavanja menjajte sto je moguce manje, idealno samo jednu promenljivu.
Vesto koriscenje pretrage: u teoriji je 99.99% GC problema vec reseno; treba nauciti napredne tehnike pretrazivaca, sa posebnim fokusom na StackOverflow, Github Issue-e i razne forume i blogove — prvo pogledajte kako su drugi resili, sto resavanje cini dvostruko laksim. Ako ste nasli ovaj clanak, vase vestine pretrage su uglavnom na nivou~
Fokus optimizacije: uopsteno, tipovi problema koje srecemo u razvoju uglavnom prate normalnu raspodelu — previse jednostavni ili previse slozeni retko se sretnu. Autor je ovde tri najvaznija scenarija u sredini obelezio sa «*»; nadam se da cete nakon citanja ovog clanka posmatrati sistem za koji ste zaduzeni i proveriti da li u njemu postoje gore navedeni problemi.
GC parametri: ako se hip i stek zaista ne mogu odmah sauvati, obavezno sacuvajte GC log, cime cemo barem moci da vidimo GC Cause i dobijemo okviran smer otklanjanja. Sto se tice parametara vezanih za GC log, necemo ponavljati ni najosnovnije poput
-XX:+HeapDumpOnOutOfMemoryError; autor preporucuje dodavanje sledecih parametara, koji mogu povecati efikasnost analize problema.
GC log parametri po kategoriji:
- Vreme:
-XX:+PrintGCDetails,-XX:+PrintGCDateStamps,-XX:+PrintGCTimeStamps - Starost objekata:
-XX:+PrintTenuringDistribution - Promene prostora: zapisi o generatorima pre/posle
- Reference:
-XX:+PrintReferenceGC
Ostali predlozi: neki predlozi koji nisu pomenuci u gorenjim scenarijima, ali takode poboljsavaju GC performanse.
Proaktivni GC: postoji i drugaciji pristup — nadzorom pracenjem iskoriscenosti Old oblasti, neposredno pre dostizanja praga, skloniti saobracaj sa servisne aplikacije i rucno okinuti jedan Major GC, cime se smanjuju pauze koje donosi CMS GC; medjutim, time se smanjuje i robusnost sistema, pa se, osim ako nije neophodno, ne preporucuje.
Iskljucivanje biased lock-a: biased lock je veoma efikasan kada samo jedna nit koristi tu bravu, ali u izrazitoj konkurenciji nadgradice se u lightweight lock, a tada je prvo potrebno ukloniti biased lock, sto je STW proces. Ako svaka sinhronizovani resurs prolazi kroz taj proces nadgradnje, trosak je ogroman; zato se, po poznatoj jakoj konkurenciji, obicno iskljucuje biased lock
-XX:-UseBiasedLockingradi poboljsanja performansi.Virtuelna memorija: u pocetnoj fazi pokretanja, neki operativni sistemi (npr. Linux) JVM-u ne daju stvarno fizicku memoriju, vec je dodeljuju u virtuelnoj memoriji, a memorijske stranice u fizickoj memoriji dodeljuju se tek pri upotrebi, sto takode moze produziiti GC vreme. U ovoj situaciji moze se dodati parametar
-XX:+AlwaysPreTouch, kojim se VM-u kaze da pri commit-u memorije pokrene petlju i time prinudno osigura da je zatrazena memorija stvarno commit-ovana, izbegavajuci izuzetke nedostajucih stranica pri izvrsavanju. U nekim scenarijima sa velikom memorijom, to moze spustiti vreme prvih nekoliko GC-a za jedan red velicine, ali nakon dodavanja tog parametra proces pokretanja moze usporiti.
6. Na kraju
Na kraju, jos nekoliko licnih saveta autora: kada naidjete na GC probleme, ako imate energije, obavezno otkrijte najdublji uzrok. Pored toga, u ovo doba preplavljenosti informacijama, neka iskustva koja se uzimaju kao «nepogresiva merila» mozda su zapravo pogresna; trudite se da negujete naviku citanja izvornog koda. Kaze se «pred izvornim kodom nema tajni» — drugim recima, kada naidjete na problem koji ne mozete da razumete, mozemo zaviriti u izvorni kod, sto u odredjenim scenarijima zaista daje cudesne efekte. Ali ne ucite samo citanjem izvornog koda: ako grubo grizete izvorni kod a ne obracate paznju na teorijske osnove koje on moze sadrzati, lako cete «saku plucka ostaviti, a lubenicu izgubiti», «videti drvo, a ne videti sumu», pa «nema tajni» postaje prazna rec. Moramo se osloniti i na konkretne poslovne scenarije i ciljano uciti.
Gde je tvoje vreme, tu je i tvoj uspeh. Autor je tek u prethodne dve godine postepeno sve dublje ulazio u oblast GC-a: trazio probleme, citao izvorni kod, pravio rezimee, oblikujuci za svaki Case mali zatvoreni krug; trenutno je preliminarno nasao neke pristupe obradi GC problema, a iskustva istovremeno primenjuje u praksi u produkciji, polako formirajuci pozitivan krug.
Ovaj clanak je uglavnom predstavio analizu nekoliko uobicajenih scenarija CMS GC-a; neki drugi, kao sto su JIT neispravnost usled CodeCache problema, dugo vreme spremnosti SafePoint-a, vreme skeniranja Card Table-a i sl., nisu cesti, pa im nije posveceno previse prostora. Java GC je pod idejom «generacija» zapela mnogo godina pre nego sto se probo do «regiona». Trenutno je u Meituan-u vec pocelo koriscenje G1 da se zameni CMS koji se godinama koristio; iako je G1 kod malih hipova i dalje nesto losiji od CMS-a, to je trend, a u kratkom roku se ne moze preci na ZGC, pa ce u buducnosti problemi sa G1 verovatno postepeno rasti. Vec su prikupljeni problemi poput grubljenja Remember Set-a, Humongous dodele, Ergonomics anomalija, Evacuation Failure u Mixed GC-u; pored toga, bice dato i nekoliko predloga za prelazak sa CMS-a na G1. Autor ce nastaviti sa pripremom tog dela clanka — budite u ocekivanju.
«Preventivno delovanje» uvek je bolje od «gasenja pozara»: ne propustite nijednu anomalnu malu metliku (uopsteno, svaka neglatka kriva je sumnjiva), cime se moze izbeci nastanak kvara. Kao Java programeri, skoro svi cemo se susresti sa nekim GC problemima, a samostalno resavanje GC problema jeste prepreka koju moramo preskociti. U uvodu je vec receno da je GC, kao klasicna tehnologija, veoma vredna ucenja; neki materijali za ucenje GC-a, poput «The Garbage Collection Handbook», «Duboko razumevanje Java virtuelne masine» i sl., uvek iznova donose novo — pa krenite i uporno vezbajte GC osnove.
Na samom kraju, jos jedna opaska: trenutno svi clanci vezani za GC optimizaciju na prvom mestu kazu «ne optimizujte previse rano», zbog cega se mnogi ucenici suzdrzavaju od GC optimizacije. Ovde autor iznosi drugacije vidjenje: zakon porasta entropije (u izolovanom sistemu, bez spoljasnjeg rada, ukupna haoticnost, odnosno entropija, neprestano raste) vazi i za racunarske sisteme — ako se ne obavi aktivni rad kojim bi se entropija smanjila, sistem ce vas na kraju izmaci iz kontrole. Kada poslovni sistem i principe GC-a savladamo dovoljno duboko, mozemo optimizaciju raditi smelo i bez straha, jer uopsteno mozemo predvideti ishod svake operacije — pa udri, omladino!
