Programavimas

„Java“ patarimas 130: Ar žinote savo duomenų dydį?

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 naudoti Runtime.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 

Dydispagrindiniai 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 StygosTurint turinio, man reikia pagalbinio metodo Stygoss 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 Stygosatminties 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į Stygoss 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, Stygosatminties gali suvartoti dar daugiau, nei rodo jų ilgis: Stygossukurtas 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 Stygoss ir kitų tipų, kuriuos teikia „Java“, ar ne? "Aš girdžiu jūsų klausiant. Sužinokime.

Vyniotojų klasės