Java >> Java opplæring >  >> Java

Java:Hvordan gjenbruk av objekter kan redusere ventetiden og forbedre ytelsen

Bli kjent med gjenbrukskunsten ved å lese denne artikkelen og lær fordeler og ulemper med ulike gjenbruksstrategier i en flertråds Java-applikasjon. Dette lar deg skrive mer effektiv kode med mindre ventetid.

Mens bruk av objekter i objektorienterte språk som Java gir en utmerket måte å abstrahere bort kompleksitet, kan hyppig objektskaping ha ulemper i form av økt minnepress og søppelsamling, noe som vil ha en negativ effekt på applikasjoners ventetid og ytelse .

Nøye gjenbruk av objekter gir en måte å opprettholde ytelsen samtidig som de fleste deler av det tiltenkte abstraksjonsnivået beholdes. Denne artikkelen utforsker flere måter å gjenbruke objekter på.

Problemet

Som standard vil JVM tildele nye objekter på heapen. Dette betyr at disse nye objektene vil samle seg på haugen og at plassen som er okkupert til slutt må gjenvinnes når objektene går utenfor rekkevidde (dvs. ikke refereres til lenger) i en prosess som kalles "Garbage Collection" eller GC for kort. Ettersom flere sykluser med å opprette og fjerne objekter passeres, blir minnet ofte stadig mer fragmentert.

Selv om dette fungerer bra for applikasjoner med lite eller ingen ytelseskrav, blir det en betydelig flaskehals i ytelsessensitive applikasjoner. For å gjøre ting verre, blir disse problemene ofte forverret i servermiljøer med mange CPU-kjerner og på tvers av NUMA-regioner.

Minnetilgangsforsinkelser

Tilgang til data fra hovedminnet er relativt treg (rundt 100 sykluser, så omtrent 30 ns på gjeldende maskinvare sammenlignet med sub ns-tilgang ved bruk av registre), spesielt hvis en minneregion ikke har vært aksessert på lenge (noe som fører til økt sannsynlighet for en TLB-miss eller til og med en sidefeil). Progresjon mot mer lokaliserte data som ligger i L3, L2, L1 CPU-cacher ned til selve CPU-registrene, forbedres ventetiden med størrelsesordener. Derfor blir det viktig å ha et lite fungerende sett med data.

Konsekvenser av minneforsinkelser og spredte data

Ettersom nye objekter blir opprettet på haugen, må CPU-ene skrive disse objektene i minneplasseringer som uunngåelig befinner seg lenger og lenger fra hverandre ettersom minne som ligger nær det opprinnelige objektet blir allokert. Dette er kanskje ikke et vidtrekkende problem under oppretting av objekter, da cache og TLB-forurensning vil bli spredt over tid og skape en statistisk rimelig jevnt fordelt ytelsesreduksjon i applikasjonen.

Men når disse objektene skal gjenvinnes, er det en minnetilgangs-"storm" opprettet av GC som får tilgang til store områder med urelatert minne over en kort periode. Dette ugyldiggjør effektivt CPU-cacher og metter minnebåndbredde, noe som resulterer i betydelige og ikke-deterministiske applikasjonsytelsesfall.

For å gjøre ting verre, hvis applikasjonen muterer minnet på en måte som GC ikke kan fullføre innen rimelig tid, vil noen GC gripe inn og stoppe alle applikasjonstråder slik at den kan fullføre oppgaven. Dette skaper enorme søknadsforsinkelser, potensielt i løpet av sekunder eller enda verre. Dette blir referert til som "stopp-verden-samlinger".

Forbedrede GC-er

De siste årene har det vært en betydelig forbedring i GC-algoritmer som kan dempe noen av problemene beskrevet ovenfor. Imidlertid er grunnleggende båndbreddebegrensninger for minnetilgang og problemer med uttømming av CPU-hurtigbuffer fortsatt en faktor når du lager enorme mengder nye objekter.

Å gjenbruke objekter er ikke enkelt

Etter å ha lest om problemene ovenfor, kan det se ut til at gjenbruk av gjenstander er en lavthengende frukt som enkelt kan plukkes etter eget ønske. Det viser seg at dette ikke er tilfelle da det er flere restriksjoner på gjenbruk av gjenstander.

Et objekt som er uforanderlig kan alltid gjenbrukes og overleveres mellom tråder, dette er fordi feltene er endelige og satt av konstruktøren som sikrer fullstendig synlighet. Så gjenbruk av uforanderlige objekter er enkelt og nesten alltid ønskelig, men uforanderlige mønstre kan føre til en høy grad av objektskaping.

Når en foranderlig forekomst er konstruert, krever imidlertid Javas minnemodell at normal lese- og skrivesemantikk skal brukes når man leser og skriver normale forekomstfelt (dvs. et felt som ikke er flyktig). Derfor er disse endringene garantert bare synlige for den samme tråden som skriver feltene.

Derfor, i motsetning til mange tro, vil det rett og slett ikke fungere å lage en POJO, sette noen verdier i en tråd og overføre den til en annen tråd. Mottakstråden ser kanskje ingen oppdateringer, kan se delvise oppdateringer (slik som de nedre fire bitene av en long ble oppdatert, men ikke de øverste) eller alle oppdateringer. For å gjøre lårene verre, kan endringene sees 100 nanosekunder senere, ett sekund senere, ellers kan de aldri bli sett i det hele tatt. Det er rett og slett ingen måte å vite det.

Ulike løsninger

En måte å unngå POJO-problemet på er å erklære primitive felt (som int og lange felt) for flyktige og bruke atomvarianter for referansefelt. Å erklære en matrise som flyktig betyr at bare selve referansen er flyktig og gir ikke flyktig semantikk til elementene. Dette kan løses, men den generelle løsningen er utenfor rammen av denne artikkelen, selv om Atomic*Array-klassene gir en god start. Å erklære alle felt flyktige og bruke samtidige innpakningsklasser kan medføre en viss ytelsesstraff.

En annen måte å gjenbruke objekter på er ved hjelp av ThreadLocal-variabler som vil gi distinkte og tidsinvariante forekomster for hver tråd. Dette betyr at normal ytelsesminnesemantikk kan brukes. I tillegg, fordi en tråd bare kjører kode sekvensielt, er det også mulig å gjenbruke det samme objektet i urelaterte metoder. Anta at en StringBuilder er nødvendig som en scratch-variabel i en rekke metoder (og deretter tilbakestille lengden på StringBuilder til null mellom hver bruk), så kan en ThreadLocal som har samme forekomst for en bestemt tråd gjenbrukes i disse urelaterte metodene (forutsatt at ingen metode kaller en metode som deler gjenbruken, inkludert selve metoden). Dessverre skaper mekanismen rundt å skaffe ThreadLocals indre instans en del overhead. Det er en rekke andre skyldige knyttet til bruken av kodedelte ThreadLocal-variabler som gjør dem:

  • Vanskelig å rydde opp etter bruk.
  • Mottakelig for minnelekkasjer.
  • Potensielt uskalerbar. Spesielt fordi Javas kommende virtuelle trådfunksjon fremmer å lage en enorm mengde tråder.
  • Konstituerer effektivt en global variabel for tråden.

Det kan også nevnes at en trådkontekst kan brukes til å holde gjenbrukbare objekter og ressurser. Dette betyr vanligvis at trådkonteksten på en eller annen måte vil bli eksponert i APIen, men resultatet er at det gir rask tilgang til gjenbrukte gjenbrukte gjenstander. Fordi objekter er direkte tilgjengelige i trådkonteksten, gir det en mer grei og deterministisk måte å frigjøre ressurser på. For eksempel når trådkonteksten er lukket.

Til slutt kan konseptet ThreadLocal og trådkontekst blandes, og gir et ubesmittet API samtidig som det gir forenklet ressursrensing og dermed unngå minnelekkasjer.

Det bør bemerkes at det er andre måter å sikre minnekonsistens på. For eksempel ved å bruke den kanskje mindre kjente Java-klassen Exchanger. Sistnevnte tillater utveksling av meldinger hvorved det er garantert at alle minneoperasjoner utført av fra-tråden før utvekslingen skjer-før enhver minneoperasjon i til-tråden.

Enda en måte er å bruke åpen kildekode Chronicle Queue som gir en effektiv, trådsikker, objektopprettingsfri måte å utveksle meldinger mellom tråder på.

I Chronicle Queue blir meldinger også bevart, noe som gjør det mulig å spille av meldinger fra et bestemt punkt (f.eks. fra begynnelsen av køen) og å rekonstruere tilstanden til en tjeneste (her blir en tråd sammen med dens tilstand referert til som en service). Hvis det oppdages en feil i en tjeneste, kan denne feiltilstanden gjenopprettes (for eksempel i feilsøkingsmodus) ganske enkelt ved å spille av alle meldingene i inndatakøen(e). Dette er også veldig nyttig for testing der en rekke forhåndslagrede køer kan brukes som testinndata til en tjeneste.

Høyere ordens funksjonalitet kan oppnås ved å sette sammen en rekke enklere tjenester, som hver kommuniserer via en eller flere Chronicle Queue og produserer et utdataresultat, også i form av en Chronicle Queue.

Summen av dette gir en fullstendig deterministisk og frakoblet hendelsesdrevet mikrotjenesteløsning.

Gjenbruk av objekter i Chronicle Queue

I en tidligere artikkel ble Chronicle Queue med åpen kildekode benchmarked og demonstrert å ha høy ytelse. Et mål med denne artikkelen er å se nærmere på hvordan dette er mulig og hvordan gjenbruk av gjenstander fungerer under panseret i Chronicle Queue (ved bruk av versjon 5.22ea6).

Som i forrige artikkel er det samme enkle dataobjektet brukt:

public class MarketData extends SelfDescribingMarshallable {
    int securityId;
    long time;
    float last;
    float high;
    float low;


    // Getters and setters not shown for brevity

}

Ideen er å lage et objekt på øverste nivå som gjenbrukes når et stort antall meldinger legges til en kø og deretter analysere intern objektbruk for hele stabelen når denne koden kjøres:

public static void main(String[] args) {

    final MarketData marketData = new MarketData();

    final ChronicleQueue q = ChronicleQueue

            .single("market-data");

    final ExcerptAppender appender = q.acquireAppender();


    for (long i = 0; i < 1e9; i++) {

        try (final DocumentContext document =

                     appender.acquireWritingDocument(false)) {

             document

                    .wire()

                    .bytes()

                    .writeObject(MarketData.class, 

                            MarketDataUtil.recycle(marketData));


        }

    }

}

Siden Chronicle Queue serialiserer objektene til minnetilordnede filer, er det viktig at den ikke oppretter andre unødvendige objekter av ytelsesgrunnene som er angitt ovenfor.

Minnebruk

Applikasjonen startes med VM-alternativet "-verbose:gc", slik at eventuelle potensielle GC-er tydelig kan detekteres ved å observere standardutgangen. Når applikasjonen starter, dumpes et histogram over de mest brukte objektene etter at de første 100 millioner meldingene er satt inn:

pemi@Pers-MBP-2 queue-demo % jmap -histo 8536


 num     #instances         #bytes  class name

----------------------------------------------

   1:         14901       75074248  [I

   2:         50548       26985352  [B

   3:         89174        8930408  [C

   4:         42355        1694200  java.util.HashMap$KeyIterator

   5:         56087        1346088  java.lang.String

…

2138:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl

Total        472015      123487536

Etter at applikasjonen la til omtrent 100 millioner ekstra meldinger noen sekunder senere, ble det laget en ny dump:

pemi@Pers-MBP-2 queue-demo % jmap -histo 8536


 num     #instances         #bytes  class name

----------------------------------------------

   1:         14901       75014872  [I

   2:         50548       26985352  [B

   3:         89558        8951288  [C

   4:         42355        1694200  java.util.HashMap$KeyIterator

   5:         56330        1351920  java.lang.String

…

2138:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl

Total        473485      123487536

Som man kan se, var det kun en liten økning i antall tildelte objekter (rundt 1500 objekter), noe som indikerer at ingen objekttildeling ble gjort per sendt melding. Ingen GC ble rapportert av JVM, så ingen gjenstander ble samlet inn under prøvetakingsintervallet.

Å designe en så relativt kompleks kodebane uten å lage noe objekt mens man tar i betraktning alle begrensningene ovenfor er selvfølgelig ikke-trivielt og indikerer at biblioteket har nådd et visst modenhetsnivå når det gjelder ytelse.

Profileringsmetoder

Profileringsmetoder kalt under utførelse avslører Chronicle Queue bruker ThreadLocal-variabler:

Den bruker omtrent 7 % av tiden på å finne trådlokale variabler via

ThreadLocal$ThreadLocalMap.getEntry(ThreadLocal) metoden, men dette er vel verdt innsatsen sammenlignet med å lage objekter i farten.

Som man kan se, bruker Chronicle Queue mesteparten av tiden sin på å få tilgang til feltverdier i POJO som skal skrives til køen ved hjelp av Java-refleksjon. Selv om det er en god indikator på at den tiltenkte handlingen (dvs. kopiering av verdier fra en POJO til en kø) vises et sted nær toppen, er det måter å forbedre ytelsen enda mer ved å tilby håndlagde metoder for serialisering som reduserer utførelsestiden betydelig. Men det er en annen historie.

Hva er det neste?

Når det gjelder ytelse, er det andre funksjoner som å kunne isolere CPU-er og låse Java-tråder til disse isolerte CPU-ene, noe som reduserer applikasjonsjitter betraktelig samt å skrive tilpassede serialiseringsprogrammer.

Til slutt er det en bedriftsversjon med replikering av køer på tvers av serverklynger som baner vei mot høy tilgjengelighet og forbedret ytelse i distribuerte arkitekturer. Enterprise-versjonen inkluderer også et sett med andre funksjoner som kryptering, tidssonerulling og asynkron meldingshåndtering.

Ressurser

Chronicle Queue (åpen kildekode)
Chronicle hjemmeside

Chronicle Queue Enterprise

Java Tag