Java >> Java tutorial >  >> Java

Brug af virtuel hukommelse fra Java under Linux, for meget hukommelse brugt

Dette har været en langvarig klage med Java, men det er stort set meningsløst og normalt baseret på at se på de forkerte oplysninger. Den sædvanlige formulering er noget i retning af "Hello World på Java tager 10 megabyte! Hvorfor har den brug for det?" Nå, her er en måde at få Hello World på en 64-bit JVM til at hævde at overtage 4 gigabyte ... i det mindste ved én form for måling.

java -Xms1024m -Xmx4096m com.example.Hello

Forskellige måder at måle hukommelse på

På Linux giver den øverste kommando dig flere forskellige numre til hukommelsen. Her er, hvad der står om Hello World-eksemplet:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 2120 kgregory  20   0 4373m  15m 7152 S    0  0.2   0:00.10 java
  • VIRT er det virtuelle hukommelsesrum:summen af ​​alt i det virtuelle hukommelseskort (se nedenfor). Det er stort set meningsløst, undtagen når det ikke er det (se nedenfor).
  • RES er den faste sætstørrelse:antallet af sider, der aktuelt findes i RAM. I næsten alle tilfælde er dette det eneste tal, du skal bruge, når du siger "for stort". Men det er stadig ikke et særlig godt tal, især når man taler om Java.
  • SHR er mængden af ​​resident hukommelse, der deles med andre processer. For en Java-proces er dette typisk begrænset til delte biblioteker og hukommelseskortede JAR-filer. I dette eksempel havde jeg kun én Java-proces kørende, så jeg formoder, at 7k er et resultat af biblioteker brugt af OS.
  • SWAP er ikke slået til som standard og vises ikke her. Det angiver mængden af ​​virtuel hukommelse, der aktuelt findes på disken, uanset om den faktisk er i swap-pladsen eller ej . Operativsystemet er meget godt til at holde aktive sider i RAM, og de eneste midler til at bytte er (1) køb mere hukommelse eller (2) reducer antallet af processer, så det er bedst at ignorere dette tal.

Situationen for Windows Task Manager er lidt mere kompliceret. Under Windows XP er der kolonner "Hukommelsesbrug" og "Virtuel hukommelsesstørrelse", men den officielle dokumentation er tavs om, hvad de betyder. Windows Vista og Windows 7 tilføjer flere kolonner, og de er faktisk dokumenterede. Af disse er "Working Set"-målingen den mest nyttige; det svarer nogenlunde til summen af ​​RES og SHR på Linux.

Forstå det virtuelle hukommelseskort

Den virtuelle hukommelse, der forbruges af en proces, er summen af ​​alt, hvad der er i proceshukommelseskortet. Dette inkluderer data (f.eks. Java-heapen), men også alle de delte biblioteker og hukommelseskortede filer, der bruges af programmet. På Linux kan du bruge kommandoen pmap til at se alle de ting, der er kortlagt i procesrummet (herfra og ud vil jeg kun henvise til Linux, fordi det er det, jeg bruger; jeg er sikker på, at der er tilsvarende værktøjer til Windows). Her er et uddrag fra hukommelseskortet for programmet "Hello World"; hele hukommelseskortet er over 100 linjer langt, og det er ikke usædvanligt at have en liste med tusind linjer.

0000000040000000     36K r-x--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000      8K rwx--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000    676K rwx--    [ anon ]
00000006fae00000  21248K rwx--    [ anon ]
00000006fc2c0000  62720K rwx--    [ anon ]
0000000700000000 699072K rwx--    [ anon ]
000000072aab0000 2097152K rwx--    [ anon ]
00000007aaab0000 349504K rwx--    [ anon ]
00000007c0000000 1048576K rwx--    [ anon ]
...
00007fa1ed00d000   1652K r-xs-  /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000   1024K rwx--    [ anon ]
00007fa1ed2d3000      4K -----    [ anon ]
00007fa1ed2d4000   1024K rwx--    [ anon ]
00007fa1ed3d4000      4K -----    [ anon ]
...
00007fa1f20d3000    164K r-x--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000   1020K -----  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000     28K rwx--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000     16K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3837000      4K rwx--  /lib/x86_64-linux-gnu/libc-2.13.so
...

En hurtig forklaring af formatet:hver række starter med segmentets virtuelle hukommelsesadresse. Dette efterfølges af segmentets størrelse, tilladelser og kilden til segmentet. Dette sidste element er enten en fil eller "anon", som angiver en hukommelsesblok tildelt via mmap.

Startende fra toppen har vi

  • JVM-indlæseren (dvs. det program, der køres, når du skriver java ). Dette er meget lille; alt det gør, er at indlæse i de delte biblioteker, hvor den rigtige JVM-kode er gemt.
  • En flok anon-blokke, der indeholder Java-heapen og interne data. Dette er en Sun JVM, så bunken er opdelt i flere generationer, som hver er sin egen hukommelsesblok. Bemærk, at JVM tildeler virtuel hukommelsesplads baseret på -Xmx værdi; dette giver den mulighed for at have en sammenhængende bunke. -Xms værdi bruges internt til at sige, hvor meget af heapen der er "i brug", når programmet starter, og til at udløse skraldindsamling, når denne grænse nærmer sig.
  • En hukommelseskortlagt JAR-fil, i dette tilfælde filen, der indeholder "JDK-klasserne." Når du hukommelseskorter en JAR, kan du få adgang til filerne i den meget effektivt (i forhold til at læse den fra starten hver gang). Sun JVM vil hukommelseskorte alle JAR'er på klassestien; hvis din applikationskode skal have adgang til en JAR, kan du også hukommelseskorte den.
  • Data pr. tråd for to tråde. 1M blokken er trådstakken. Jeg havde ikke en god forklaring på 4k-blokken, men @ericsoe identificerede den som en "vagtblokering":den har ikke læse-/skrivetilladelser, så den vil forårsage en segmentfejl, hvis den åbnes, og JVM'en fanger det og oversætter det til en StackOverFlowError . For en rigtig app vil du se dusinvis, hvis ikke hundredvis, af disse poster gentaget gennem hukommelseskortet.
  • Et af de delte biblioteker, der har den faktiske JVM-kode. Der er flere af disse.
  • Det delte bibliotek for C-standardbiblioteket. Dette er blot en af ​​mange ting, som JVM indlæser, og som ikke er en del af Java.

De delte biblioteker er særligt interessante:hvert delt bibliotek har mindst to segmenter:et skrivebeskyttet segment, der indeholder bibliotekskoden, og et læse-skrive-segment, der indeholder globale data pr. proces for biblioteket (jeg ved ikke, hvad segment uden tilladelser er; Jeg har kun set det på x64 Linux). Den skrivebeskyttede del af biblioteket kan deles mellem alle processer, der bruger biblioteket; for eksempel libc har 1,5 M virtuel hukommelsesplads, der kan deles.

Hvornår er den virtuelle hukommelsesstørrelse vigtig?

Det virtuelle hukommelseskort indeholder en masse ting. Noget af det er skrivebeskyttet, noget af det er delt, og noget af det er allokeret, men aldrig rørt (f.eks. næsten hele 4Gb-bunken i dette eksempel). Men operativsystemet er smart nok til kun at indlæse, hvad det har brug for, så størrelsen af ​​den virtuelle hukommelse er stort set irrelevant.

Hvor den virtuelle hukommelsesstørrelse er vigtig, er hvis du kører på et 32-bit operativsystem, hvor du kun kan tildele 2 Gb (eller i nogle tilfælde 3 Gb) procesadresseplads. I så fald har du at gøre med en knap ressource, og du bliver måske nødt til at foretage afvejninger, såsom at reducere din heap-størrelse for at hukommelseskortlægge en stor fil eller oprette en masse tråde.

Men i betragtning af at 64-bit maskiner er allestedsnærværende, tror jeg ikke, der går lang tid, før Virtual Memory Size er en fuldstændig irrelevant statistik.

Hvornår er størrelsen på beboerindstillingen vigtig?

Resident Set-størrelse er den del af den virtuelle hukommelsesplads, der faktisk er i RAM. Hvis din RSS vokser til at være en betydelig del af din samlede fysiske hukommelse, er det måske på tide at begynde at bekymre dig. Hvis din RSS vokser til at optage al din fysiske hukommelse, og dit system begynder at skifte, er det godt nok tid til at begynde at bekymre sig.

Men RSS er også vildledende, især på en let belastet maskine. Operativsystemet bruger ikke mange kræfter på at genvinde de sider, der bruges af en proces. Der er ringe fordele ved at gøre det, og potentialet for en dyr sidefejl, hvis processen berører siden i fremtiden. Som følge heraf kan RSS-statistikken omfatte mange sider, der ikke er i aktiv brug.

Bundlinje

Medmindre du bytter, skal du ikke blive alt for bekymret over, hvad de forskellige hukommelsesstatistikker fortæller dig. Med det forbehold, at en stadigt voksende RSS kan indikere en form for hukommelseslækage.

Med et Java-program er det langt vigtigere at være opmærksom på, hvad der sker i dyngen. Den samlede mængde plads, der forbruges, er vigtig, og der er nogle trin, du kan tage for at reducere det. Vigtigere er mængden af ​​tid, du bruger på affaldsindsamling, og hvilke dele af dyngen, der bliver indsamlet.

Adgang til disken (dvs. en database) er dyrt, og hukommelsen er billig. Hvis du kan bytte det ene til det andet, så gør det.


Der er et kendt problem med Java og glibc>=2.10 (inkluderer Ubuntu>=10.04, RHEL>=6).

Kuren er at indstille denne env. variabel:

export MALLOC_ARENA_MAX=4

Hvis du kører Tomcat, kan du tilføje dette til TOMCAT_HOME/bin/setenv.sh fil.

For Docker, føj dette til Dockerfile

ENV MALLOC_ARENA_MAX=4

Der er en IBM-artikel om indstilling af MALLOC_ARENA_MAXhttps://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

Dette blogindlæg siger

beboerhukommelse har været kendt for at krybe på en måde, der ligner hukommelseslækage eller hukommelsesfragmentering.

Der er også en åben JDK-fejl JDK-8193521 "glibc spilder hukommelse med standardkonfiguration"

søg efter MALLOC_ARENA_MAX på Google eller SO for flere referencer.

Du vil måske også justere andre malloc-indstillinger for at optimere til lav fragmentering af allokeret hukommelse:

# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536

Mængden af ​​hukommelse, der er allokeret til Java-processen, er stort set på niveau med, hvad jeg ville forvente. Jeg har haft lignende problemer med at køre Java på indlejrede/hukommelsesbegrænsede systemer. Kører enhver applikation med vilkårlige VM-grænser eller på systemer, der ikke har tilstrækkelige mængder swap, har en tendens til at bryde. Det ser ud til at være karakteren af ​​mange moderne apps, der ikke er designet til brug på ressourcebegrænsede systemer.

Du har et par flere muligheder, du kan prøve og begrænse din JVM's hukommelsesfodaftryk. Dette kan reducere den virtuelle hukommelses fodaftryk:

-XX:ReservedCodeCacheSize=32m Reserveret kodecachestørrelse (i bytes) - maksimal kodecachestørrelse. [Solaris 64-bit, amd64 og -server x86:48m; i1.5.0_06 og tidligere, Solaris 64-bit og and64:1024m.]

-XX:MaxPermSize=64m Størrelse af den permanente generation. [5.0 og nyere:64 bit VM'er skaleres 30 % større; 1.4amd64:96m; 1.3.1 -klient:32m.]

Du bør også indstille din -Xmx (max heap-størrelse) til en værdi så tæt som muligt på den faktiske maksimale hukommelsesbrug af din ansøgning. Jeg tror, ​​at standardadfærden for JVM stadig er dobbelt bunkestørrelsen hver gang den udvider den op til max. Hvis du starter med 32 mio. heap, og din app toppede til 65 mio., ville heapen ende med at vokse 32 mio. -> 64 mio. -> 128 mio.

Du kan også prøve dette for at gøre VM'en mindre aggressiv med hensyn til at dyrke bunken:

-XX:MinHeapFreeRatio=40 Minimum procentdel af heap fri efter GC for at undgå ekspansion.

Også, efter hvad jeg husker fra at eksperimentere med dette for et par år siden, havde antallet af indlæste biblioteker en enorm indflydelse på minimumsfodaftrykket. Indlæser java.net.Socket tilføjet mere end 15M, hvis jeg husker korrekt (og det gør jeg nok ikke).


Java tag