Neseniai aš padėjau sukurti „Java“ serverio programą, panašią į atminties duomenų bazę. Tai reiškia, kad mes pritaikėme daugybę duomenų talpykloje atmintyje, kad užklausa būtų labai greita.
Pradėję veikti prototipą, mes natūraliai nusprendėme peržiūrėti duomenų atminties pėdsaką, kai jis buvo išanalizuotas ir įkeltas iš disko. Tačiau nepatenkinami pradiniai rezultatai paskatino mane ieškoti paaiškinimų.
Pastaba: Šio straipsnio šaltinio kodą galite atsisiųsti iš išteklių.
Įrankis
Kadangi „Java“ tikslingai slepia daugelį atminties valdymo aspektų, reikia šiek tiek padirbėti norint sužinoti, kiek atminties sunaudoja jūsų objektai. Galite naudoti „Runtime.freeMemory“ ()
krūvos dydžio skirtumų matavimo metodas prieš ir po kelių objektų paskyrimo. Keletas straipsnių, pavyzdžiui, Ramchanderio Varadarajano „Savaitės klausimas Nr. 107“ („Sun Microsystems“, 2000 m. Rugsėjo mėn.) Ir Tony Sinteso „Atminties reikalai“ („JavaWorld“, 2001 m. Gruodžio mėn.). Deja, ankstesnio straipsnio sprendimas nepavyksta, nes įgyvendinant naudojamas neteisingas Veikimo laikas
metodo, o pastarojo straipsnio sprendimas turi savo trūkumų:
- Vienas skambutis
„Runtime.freeMemory“ ()
pasirodė nepakankama, nes JVM gali nuspręsti bet kuriuo metu padidinti dabartinį krūvos dydį (ypač kai jis vykdo šiukšlių surinkimą). Jei bendras krūvos dydis jau nėra didžiausias -Xmx dydis, turėtume naudotiRuntime.totalMemory () - Runtime.freeMemory ()
kaip naudojamas krūvos dydis. - Vykdant singlą
„Runtime.gc“ ()
skambutis gali pasirodyti nepakankamai agresyvus, norint pateikti prašymą dėl šiukšlių surinkimo. Mes, pavyzdžiui, galėtume paprašyti paleisti ir objektų užbaigėjus. Ir nuo to laiko„Runtime.gc“ ()
nėra dokumentais blokuojamas, kol kolekcija bus baigta, verta palaukti, kol suvokiamas krūvos dydis stabilizuosis. - Jei profiliuota klasė sukuria bet kokius statinius duomenis, vykdydama kiekvienos klasės inicijavimą (įskaitant statinius klases ir lauko inicializatorius), pirmosios klasės egzemplioriui naudojama kaupiamoji atmintis gali apimti tuos duomenis. Turėtume nepaisyti krūvos vietos, kurią sunaudoja pirmos klasės egzempliorius.
Atsižvelgdamas į tas problemas, pateikiu Dydis
, įrankis, su kuriuo aš žiūriu į įvairias „Java“ pagrindines ir programų klases:
viešoji klasė Sizeof {public static void main (String [] args) meta išimtį {// Sušildykite visas klases / metodus, kuriuos naudosime runGC (); usedMemory (); // Masyvas, kad būtų stiprios nuorodos į paskirtus objektus, galutinis int skaičius = 100000; Object [] objektai = new Object [count]; ilgas krūva1 = 0; // Paskirkite skaičių + 1 objektus, pirmąjį išmeskite (int i = -1; i = 0) objektams [i] = objektas; else {objektas = nulis; // Išmeskite sušilimo objektą runGC (); krūva1 = naudotaAtmintis (); // Padarykite prieš kaupą momentinę nuotrauką}} runGC (); ilgas krūva2 = naudotaAtmintis (); // Padarykite po kaupo momentinę nuotrauką: final int size = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'prieš' krūva:" + krūva1 + "," po "krūva:" + krūva2); System.out.println ("krūvos delta:" + (kaupas2 - kaupas1) + ", {" + objektai [0] .getClass () + "} dydis =" + dydis + "baitai"); for (int i = 0; i <count; ++ i) objektai [i] = null; objektai = nulis; } private static void runGC () meta išimtį {// Tai padeda iškviesti Runtime.gc () // naudojant kelis metodo iškvietimus: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () meta išimtį {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; for (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = naudotasMem1; usedMem1 = naudota atmintis (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Klasės pabaiga
Dydis
pagrindiniai metodai yra „runGC“ ()
ir usedMemory ()
. Aš naudoju a „runGC“ ()
vyniojimo būdas skambinti _runGC ()
kelis kartus, nes atrodo, kad metodas tampa agresyvesnis. (Nesu tikras, kodėl, bet įmanoma, sukūrus ir sunaikinus metodo skambučių kamino rėmą, pasikeičia pasiekiamumo šaknies rinkinys ir šiukšlių surinkėjas skatina dirbti daugiau. Be to, sunaudojant didelę dalį krūvos vietos sukuriama pakankamai darbo taip pat padeda šiukšlių surinkėjas. Apskritai sunku užtikrinti, kad viskas būtų surinkta. Tiksli informacija priklauso nuo JVM ir šiukšlių surinkimo algoritmo.)
Atidžiai atkreipkite dėmesį į vietas, į kurias aš kreipiuosi „runGC“ ()
. Galite redaguoti kodą tarp krūva1
ir krūva2
deklaracijos, norėdamos inicijuoti viską, kas įdomu.
Taip pat atkreipkite dėmesį, kaip Dydis
atspausdina objekto dydį: visiems reikalingas pereinamasis duomenų uždarymas suskaičiuoti
klasės egzemplioriai, padalyti iš suskaičiuoti
. Daugelio klasių rezultatas bus vienos klasės egzemplioriaus suvartota atmintis, įskaitant visus jai priklausančius laukus. Ta atminties pėdsako vertė skiriasi nuo daugelio komercinių profilių pateiktų duomenų, kurie praneša apie negilius atminties pėdsakus (pavyzdžiui, jei objekte yra int []
lauke, jo atminties sąnaudos bus rodomos atskirai).
Rezultatai
Taikykime šį paprastą įrankį kelioms klasėms, tada patikrinkime, ar rezultatai atitinka mūsų lūkesčius.
Pastaba: Šie rezultatai yra pagrįsti „Sun“ JDK 1.3.1, skirta „Windows“. Dėl to, ko garantuoja ir negarantuoja „Java“ kalba ir JVM specifikacijos, negalite pritaikyti šių konkrečių rezultatų kitoms platformoms ar kitoms „Java“ versijoms.
java.lang.Object
Na, visų objektų šaknis tiesiog turėjo būti mano pirmasis atvejis. Dėl java.lang.Object
, Gaunu:
„prieš“ krūva: 510696, „po“: krūva: 1310696 krūvos delta: 800000, {class java.lang.Object} dydis = 8 baitai
Taigi, lyguma Objektas
paima 8 baitus; žinoma, niekas neturėtų tikėtis, kad dydis bus 0, nes kiekvienas egzempliorius turi nešiotis laukus, palaikančius panašias bazines operacijas lygi ()
, hashCode ()
, laukti () / pranešti ()
, ir taip toliau.
java.lang.Integer
Mes su kolegomis dažnai vyniojame gimtąsias intai
į Sveikasis skaičius
egzempliorių, kad galėtume juos laikyti „Java“ kolekcijose. Kiek mums tai kainuoja atmintyje?
„prieš“ krūva: 510696, „po“: krūva: 2110696 krūvos delta: 1600000, {class java.lang.Integer} dydis = 16 baitų
16 baitų rezultatas yra šiek tiek blogesnis, nei tikėjausi, nes tarpt
vertė gali tilpti tik į 4 papildomus baitus. Naudojant Sveikasis skaičius
kainuoja 300 procentų atminties pridėtinių išlaidų, palyginti su tuo, kai galiu išsaugoti vertę kaip primityvų tipą.
java.lang.Long
Ilgas
turėtų užimti daugiau atminties nei Sveikasis skaičius
, bet tai nėra:
„prieš“ krūva: 510696, „po“: krūva: 2110696 krūvos delta: 1600000, {class java.lang.Long} dydis = 16 baitų
Akivaizdu, kad faktinis kaupo objektų dydis priklauso nuo žemo lygio atminties sulyginimo, kurį atlieka tam tikras JVM diegimas tam tikram procesoriaus tipui. Panašu, kad a Ilgas
yra 8 baitai Objektas
pridėtinės 8 baitais daugiau už faktinę ilgąją vertę. Priešingai, Sveikasis skaičius
turėjo nepanaudotą 4 baitų skylę, greičiausiai todėl, kad JVM, kurį naudoju, jėgų objektą išlygina ant 8 baitų žodžio ribos.
Masyvai
Žaidimas su primityvaus tipo masyvais yra pamokantis, iš dalies norint atrasti paslėptas pridėtines išlaidas, iš dalies pateisinant kitą populiarią gudrybę: primityvių reikšmių apvyniojimą 1 dydžio masyvu, kad jos būtų naudojamos kaip objektai. Modifikuodamas Mainof.main ()
norėdamas turėti kilpą, kuri padidina sukurtą masyvo ilgį kiekvienoje iteracijoje tarpt
masyvai:
ilgis: 0, {klasės [I} dydis = 16 baitų ilgis: 1, {klasės [I} dydis = 16 baitų ilgis: 2, {klasės [I} dydis = 24 baitų ilgis: 3, {klasės [I} dydis = 24 baitų ilgis: 4, {klasės [I} dydis = 32 baitų ilgis: 5, {klasės [I} dydis = 32 baitų ilgis: 6, {klasės [I} dydis = 40 baitų ilgis: 7, {klasė [I}) dydis = 40 baitų ilgis: 8, {klasės [I} dydis = 48 baitų ilgis: 9, {klasės [I} dydis = 48 baitų ilgis: 10, {klasės [I} dydis = 56 baitai)
ir už char
masyvai:
ilgis: 0, {klasė [C} dydis = 16 baitų ilgis: 1, {klasė [C} dydis = 16 baitų ilgis: 2, {klasė [C} dydis = 16 baitų ilgis: 3, {klasė [C} dydis = = 24 baitų ilgis: 4, {klasės [C} dydis = 24 baitų ilgis: 5, {klasės [C} dydis = 24 baitų ilgis: 6, {klasės [C} dydis = 24 baitų ilgis: 7, {klasė [C}) dydis = 32 baitų ilgis: 8, {klasės [C} dydis = 32 baitų ilgis: 9, {klasės [C} dydis = 32 baitų ilgis: 10, {klasės [C} dydis = 32 baitai)
Aukščiau vėl pasirodo 8 baitų derinimo įrodymai. Be to, be neišvengiamo Objektas
8 baitų viršutinis, primityvus masyvas prideda dar 8 baitus (iš kurių mažiausiai 4 baitai palaiko ilgio
srityje). Ir naudojant int [1]
atrodo, kad nesiūlo jokių atminties pranašumų prieš Sveikasis skaičius
išskyrus tuos pačius duomenis kaip galimai kintamą versiją.
Daugiamatės masyvai
Daugialypiai matricos suteikia dar vieną staigmeną. Kūrėjai dažniausiai naudoja tokias konstrukcijas kaip int [dim1] [dim2]
skaitinėje ir mokslinėje skaičiavime. Į int [dim1] [dim2]
masyvo egzempliorius, kiekvienas įdėtas int [dim2]
masyvas yra Objektas
savaime. Kiekvienas prideda įprastą 16 baitų masyvo pridėtinę sumą. Kai man nereikia trikampio ar nelygios masyvo, tai reiškia gryną pridėtinę sumą. Poveikis didėja, kai masyvo matmenys labai skiriasi. Pavyzdžiui, a int [128] [2]
egzempliorius užima 3600 baitų. Palyginti su 1040 baitų an int [256]
egzempliorių naudojimo (kuris turi tą patį pajėgumą), 3 600 baitų sudaro 246 procentus pridėtinių išlaidų. Kraštutiniu atveju baitas [256] [1]
, pridėtinis koeficientas yra beveik 19! Palyginkite tai su situacija C / C ++, kai ta pati sintaksė neprideda jokių saugyklos pridėtinių išlaidų.
java.lang.Stringas
Pabandykime tuščią Stygos
, pirmiausia sukonstruotas kaip nauja eilutė ()
:
„prieš“ krūva: 510696, „po“: krūva: 4510696 krūvos delta: 4000000, {class java.lang.String} dydis = 40 baitų
Rezultatas pasirodo gana slegiantis. Tuščia Stygos
užima 40 baitų - pakankamai atminties, kad tilptų 20 „Java“ simbolių.
Prieš bandydamas Stygos
Turint turinio, man reikia pagalbinio metodo Stygos
s garantuojama, kad nebus internuota. Vien tik vartojant pažodžius, kaip nurodyta:
object = "eilutė su 20 simbolių";
neveiks, nes visos tokios objekto rankenos galiausiai rodys tą patį Stygos
instancija. Kalbos specifikacijos diktuoja tokį elgesį (taip pat žr java.lang.String.intern ()
metodas). Todėl, norėdami tęsti atminties šnipinėjimą, pabandykite:
public static String createString (galutinis int ilgis) {char [] rezultatas = new char [length]; už (int i = 0; i <ilgis; ++ i) rezultatas [i] = (char) i; grąžinti naują eilutę (rezultatas); }
Apsiginklavęs tuo Stygos
kūrėjo metodas, gaunu šiuos rezultatus:
ilgis: 0, {klasės java.lang.String} dydis = 40 baitų ilgis: 1, {klasės java.lang.String} dydis = 40 baitų ilgis: 2, {class java.lang.String} dydis = 40 baitų ilgis: 3, {class java.lang.String} dydis = 48 baitų ilgis: 4, {class java.lang.String} dydis = 48 baitų ilgis: 5, {class java.lang.String} dydis = 48 baitų ilgis: 6, {class java.lang.String} dydis = 48 baitų ilgis: 7, {class java.lang.String} dydis = 56 baitų ilgis: 8, {class java.lang.String} dydis = 56 baitų ilgis: 9, {klasė java.lang.String} dydis = 56 baitų ilgis: 10, {class java.lang.String} dydis = 56 baitai
Rezultatai aiškiai rodo, kad a Stygos
atminties augimas seka jo vidinę char
masyvo augimas. Tačiau Stygos
klasė prideda dar 24 baitus pridėtinių išlaidų. Už nevykusį Stygos
10 simbolių ar mažiau, pridėtos pridėtinės išlaidos, palyginti su naudinga apkrova (2 baitai kiekvienam char
plius 4 baitai už ilgį), svyruoja nuo 100 iki 400 procentų.
Žinoma, bauda priklauso nuo jūsų programos duomenų paskirstymo. Kažkaip įtariau, kad 10 simbolių atspindi tipinį Stygos
ilgis įvairioms programoms. Norėdami gauti konkretų duomenų tašką, aš sukūriau „SwingSet2“ demonstracinę versiją (modifikuodamas Stygos
klasės diegimas tiesiogiai), pateiktas kartu su JDK 1.3.x, kad būtų galima stebėti ilgį Stygos
s tai kuria. Po kelių minučių žaidžiant su demonstracine versija, duomenų sąvartynas parodė, kad apie 180 tūkst Stygos
buvo akimirksniu. Rūšiuoti juos į dydžio kibirus patvirtino mano lūkesčius:
[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ...
Tiesa, daugiau nei 50 procentų visų Stygos
ilgis krito į 0-10 kibirą, labai karštą Stygos
klasės neefektyvumas!
Realybėje, Stygos
atminties gali suvartoti dar daugiau, nei rodo jų ilgis: Stygos
sukurtas iš „StringBuffer“
greičiausiai (arba tiesiogiai, arba per „+“ sujungimo operatorių) char
masyvai, kurių ilgiai didesni už nurodytą Stygos
ilgio, nes „StringBuffer“
Paprastai jie prasideda nuo 16 talpos, tada padvigubina pridėti ()
operacijos. Taigi, pavyzdžiui, createString (1) + “
baigiasi a char
16 dydžio, o ne 2 masyvas.
Ką mes darome?
„Viskas yra labai gerai, bet mes neturime kito pasirinkimo, kaip tik naudoti Stygos
s ir kitų tipų, kuriuos teikia „Java“, ar ne? "Aš girdžiu jūsų klausiant. Sužinokime.