Java >> Java Program >  >> JVM

JVM Sophämtning och optimeringar

Översikt

Vid felsökning av system för prestandarelaterade problem är minnesoptimeringar en plats som behöver en djupgående analys av vad varje system lagrar i minnet, hur länge de lagras och åtkomstmönster. Det här inlägget är för att hålla en anteckning om bakgrundsinformation och värdefulla punkter att notera i en sådan ansträngning, specifik för Java-baserade implementeringar, eftersom en djup förståelse av JVM-beteenden är mycket fördelaktig i processen.

Java-språket ger mycket bekvämlighet för utvecklarna genom att ta hand om minneshanteringen i hög grad och låta fokus ligga på resten av logiken. Fortfarande ha en god förståelse för hur Java gör detta under, rationalisera flera bästa praxis vi följer i Java-implementationer och hjälpa till att designa programmen bättre och tänka allvarligt på vissa aspekter som senare kan leda till minnesläckor och systemstabilitet på lång sikt. Java Garbage Collector har en stor roll i detta varit ansvarig för att frigöra minne genom att ta bort minnesskräp.

JVM

Denna information är allmänt tillgänglig, men jag sammanfattar här för referens på ett ställe. :)

JVM gör att Java-kod kan köras på ett hårdvaru- och OS-oberoende sätt. Det fungerar på minnesplatser som tilldelats för egen process av operativsystemet och fungerar som en annan abstraktion av en fysisk maskin.

JVM:er kan implementeras baserat på den öppna standarden som publicerades på [1], allmänt kända implementeringar är Oracle Hotspot JVM, nästan samma öppen källkodsversion OpenJDK, IBM J9, JRockit och Dalvik VM som används i Android OS med vissa avvikelser.

Kort sagt, JVM laddar och exekverar kompilerad Java-byte-kod med hjälp av de resurser som tilldelats den från plattformen, den körs vidare.

JVM-struktur

ClassLoaders

laddar bytekoden i JVM-minnet (ladda, länk (verifiera, förbered, lösa –> om misslyckats NoClassDef found undantag utfärdas), initiera) Bootstrap-klassladdare, Extension-klassladdare, Application class-loaders

Minnes- och körtidsdataområde

Detta fångar några viktiga avsnitt nedan, även om det inte är heltäckande.

  • Native method stack – den inbyggda java-biblioteksstacken som är plattformsberoende, mestadels skriven på C-språk.
  • JVM-stacken (den för närvarande körande metoden stackspårning behålls, per tråd. Rekursiva metodanrop kan göra att stacken fylls och svämmar över (java.lang.StackOverFlowError) om korrekta brytningar inte ställs in. -Xss JVM-alternativet gör det möjligt att konfigurera stackstorleken.), PC-register (programräknare, pekar på nästa instruktion som ska köras per tråd. )
  • Metodområde (lagrar klassdata, storlek styrs av XX:MaxPermSize , PermGen-utrymme 64MB standard, om det ska tjäna enorma serverappar som laddar miljontals klasser, så kan vi ställa in genom att öka för att undvika problem med OOM:PermGen-utrymme. Från Java 8 och framåt kallas detta PermGen-utrymme som Metaspace utan gräns i java8 som standard även om det är tillåtet att finjusteras och begränsa), Heap(Xms, Xmx), Körtidskonstantpool

Exekveringsmotor

Denna motor exekverar bytekoden som tilldelas körtidsdataområdena genom klassladdaren. Den använder sig av tolk, garbage Collector, Hotspot-profilerare, JIT-kompilator för optimerad exekvering av programmet.

Se [2] för mer information om JVM-arkitekturen.

Nu vet vi var Garbage Collector sitter i JVM-arkitekturen. Låt oss gå djupt in i det inre.

Sopsamlare

Det är den automatiska Java-minneshanteringsprocessen som tar bort de objekt som inte används längre. Sedan kommer frågan, hur avgör det om föremålet används eller inte.

Den definierar två kategorier av objekt som,

levande objekt – nåbara objekt som refereras från ett annat objekt. I slutändan kommer referenslänkarna att nå roten som är huvudtråden som skapar hela objektgrafen.

döda föremål – oåtkomliga föremål som inte refereras av någon annan som bara ligger i högen.

denna kategorisering och sophämtning bygger på två fakta enligt nedan.

1. De flesta av föremålen blir snart oåtkomliga efter skapandet. Mest de kortlivade objekten som endast lever i ett metodsammanhang.

2. Gamla föremål hänvisar sällan till unga föremål. Till exempel skulle en långlivad cache knappast referera ett nyare objekt från den.

Steg för sophämtning

Nyskapade objektinstanser finns i Java-högen, som går till olika generationer som visas nedan. Sophämtning görs av en demontråd som heter "Garbage Collector" som leder objekten genom olika utrymmen i högen.

Sophämtning sker i 3 steg.

1. Markera – Börja från roten och gå igenom objektgrafen och markera de nåbara objekten som levande.

2. Svep – Ta bort de omarkerade objekten.

3. Kompakt – Defragmentera minnet så att allokeringarna hänger samman för de levande objekten. Det anses vara den process som tar mest tid.

Högområdet är uppdelat enligt nedan.

Gammal (fastställd) generation – Föremål som överlevt länge, stanna här tills det blir markerat som oåtkomligt och städas upp i en stor sophämtning som går genom hela högen.

Ung generation – detta är ytterligare uppdelat i 3 som Eden space och 2 Survivor spaces.

Sophämtning i två steg som "Minor" eller "Major". Båda dessa skräpinsamlingar är stop-the-world-operationer som stoppar varannan minnesåtkomst. Minor GC kanske inte känns av applikationen men eftersom den bara skannar genom den unga generationens utrymme kommer att vara liten i storlek.

Sopsamlare

Minnets livscykel går enligt nedan som visas i ovanstående animation.

1. Nyskapade objekt finns i Eden-utrymmet. (Precis som människor började från Edens trädgård :) ) Tills Edens utrymme blir fullt, fortsätter det att lägga till nya föremål där.

2. När Eden-utrymmet är fullt, springer en mindre GC, markerar de levande objekten, flyttar dessa levande objekt till 'Survivor from'-rymden och sveper Eden-utrymmet som blir ledigt.

3. Sedan fortsätter den att fylla Eden-utrymmet med nya objekt medan programmet körs. Nu när Eden-utrymmet är fullt, har vi tidigare också flyttat objekt i 'Survivor from'-utrymmet. Minor GC kör och markerar objekt i båda dessa utrymmen, flytta de återstående levande objekten som helhet till det andra överlevande utrymmet. Undrar varför inte kopiera de levande objekten från Eden-rymden till det återstående utrymmet i "Survivor from" istället för att flytta alla till det andra överlevandeutrymmet? Tja, att flytta alla till den andra har visat sig vara effektivare i kompakt steg över att komprimera området med föremål i det.

4. Denna cykel kommer att upprepa rörliga objekt mellan de överlevande utrymmena tills en konfigurerad tröskel (-XX:MaxTenuringThreshold ) är mött. (Den håller reda på hur många antal GC-cykler som har överlevts av varje objekt). När tröskeln är uppfylld kommer dessa objekt att flyttas till det upplåtna utrymmet.

5. Allteftersom tiden går, om det fasta utrymmet också fylls upp, kickar den stora GC in och går igenom hela Heap-minnesutrymmet och utför GC-stegen. Denna paus kan kännas i mänsklig interaktion och är inte önskvärd.

När det finns en minnesläcka eller stora cacher som finns kvar under lång tid, fylls det angivna utrymmet upp med tiden. Vid sådana tillfällen kanske dessa föremål inte ens upptäcks som döda. Detta resulterar i att stora GC:er körs ofta eftersom det upptäcker att det angivna utrymmet är fullt, men det misslyckas med att rensa upp tillräckligt med minne eftersom ingenting kan sopas ut.

Det här felet "java.lang.OutOfMemoryError" i loggarna skulle tydligt antyda oss när minnet inte räcker till. Även om vi ser frekventa CPU-vandringar med hög minnesanvändning, kan det vara ett symptom på frekvent GC-körning på grund av någon form av minneshanteringsproblem som kräver uppmärksamhet.

Prestanda

När man fokuserar på JVM-finjustering med fokus på minnesanvändning, är den viktigaste avgörande faktorn vad som är mer kritiskt från Responsivitet/latens och Genomströmning . Om genomströmningen är av yttersta vikt som vid batchbearbetning, kan vi kompromissa med att ha några pauser för stora GC att köra, om det hjälper den totala genomströmningen. Eftersom applikationen ibland blir mindre responsiv kanske inte är ett problem där.

Å andra sidan, om lyhördhet är av yttersta vikt som i en UI-baserad applikation, bör vi försöka undvika stora GC. Att göra detta skulle dock inte hjälpa. Till exempel kan vi försena ett stort GC genom att öka utrymmet för den unga generationen. Men då skulle den mindre GC-en börja ta mycket tid eftersom den behöver korsa och kompaktera ett stort utrymme nu. Därför har rätt storlek, det korrekta förhållandet mellan unga och gamla generationer måste göras noggrant för att uppnå detta. Ibland kan detta till och med gå in i applikationsdesigndetaljerna för att finjustera minnesanvändningen med objektskapande mönster och cachingplatser. Det kommer att bli ett ämne för ett annat inlägg för att analysera högdumparna och flamgraferna för att bestämma de bästa sakerna som ska cachelagras.

Sopsamlare

Eftersom sophämtningens roll har så stor inverkan på en applikations prestanda, har ingenjörerna lagt ner så mycket ansträngningar på att förbättra den. Resultatet är att vi kan välja den bästa sophämtaren att använda enligt kraven. Nedan finns en icke heltäckande lista över alternativ.

1. Seriesamlare

Går i en enda tråd. Endast lämplig för grundläggande applikationer.

2. Concurrent Collector (CMS – Concurrent Mark and Sweep)

En enda tråd utför sophämtning. Det stoppar bara världen i markerings- och ommarkeringsfas. Resten av arbetet görs medan applikationen körs och väntar inte på att den gamla generationen ska vara full. Detta är ett bra val när minnesutrymmet är stort, har ett stort antal processorer för att kunna köras samtidigt, och när applikationen kräver de kortaste pauserna med respons som den kritiska faktorn. Detta har varit det mest gynnade i de flesta webbapplikationer tidigare.

3. Parallellsamlare

Denna samlare använder flera processorer. Den väntar på att den gamla generationen ska vara full eller nästan full, men när den körs stoppar den världen. Flera trådar gör markeringen, sveper och komprimerar vilket gör att sophämtningen går mycket snabbare. När minnet inte är särskilt stort och antalet processorer är begränsat är detta ett bra alternativ för att tillgodose krav på genomströmning som tål pauser.

4. G1(Garbage First) samlare (1,7 uppåt)

Det här alternativet förbättrar sophämtningen så att den blir mer förutsägbar genom att tillåta konfigurationer som paustid när GC körs. Det sägs ha fördelen av båda världarna av parallellism och samtidighet. Den delar upp minnet i regioner och varje region betraktas som antingen en Eden, en överlevande eller ett fast utrymme. Om regionen har fler oåtkomliga objekt samlas den regionen först.

Standard garbage Collector i versioner

  • Java 7 – Parallell GC
  • Java 8 – Parallell GC
  • Java 9 – G1 GC
  • Java 10 – G1 GC
  • Java 11 – G1 GC (ZGC tillhandahålls som en experimentell funktion tillsammans med Epsilon)
  • Java 12 – G1 GC (Shenandoah GC introducerad. Endast OpenJDK.)

Tune-up parametrar för sopsamlaren

Tumregeln för att justera JVM är att inte göra det om det inte finns ett problem som ska åtgärdas med standardinställningarna eller beslutas efter mycket övervägande med bevisade effekter efter långvariga belastningsmönster på produktionsnivå. Detta beror på att Java Ergonomics har avancerat mycket och för det mesta skulle kunna utföra en hel del optimeringar om applikationen inte beter sig fult. En omfattande lista med alternativ finns på [5] inklusive konfigurering av storleken på högutrymmen, trösklar, typ av sophämtare som ska användas, etc.

Diagnostisera

Nedanstående konfigurationer är användbara för att diagnostisera minnesproblem med hjälp av GC-beteende utöver heap-dumparna.

-XX:-PrintGCDetails – Skriv ut information om sophämtning.
-Xloggc: – Skriv ut GC-loggningsdetaljer till en given fil.
-XX:-UseGCLogFileRotation – Aktivera rotation av GC-loggfil när ovanstående konfiguration är klar.
-XX:-HeapDumpOnOutOfMemoryError – Dumpa höginnehållet för vidare analys om ett OOM-fel uppstår.
-XX:OnOutOfMemoryError=”; – Uppsättning kommandon som ska köras om ett OOM-fel uppstår. Tillåter att utföra alla anpassade uppgifter när felet står inför.

Vi kommer att gå in på diagnosen och analysera detaljer i ett annat inlägg.

Skål![1] – https://docs.oracle.com/javase/specs/index.html
[2] – https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.6
[2] – Oracle Garbage Collection inställningsguide –
https://docs.oracle.com/javase/9/gctuning/ergonomics.htm#JSGCT-GUID-DB4CAE94-2041-4A16-90EC-6AE3D91EC1F1
[3] –  Nya java sophämtare –
https://blogs.oracle.com/javamagazine/understanding-the-jdks-new-superfast-garbage-collectors
[4] –  Tillgängliga samlare –
https://docs.oracle.com/en/java/javase/13/gctuning/available-collectors.html#GUID-F215A508-9E58-40B4-90A5-74E29BF3BD3C
[5] – JVM-alternativ –
https://www.oracle.com/technetwork/articles/java/vmoptions-jsp-140102.html

Java-tagg