Ohjelmointi

JVM: n suorituskyvyn optimointi, osa 3: Jätteiden keräys

Java-alustan roskien keräysmekanismi lisää huomattavasti kehittäjien tuottavuutta, mutta huonosti toteutettu roskien kerääjä voi kuluttaa sovellusresursseja liikaa. Tässä kolmannessa artikkelissa JVM: n suorituskyvyn optimointi -sarja, Eva Andreasson tarjoaa Java-aloittelijoille yleiskuvan Java-alustan muistimallista ja GC-mekanismista. Sitten hän selittää, miksi pirstoutuminen (eikä GC) on tärkein "gotcha!" Java-sovellusten suorituskyvystä ja miksi sukupolvien roskien kerääminen ja tiivistäminen ovat tällä hetkellä johtavia (vaikkakaan ei innovatiivisimpia) lähestymistapoja Java-sovellusten kasan pirstoutumisen hallintaan.

Roskakokoelma (GC) on prosessi, jonka tarkoituksena on vapauttaa varattu muisti, johon mikään tavoitettavissa oleva Java-objekti ei enää viittaa, ja se on olennainen osa Java-virtuaalikoneen (JVM) dynaamista muistinhallintajärjestelmää. Tyypillisessä roskien keräysjaksossa kaikki objektit, joihin vielä viitataan ja jotka ovat siten tavoitettavissa, pidetään. Aikaisemmin viitattujen objektien viemä tila vapautetaan ja otetaan takaisin käyttöön uusien objektien kohdentamisen mahdollistamiseksi.

Ymmärtääksesi roskien keräämisen ja erilaiset GC-lähestymistavat ja algoritmit sinun on ensin tiedettävä muutama asia Java-alustan muistimallista.

JVM: n suorituskyvyn optimointi: Lue sarja

  • Osa 1: Yleiskatsaus
  • Osa 2: Kääntäjät
  • Osa 3: Jätteiden keruu
  • Osa 4: Samanaikaisesti tiivistyvä GC
  • Osa 5: Skaalautuvuus

Roskakokoelma ja Java-alustan muistimalli

Kun määrität käynnistysvaihtoehdon -Xmx Java-sovelluksesi komentorivillä (esimerkiksi: java -Xmx: 2g MyApp) muisti on osoitettu Java-prosessille. Tätä muistia kutsutaan nimellä Java-kasa (tai vain pino). Tämä on oma muistiosoitetila, johon kaikki Java-ohjelman (tai joskus JVM) luomat objektit varataan. Kun Java-ohjelmasi jatkuu ja jakaa uusia objekteja, Java-kasa (eli osoiteavaruus) täyttyy.

Lopulta Java-kasa on täynnä, mikä tarkoittaa, että allokoiva säie ei löydä riittävän suurta peräkkäistä osaa vapaata muistia varatulle objektille. Siinä vaiheessa JVM päättää, että roskien keräys on tapahduttava, ja se ilmoittaa siitä keräilijälle. Roskakorin voi laukaista myös, kun Java-ohjelma soittaa System.gc (). Käyttämällä System.gc () ei takaa roskien keräystä. Ennen kuin roskat voidaan aloittaa, GC-mekanismi määrittää ensin, onko sen aloittaminen turvallista. Roskakorin aloittaminen on turvallista, kun kaikki sovelluksen aktiiviset säikeet ovat turvallisessa paikassa sen sallimiseksi, esim. yksinkertaisesti selitti, että olisi huono aloittaa roskien kerääminen keskellä käynnissä olevaa objektin allokointia tai suorittamalla optimoitujen suorittimen ohjeiden sarja (katso edellinen artikkeli kääntäjistä), koska saatat menettää kontekstin ja siten sekoittaa loppu tuloksia.

Jätteiden keräilijän pitäisi ei koskaan takaisin aktiivisesti viitattu objekti; se rikkoo Java-virtuaalikoneen määrittelyä. Jätteiden kerääjää ei myöskään vaadita keräämään kuolleita esineitä välittömästi. Kuolleet esineet kerätään lopulta seuraavien jätteiden keräysjaksojen aikana. Vaikka roskien keräystä voidaan toteuttaa monin tavoin, nämä kaksi oletusta ovat totta kaikille lajikkeille. Jätteiden keräämisen todellinen haaste on tunnistaa kaikki elävät (vielä viitatut) ja palauttaa kaikki rekisteröimättömät muistit, mutta tehdä se vaikuttamatta käynnissä oleviin sovelluksiin enemmän kuin tarpeen. Jätteenkerääjällä on siis kaksi tehtävää:

  1. Viittaamattoman muistin nopea vapauttaminen sovelluksen allokointinopeuden tyydyttämiseksi, jotta muisti ei loppu.
  2. Muistin palauttaminen vaikuttamalla minimaalisesti käynnissä olevan sovelluksen suorituskykyyn (esim. Viive ja läpäisykyky).

Kahdenlaisia ​​roskien keräys

Tämän sarjan ensimmäisessä artikkelissa käsittelin kahta pääasiallista lähestymistapaa roskien keräykseen, jotka ovat vertailulaskenta ja keräilijät. Tällä kertaa tarkastelen tarkemmin kutakin lähestymistapaa ja esitän sitten joitain algoritmeja, joita käytetään keräilijöiden jäljittämiseen tuotantoympäristöissä.

Lue JVM: n suorituskyvyn optimointisarja

  • JVM: n suorituskyvyn optimointi, osa 1: Yleiskatsaus
  • JVM: n suorituskyvyn optimointi, osa 2: kääntäjät

Referenssilaskurit

Referenssilaskurit Seuraa kuinka monta viittausta osoittaa jokaiselle Java-objektille. Kun objektin määrä muuttuu nollaksi, muisti voidaan heti palauttaa. Tämä välitön pääsy talteenotettuun muistiin on viitteiden laskentamenetelmän suurin etu roskakoriin. Viitteettömän muistin pitämisessä on hyvin vähän yleiskustannuksia. Kaikkien viitemäärien pitäminen ajan tasalla voi kuitenkin olla melko kallista.

Suurin vaikeus vertailulaskurien kanssa on referenssilukemien pitäminen tarkkoina. Toinen tunnettu haaste on pyöreiden rakenteiden käsittelyyn liittyvä monimutkaisuus. Jos kaksi kohdetta viittaa toisiinsa eikä mikään elävä esine viittaa niihin, heidän muistia ei koskaan vapauteta. Molemmat objektit pysyvät ikuisesti nollan ulkopuolella. Pyöreisiin rakenteisiin liittyvän muistin palauttaminen vaatii suuria analyysejä, mikä tuo kalliita yleiskustannuksia algoritmille ja siten sovellukselle.

Keräilijöiden jäljittäminen

Keräilijöiden jäljittäminen perustuvat oletukseen, että kaikki elävät kohteet voidaan löytää jäljittämällä iteratiivisesti kaikki viittaukset ja myöhemmät viitteet alkuperäisestä joukosta, jonka tiedetään olevan eläviä esineitä. Alkuperäinen joukko eläviä esineitä (kutsutaan pääobjektit tai vain juuret Lyhyesti sanottuna) sijoittuvat analysoimalla rekisterit, globaalit kentät ja pinokehykset sillä hetkellä, kun roskat kerätään. Kun alkuperäinen livejoukko on tunnistettu, jäljityskerääjä seuraa näiden objektien viitteitä ja asettaa ne jonoon, jotta ne merkitään eläviksi ja myöhemmin niiden viitteet jäljitetään. Merkitään kaikki löydetyt viitatut objektit elää tarkoittaa, että tunnettu livejoukko kasvaa ajan myötä. Tämä prosessi jatkuu, kunnes kaikki viitatut (ja siten kaikki elävät) objektit on löydetty ja merkitty. Kun jäljittelijä on löytänyt kaikki elävät kohteet, se palauttaa jäljellä olevan muistin.

Seurantakollektorit eroavat referenssilaskureista, koska ne pystyvät käsittelemään pyöreitä rakenteita. Useimpien jäljittävien keräilijöiden saalis on merkintävaihe, joka edellyttää odottamista, ennen kuin voidaan palauttaa viittaamaton muisti.

Seurantakeräimiä käytetään yleisimmin muistin hallintaan dynaamisilla kielillä; ne ovat ylivoimaisesti yleisimpiä Java-kielellä ja ne on todistettu kaupallisesti tuotantoympäristöissä monien vuosien ajan. Keskityn keräilijöiden jäljittämiseen loppuosan tästä artikkelista, alkaen joistakin algoritmeista, jotka toteuttavat tämän lähestymistavan roskien keräykseen.

Keräilijän algoritmien jäljittäminen

Kopiointi ja merkitse ja pyyhkäise roskien keräys ei ole uutta, mutta ne ovat silti kaksi yleisintä algoritmia, jotka toteuttavat roskien keräämisen nykyään.

Keräilijöiden kopiointi

Perinteiset kopiointikokoajat käyttävät a avaruudesta ja a avaruuteen - eli kasan kaksi erikseen määriteltyä osoiteavaruutta. Jätteiden keräyskohdassa avaruudesta määritellyn alueen elävät objektit kopioidaan seuraavaan käytettävissä olevaan tilaan avaruudeksi määritellyllä alueella. Kun kaikki avaruudesta tulevat elävät objektit siirretään pois, koko avaruudesta voidaan palauttaa. Kun varaaminen alkaa uudelleen, se alkaa ensimmäisestä vapaasta sijainnista avaruuteen.

Tämän algoritmin vanhemmissa toteutuksissa avaruudesta ja avaruuteen vaihdetaan paikkoja, mikä tarkoittaa, että kun avaruus on täynnä, roskien keräys käynnistyy uudelleen ja avaruudesta tulee avaruudesta, kuten kuvassa 1 on esitetty.

Kopiointialgoritmin nykyaikaisemmat toteutukset mahdollistavat kasan mielivaltaisten osoitetilojen osoittamisen avaruuteen ja avaruudesta. Näissä tapauksissa heidän ei tarvitse välttämättä vaihtaa sijaintia keskenään; pikemminkin jokaisesta tulee toinen osoitetila kasan sisällä.

Keräilijöiden kopioinnin yhtenä etuna on, että objektit allokoidaan tiiviisti avaruuteen, mikä eliminoi täysin pirstoutumisen. Hajanaisuus on yleinen asia, jota muut roskien keräysalgoritmit taistelevat. jotain, josta keskustelen myöhemmin tässä artikkelissa.

Keräilijöiden kopioinnin haitat

Keräilijöiden kopiointi on yleensä stop-the-world-keräilijät, mikä tarkoittaa, että mitään sovellustyötä ei voida suorittaa niin kauan kuin roskien keräys on käynnissä. Stop-the-world-toteutuksessa, mitä suurempi kopioitava alue on, sitä suurempi vaikutus on sovelluksesi suorituskykyyn. Tämä on haitta sovelluksille, jotka ovat herkkiä vasteajalle. Kopiointikeräilijän tulee ottaa huomioon myös pahin tapaus, kun kaikki elää avaruudesta. Sinun on aina jätettävä tarpeeksi päätä liikkuvien esineiden siirtämiseen, mikä tarkoittaa, että avaruuden on oltava riittävän suuri isännöimään kaikkea avaruudesta. Kopiointialgoritmi on muistin suhteen tehoton tästä rajoituksesta johtuen.

Keräilijät

Suurin osa yritystuotantoympäristöissä käytetyistä kaupallisista JVM: istä käyttää merkintöjä kerääviä keräilijöitä, joilla ei ole suorituskykyvaikutuksia kuin keräilijöiden kopioinnilla. Jotkut tunnetuimmista merkintäkerääjistä ovat CMS, G1, GenPar ja DeterministicGC (katso Resurssit).

A merkin ja pyyhkäisyn kerääjä jäljittää viitteet ja merkitsee jokaisen löydetyn objektin "elävällä" bitillä. Yleensä joukko bittiä vastaa osoitetta tai joissakin tapauksissa joukkoa osoitteita kasalla. Elävä bitti voidaan esimerkiksi tallentaa bitinä objektin otsikkoon, bittivektoriin tai bittikarttaan.

Kun kaikki on merkitty eläväksi, lakaistusvaihe käynnistyy. Jos keräilijällä on lakaistusvaihe, se sisältää periaatteessa jonkin mekanismin kasan kulkemiseen uudelleen (paitsi live-joukko, mutta koko kasan pituus) kaikkien merkitsemättömien paikantamiseksi. palat peräkkäisiä muistiosoitealueita. Merkitsemätön muisti on vapaata ja palautettavissa. Keräilijä linkittää sitten nämä merkitsemättömät palat organisoituihin ilmaisiin luetteloihin. Jätteenkerääjässä voi olla useita ilmaisia ​​luetteloita - yleensä ne on järjestetty palan koon mukaan. Jotkut JVM: t (kuten JRockit Real Time) toteuttavat keräilijöitä, joiden heuristiikka kerää dynaamisesti kokoalueiden luettelot sovellusten profilointitietojen ja objektikoko-tilastojen perusteella.

Kun pyyhkäisovaihe on valmis, kohdentaminen alkaa uudelleen. Uudet allokointialueet allokoidaan vapaista luetteloista, ja muistipalat voidaan sovittaa objektikokoihin, objektikoon keskiarvoihin ketjutunnusta kohden tai sovellukseen sovitettuihin TLAB-kokoihin. Vapaan tilan sovittaminen tarkemmin sovelluksen koon mukaan optimoi muistin ja voi auttaa vähentämään pirstoutumista.

Lisätietoja TLAB-koosta

TLAB- ja TLA (Thread Local Allocation Buffer or Thread Local Area) -osiointia käsitellään JVM: n suorituskyvyn optimoinnin osassa 1.

Merkin ja pyyhkäisyn keräilijöiden haitat

Merkintävaihe riippuu kasan elävien tietojen määrästä, kun taas pyyhkäisovaihe riippuu kasan koosta. Koska joudut odottamaan molempia merkki ja lakaista Vaiheet ovat valmiita muistin palauttamiseksi, tämä algoritmi aiheuttaa taukoaikahaasteita suuremmille kasoille ja suuremmille reaaliaikaisille tietojoukoille.

Yksi tapa auttaa paljon muistia vievissä sovelluksissa on käyttää GC-viritysvaihtoehtoja, jotka sopivat erilaisiin sovellustilanteisiin ja tarpeisiin. Viritys voi monissa tapauksissa ainakin auttaa lykkäämään kumpaakin näistä vaiheista siitä, ettei siitä tule riski sovelluksellesi tai palvelutasosopimuksillesi. (SLA määrittää, että sovellus täyttää tietyt sovelluksen vasteajat - eli viiveen.) Jokaisen kuormituksen muutoksen ja sovelluksen muokkauksen viritys on toistuva tehtävä, koska viritys on voimassa vain tietylle työmäärälle ja allokointinopeudelle.

Merkinnän ja pyyhkäisyn toteutukset

Markkinointi- ja pyyhkäisykeräyksen toteuttamiseksi on vähintään kaksi kaupallisesti saatavilla olevaa ja osoittautunutta lähestymistapaa. Yksi on rinnakkainen lähestymistapa ja toinen on samanaikainen (tai enimmäkseen samanaikainen) lähestymistapa.

Rinnakkaiskerääjät

Rinnakkaiskokoelma tarkoittaa, että prosessille osoitettuja resursseja käytetään samanaikaisesti roskien keräykseen. Suurin osa kaupallisesti toteutetuista rinnakkaiskerääjistä on monoliittisia maailman lopun keräilijöitä - kaikki sovellusketjut pysäytetään, kunnes koko roskien keräysjakso on valmis. Kaikkien säikeiden pysäyttäminen antaa mahdollisuuden käyttää kaikkia resursseja tehokkaasti rinnakkain roskien keräyksen loppuun saattamiseksi merkin ja lakaistusvaiheiden läpi. Tämä johtaa erittäin korkeaan tehokkuustasoon, mikä johtaa yleensä korkeisiin pisteisiin suorituskyvyn vertailuarvoilla, kuten SPECjbb. Jos suorituskyky on välttämätöntä sovelluksellesi, rinnakkainen lähestymistapa on erinomainen valinta.