Monessa ohjelmistoyrityksessä tulee ennemmin tai myöhemmin vastaan tilanne, jossa muuten hyvin toiminut Java-palvelu alkaa käyttäytyä oudosti. Muistia kuluu päivä päivältä enemmän. Roskienkeruu käy yhä useammin. Vastaukset hidastuvat. Lopulta palvelu käynnistyy uudelleen tai pahimmassa tapauksessa pysähtyy kokonaan.
Ensimmäinen reaktio on usein epäillä JVM:n asetuksia tai palvelimen resursseja. Muistia lisätään, prosessoritehoa kasvatetaan ja toivotaan parasta.
Usein ongelma ei kuitenkaan ratkea.
Syy löytyy monesti sovelluksesta itsestään. Jokin tieto jää muistiin pidemmäksi aikaa kuin oli tarkoitus. Yksittäinen objekti ei vielä aiheuta ongelmaa, mutta kuukausien aikana niitä kertyy tuhansia tai miljoonia.
Tätä kutsutaan muistivuodoksi.
Java-kehittäjille aihe voi tuntua hämmentävältä, koska kielessä on automaattinen roskienkeruu. Moni ajattelee, että muistivuodot kuuluvat C- tai C++-maailmaan, eivät Javaan.
Todellisuudessa Java-sovellukset kärsivät muistivuodoista yllättävän usein. Ero on vain siinä, että vuoto ei synny varaamattoman muistin vapauttamatta jättämisestä, vaan siitä, että ohjelma säilyttää viittauksia objekteihin, joita ei enää tarvittaisi.
Harvoin kukaan huomaa muistivuotoa ensimmäisenä koodista.
Yleensä huomio kiinnittyy oireisiin.
Palvelu, joka aiemmin vastasi nopeasti, alkaa ajoittain hidastua. Käyttökatkojen määrä kasvaa. Kontit käynnistyvät uudelleen. Muistinkulutus näyttää kasvavan päivä päivältä ilman selvää syytä.
Monessa tapauksessa ongelma havaitaan vasta silloin, kun valvontajärjestelmä lähettää hälytyksen.
Tyypillinen kehitysympäristö ei paljasta tällaisia ongelmia. Testipalvelu voi olla käynnissä muutaman tunnin tai päivän. Tuotannossa sama sovellus voi pyöriä kuukausia yhtäjaksoisesti ja käsitellä miljoonia tapahtumia.
Juuri silloin pienet virheet alkavat näkyä.
Muistan erään projektin, jossa palvelu toimi moitteettomasti testauksen aikana. Tuotannossa muistinkulutus kuitenkin nousi tasaisesti viikkojen ajan. Aluksi ero oli niin pieni, ettei siihen kiinnitetty huomiota. Kuukauden kuluttua JVM käytti lähes kaiken sille annetun muistin.
Lopulta syylliseksi paljastui välimuisti, josta tietoja ei koskaan poistettu.
Koodi vaikutti ensi silmäyksellä täysin järkevältä.
Toinen toistuva ongelma liittyy staattisiin muuttujiin.
Kun tieto tallennetaan static-kokoelmaan, sen elinkaari muuttuu. Data pysyy muistissa niin kauan kuin sovellus on käynnissä.
Tämä ei ole ongelma, jos tallennettava tieto on aidosti pysyvää.
Ongelma syntyy silloin, kun rakenteeseen alkaa kertyä käyttäjäkohtaisia tai tapahtumakohtaisia tietoja.
Moni kehittäjä on joskus lisännyt väliaikaisen ratkaisun tuotanto-ongelman ympärille ja ajatellut palaavansa asiaan myöhemmin. Sitten ratkaisu jää käyttöön vuosiksi.
Kun muistivuoto lopulta havaitaan, kukaan ei enää muista miksi kyseinen rakenne alun perin lisättiin.
Muistivuoto ei aina liity suuriin tietomääriin.
Joskus riittää, että jokin komponentti rekisteröi itsensä kuuntelijaksi mutta ei koskaan poista rekisteröintiä.
Tällöin alkuperäinen objekti pysyy muistissa, vaikka sitä ei enää käytettäisi.
Tämän tyyppiset vuodot voivat olla hankalia, koska ne eivät välttämättä näy heti muistinkulutuksessa. Sen sijaan järjestelmään kertyy vähitellen suuri määrä vanhoja instansseja.
Kun niitä alkaa olla tarpeeksi paljon, vaikutukset näkyvät suorituskyvyssä.
Ensimmäisellä kerralla heap dump voi näyttää pelottavalta.
Objekteja on valtava määrä ja näkymä täynnä teknisiä yksityiskohtia.
Kokeneet kehittäjät eivät yleensä etsi ensimmäisenä yksittäistä virhettä. He etsivät poikkeavaa käyttäytymistä.
Jos jokin luokka vie huomattavan paljon muistia, se herättää kysymyksiä.
Miksi näitä objekteja on näin paljon?
Miksi ne ovat edelleen olemassa?
Mikä pitää niihin viittausta yllä?
Usein vastaus löytyy viittausketjusta.
Yksi väärässä paikassa oleva kokoelma voi pitää muistissa tuhansia objekteja, joiden olisi pitänyt poistua jo kauan sitten.
VisualVM, Eclipse MAT, Java Flight Recorder ja monet muut työkalut ovat erittäin hyödyllisiä.
Silti ne ovat vain apuvälineitä.
Työkalu voi kertoa, että tiettyä objektia on miljoona kappaletta muistissa. Se ei kuitenkaan kerro, miksi sovellus on suunniteltu pitämään ne siellä.
Siksi muistivuotojen korjaaminen vaatii lähes aina myös sovelluksen liiketoimintalogiikan ymmärtämistä.
Pelkkä tekninen analyysi ei aina riitä.
Paras muistivuoto on sellainen, jota ei koskaan synny.
Käytännössä tämä tarkoittaa muutamia melko yksinkertaisia asioita.
Välimuisteille asetetaan kokorajat.
Tietojen vanhentuminen määritellään etukäteen.
Pitkäikäisiin kokoelmiin tallennetaan vain sellaista dataa, joka todella kuuluu sinne.
Kuormitustestit tehdään riittävän realistisilla datamäärillä.
Lisäksi muistinkulutusta seurataan jatkuvasti eikä vasta silloin, kun järjestelmä alkaa oireilla.
Moni kallis tuotanto-ongelma olisi havaittu paljon aikaisemmin, jos muistinkäytölle olisi ollut selkeät mittarit ja hälytysrajat.
Java-muistivuotojen hankalin puoli on se, että ne näyttävät usein aluksi täysin tavallisilta suorituskykyongelmilta. Palvelu hidastuu vähän kerrallaan, muistia kuluu hieman enemmän kuin ennen ja roskienkeruu tekee yhä enemmän töitä.
Siksi ongelmaa ei kannata lähestyä ensimmäisenä JVM-asetusten tai palvelinresurssien kautta.
Parempi lähtökohta on kysyä yksi yksinkertainen kysymys:
"Mikä tieto jää muistissa elämään pidempään kuin sen pitäisi?"
Kun siihen löytyy vastaus, myös varsinainen muistivuoto löytyy yleensä paljon nopeammin.