Programmēšana

Java padoms 130: Vai jūs zināt savu datu lielumu?

Nesen es palīdzēju izstrādāt Java servera lietojumprogrammu, kas atgādināja atmiņas datu bāzi. Tas ir, mēs esam tendenciozi izstrādājuši daudzu datu kešatmiņu atmiņā, lai nodrošinātu ļoti ātru vaicājumu veiktspēju.

Kad prototips ir palaists, mēs, protams, nolēmām veikt datu atmiņas nospieduma profilēšanu pēc tam, kad tie bija parsēti un ielādēti no diska. Tomēr neapmierinošie sākotnējie rezultāti mani pamudināja meklēt paskaidrojumus.

Piezīme: Šī raksta avota kodu varat lejupielādēt vietnē Resursi.

Rīks

Tā kā Java mērķtiecīgi slēpj daudzus atmiņas pārvaldības aspektus, ir jāstrādā, lai uzzinātu, cik daudz atmiņas tērē jūsu objekti. Jūs varētu izmantot Runtime.freeMemory () metode kaudzes lieluma atšķirību mērīšanai pirms un pēc vairāku objektu piešķiršanas. Vairāki raksti, piemēram, Ramčandera Varadaradža "Nedēļas jautājums Nr. 107" (Sun Microsystems, 2000. gada septembris) un Tonija Sintesa "Atmiņas jautājumi" (JavaWorld, 2001. gada decembris), sīki izklāstiet šo ideju. Diemžēl iepriekšējā raksta risinājums neizdodas, jo ieviešanā tiek izmantots nepareizs Izpildlaiks metodi, bet pēdējā raksta risinājumam ir savas nepilnības:

  • Viens zvans uz Runtime.freeMemory () izrādās nepietiekams, jo JVM var izlemt palielināt savu pašreizējo kaudzes lielumu jebkurā laikā (īpaši, ja tas vada atkritumu savākšanu). Ja vien kopējais kaudzes lielums jau nepārsniedz -Xmx maksimālo lielumu, mums tas jāizmanto Runtime.totalMemory () - Runtime.freeMemory () kā izmantoto kaudzes izmēru.
  • Izpildot vienu Runtime.gc () zvans var nebūt pietiekami agresīvs, lai pieprasītu atkritumu savākšanu. Mēs varētu, piemēram, pieprasīt, lai darbojas arī objektu pabeigšanas ierīces. Un kopš tā laika Runtime.gc () nav dokumentēts, lai bloķētu, kamēr kolekcija nav pabeigta, ieteicams pagaidīt, līdz uztvertais kaudzes lielums stabilizējas.
  • Ja profilētā klase izveido visus statiskos datus kā daļu no savas klases klases inicializācijas (ieskaitot statiskās klases un lauka inicializētājus), kaudzes atmiņā, ko izmanto pirmās klases instancē, šie dati var būt iekļauti. Mums vajadzētu ignorēt kaudzes vietu, ko patērē pirmās klases instance.

Ņemot vērā šīs problēmas, es iepazīstinu Sizeof, rīks, ar kuru es aplūkoju dažādas Java kodola un lietojumprogrammu klases:

publiskā klase Sizeof {public static void main (String [] args) throws Exception {// Iesildiet visas klases / metodes, kuras mēs izmantosim runGC (); usedMemory (); // Masīvs, lai saglabātu spēcīgas atsauces uz piešķirtajiem objektiem galīgais int skaits = 100000; Object [] objekti = new Object [skaits]; garš kaudze1 = 0; // Piešķiriet skaitam + 1 objektam, atmetiet pirmo (int i = -1; i = 0) objektiem [i] = objekts; else {objekts = null; // Izmetiet iesildīšanās objektu runGC (); kaudze1 = lietotaAtmiņa (); // Uzņemiet pirms kaudzes momentuzņēmumu}} runGC (); garš kaudze2 = izlietotsAtmiņa (); // Uzņemiet pēc kaudzes momentuzņēmumu: galīgais int lielums = Math.round (((pludiņš) (kaudze2 - kaudze1)) / skaits); System.out.println ("'pirms' kaudze:" + kaudze1 + "," aiz "kaudze:" + kaudze2); System.out.println ("kaudzes delta:" + (kaudze2 - kaudze1) + ", {" + objekti [0] .getClass () + "} size =" + size + "baiti"); priekš (int i = 0; i <skaits; ++ i) objektiem [i] = nulle; objekti = nulle; } private static void runGC () izmet izņēmumu {// Tas palīdz izsaukt Runtime.gc () //, izmantojot vairākas metodes izsaukumus: for (int r = 0; r <4; ++ r) _runGC (); } private static void _runGC () izmet izņēmumu {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; par (int i = 0; (lietotsMem1 <izmantotsMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); lietotsMem2 = lietotsMem1; usedMem1 = lietotsMemory (); }} private static long usedMemory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } private static final Runtime s_runtime = Runtime.getRuntime (); } // Nodarbības beigas 

Sizeofgalvenās metodes ir runGC () un usedMemory (). Es izmantoju a runGC () iesaiņošanas metode, lai piezvanītu _runGC () vairākas reizes, jo tas, šķiet, padara metodi agresīvāku. (Es neesmu pārliecināts, kāpēc, bet ir iespējams, ka, izveidojot un iznīcinot metodes izsaukuma kaudzes rāmi, tiek mainītas sasniedzamības sakņu kopas un liekat atkritumu savācējam strādāt vairāk. Turklāt, patērējot lielu daļu kaudzes, lai izveidotu pietiekami daudz darba palīdz arī atkritumu savācējs. Parasti ir grūti nodrošināt visu savākšanu. Precīza informācija ir atkarīga no JVM un atkritumu savākšanas algoritma.)

Uzmanīgi atzīmējiet vietas, kurās es atsaucos runGC (). Kodu var rediģēt starp kaudze1 un kaudze2 deklarācijas, lai aktualizētu jebko, kas interesē.

Tāpat ņemiet vērā, kā Sizeof izdrukā objekta izmēru: visiem nepieciešamo datu pārejošu slēgšanu skaitīt klases instances, dalītas ar skaitīt. Lielākajai daļai klašu rezultāts būs atmiņa, ko patērē viena klases instance, ieskaitot visus tai piederošos laukus. Šī atmiņas nospieduma vērtība atšķiras no datiem, ko sniedz daudzi komerciālie profilētāji, kuri ziņo par seklām atmiņas pēdām (piemēram, ja objektam ir int [] laukā, tā atmiņas patēriņš parādīsies atsevišķi).

Rezultāti

Pielietosim šo vienkāršo rīku dažām klasēm, pēc tam redzēsim, vai rezultāti atbilst mūsu cerībām.

Piezīme: Šie rezultāti ir balstīti uz Sun JDK 1.3.1 operētājsistēmai Windows. Sakarā ar to, ko garantē un ko negarantē Java valoda un JVM specifikācijas, šos īpašos rezultātus nevar piemērot citām platformām vai citām Java ieviešanām.

java.lang.Object

Nu, visu objektu saknei vienkārši bija jābūt manam pirmajam gadījumam. Priekš java.lang.Object, Es saņemu:

'pirms' kaudze: 510696, 'pēc' kaudze: 1310696 kaudzes delta: 800000, {klases java.lang.Object} izmērs = 8 baiti 

Tātad, līdzenums Objekts ņem 8 baitus; protams, nevienam nevajadzētu cerēt, ka izmērs būs 0, jo katram gadījumam ir jānes lauki, kas atbalsta bāzes darbības, piemēram vienāds (), hashCode (), pagaidi () / paziņo (), un tā tālāk.

java.lang.Integer

Mēs un mani kolēģi bieži iesaiņojam vietējos ints vērā Vesels skaitlis gadījumus, lai mēs tos varētu glabāt Java kolekcijās. Cik tas mums atmiņā maksā?

'pirms' kaudze: 510696, 'aiz' kaudze: 2110696 kaudzes delta: 1600000, {klases java.lang.Integer} izmērs = 16 baiti 

16 baitu rezultāts ir nedaudz sliktāks, nekā es gaidīju, jo int vērtība var ietilpt tikai 4 papildu baitos. Izmantojot Vesels skaitlis maksā man atmiņas pieskaitāmās izmaksas par 300 procentiem, salīdzinot ar to, kad es varu saglabāt vērtību kā primitīvu tipu.

java.lang.Long

Garš vajadzētu aizņemt vairāk atmiņas nekā Vesels skaitlis, bet tas nedara:

'pirms' kaudze: 510696, 'aiz' kaudze: 2110696 kaudzes delta: 1600000, {class java.lang.Long} izmērs = 16 baiti 

Skaidrs, ka faktiskais kaudzes objekta lielums ir pakļauts zema līmeņa atmiņas izlīdzināšanai, ko veic konkrēta JVM ieviešana konkrētam CPU tipam. Tas izskatās kā Garš ir 8 baiti no Objekts pieskaitāmās izmaksas, plus 8 baiti vairāk par faktisko garo vērtību. Turpretī Vesels skaitlis bija neizmantota 4 baitu caurums, visticamāk, tāpēc, ka JVM I izmanto spēku objektu izlīdzināšanu uz 8 baitu vārdu robežas.

Masīvi

Spēle ar primitīva tipa masīviem izrādās pamācoša, daļēji, lai atklātu jebkuru slēpto pieskaitāmo daļu, un daļēji, lai attaisnotu vēl vienu populāru triku: primitīvu vērtību iesaiņošanu masīvā 1, lai tās izmantotu kā objektus. Modificējot Mainof.main () lai iegūtu cilpu, kas palielina izveidotā masīva garumu katrā atkārtojumā, es to saprotu int masīvi:

garums: 0, {klase [I} izmērs = 16 baitu garums: 1, {klase [I} izmērs = 16 baitu garums: 2, {klase [I} izmērs = 24 baiti garums: 3, {klase [I} izmērs = 24 baitu garums: 4, {klase [I} izmērs = 32 baitu garums: 5, {klase [I} izmērs = 32 baiti garums: 6, {klase [I} izmērs = 40 baiti garums: 7, {klase [I}) izmērs = 40 baitu garums: 8, {klase [I} izmērs = 48 baiti garums: 9, {klase [I} izmērs = 48 baiti garums: 10, {klase [I} izmērs = 56 baiti) 

un par char masīvi:

garums: 0, {klase [C} izmērs = 16 baitu garums: 1, {klase [C} izmērs = 16 baitu garums: 2, {klase [C} izmērs = 16 baitu garums: 3, {klase [C} izmērs = 24 baitu garums: 4, {klases [C} izmērs = 24 baitu garums: 5, {klases [C} izmērs = 24 baitu garums: 6, {klases [C} izmērs = 24 baitu garums: 7, {klase [C}) izmērs = 32 baitu garums: 8, {klase [C} izmērs = 32 baitu garums: 9, {klase [C} izmērs = 32 baitu garums: 10, {klase [C} izmērs = 32 baiti) 

Augšpusē atkal parādās pierādījumi par 8 baitu izlīdzināšanu. Turklāt papildus neizbēgamajam Objekts 8 baitu virs galvas primitīvs masīvs pievieno vēl 8 baitus (no kuriem vismaz 4 baiti atbalsta garums laukā). Un izmantojot int [1] šķiet, ka nepiedāvā atmiņas priekšrocības salīdzinājumā ar Vesels skaitlis izņemot to pašu datu maināmu versiju.

Daudzdimensionāli masīvi

Daudzdimensionāli masīvi piedāvā vēl vienu pārsteigumu. Izstrādātāji parasti izmanto tādas konstrukcijas kā int [dim1] [dim2] skaitliskajā un zinātniskajā skaitļošanā. In int [dim1] [dim2] masīva instance, katrs ligzdots int [dim2] masīvs ir Objekts pati par sevi. Katrs no tiem pievieno parasto 16 baitu masīva virs galvas. Kad man nav vajadzīgs trīsstūrveida vai sasmalcināts masīvs, tas attēlo tīru virs galvas. Ietekme pieaug, ja masīva izmēri ievērojami atšķiras. Piemēram, a int [128] [2] eksemplārs aizņem 3600 baitus. Salīdzinot ar 1040 baitiem un int [256] gadījumu izmanto (kam ir vienāda jauda), 3600 baiti ir 246 procentu pieskaitāmās izmaksas. Galējā gadījumā baits [256] [1], pieskaitāmo faktors ir gandrīz 19! Salīdziniet to ar situāciju C / C ++, kurā viena un tā pati sintakse nepievieno krātuves papildu izmaksas.

java.lang.Strings

Izmēģināsim tukšu Stīga, vispirms konstruēts kā jauna virkne ():

'pirms' kaudze: 510696, 'aiz' kaudze: 4510696 kaudzes delta: 4000000, {class java.lang.String} izmērs = 40 baiti 

Rezultāts izrādās diezgan nomācošs. Tukša Stīga aizņem 40 baitus - pietiekami daudz atmiņas, lai ietilptu 20 Java rakstzīmes.

Pirms mēģinu Stīgas ar saturu, man ir nepieciešama palīga metode, lai izveidotu Stīgas garantē, ka netiks internēts. Vienkārši lietojot literārus, kā tas ir:

 object = "virkne ar 20 rakstzīmēm"; 

nedarbosies, jo visi šādi objektu rokturi galu galā norādīs uz to pašu Stīga instancē. Valodas specifikācija nosaka šādu rīcību (sk. Arī java.lang.String.intern () metode). Tāpēc, lai turpinātu atmiņas snoopēšanu, mēģiniet:

 publiskā statiskā virkne createString (galīgais int garums) {char [] rezultāts = new char [length]; par (int i = 0; i <garums; ++ i) rezultāts [i] = (char) i; atgriezt jaunu virkni (rezultāts); } 

Pēc tam, kad esmu sevi apbruņojis ar šo Stīga radītāja metodi, es saņemu šādus rezultātus:

garums: 0, {klases java.lang.String} izmērs = 40 baitu garums: 1, {klases java.lang.String} izmērs = 40 baitu garums: 2, {klases java.lang.String} izmērs = 40 baitu garums: 3, {klases java.lang.String} izmērs = 48 baitu garums: 4, {klases java.lang.String} izmērs = 48 baitu garums: 5, {klases java.lang.String} izmērs = 48 baitu garums: 6, {class java.lang.String} izmērs = 48 baitu garums: 7, {class java.lang.String} izmērs = 56 baitu garums: 8, {class java.lang.String} izmērs = 56 baitu garums: 9, {klase java.lang.String} izmērs = 56 baitu garums: 10, {klases java.lang.String} izmērs = 56 baiti 

Rezultāti skaidri parāda, ka a Stīgaatmiņas pieaugums izseko tās iekšējo char masīva izaugsme. Tomēr Stīga klase pievieno vēl 24 baitus pieskaitāmās izmaksas. Par neizturīgu Stīga 10 rakstzīmes vai mazāk, pievienotās pieskaitāmās izmaksas attiecībā pret lietderīgo kravu (2 baiti katram char plus 4 baiti garumam), svārstās no 100 līdz 400 procentiem.

Protams, sods ir atkarīgs no jūsu lietojumprogrammas datu izplatīšanas. Kaut kā man bija aizdomas, ka 10 rakstzīmes pārstāv tipisko Stīga garums dažādām lietojumprogrammām. Lai iegūtu konkrētu datu punktu, es izveidoju instrumentu SwingSet2 demonstrācijai (modificējot Stīga klases ieviešana tieši), kas tika piegādāts kopā ar JDK 1.3.x, lai izsekotu Stīgas tas rada. Pēc dažām minūtēm, spēlējot ar demonstrāciju, datu izgāztuve parādīja, ka apmēram 180 000 Stīgas tika instancēti. To šķirošana lieluma kausos apstiprināja manas cerības:

[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 ... 

Pareizi, vairāk nekā 50 procenti no visiem Stīga garums iekrita 0-10 spainī, ļoti karstā vieta Stīga klases neefektivitāte!

Īstenībā, Stīgas var patērēt pat vairāk atmiņas, nekā liecina to garums: Stīgas ģenerēts no StringBuffers (vai nu tieši, vai ar “+” savienošanas operatora starpniecību) char masīvi, kuru garums ir lielāks par norādīto Stīga garumi, jo StringBufferParasti tās sākas ar ietilpību 16, pēc tam dubultojiet to pievienot () operācijas. Tā, piemēram, createString (1) + " beidzas ar a char 16 izmēra masīvs, nevis 2.

Ko mēs darām?

"Tas viss ir ļoti labi, taču mums nekas cits neatliek kā izmantot Stīgas un citi Java piedāvātie veidi, vai ne? "Es dzirdu jūs jautājam. Uzzināsim.

Iesaiņotāju klases

$config[zx-auto] not found$config[zx-overlay] not found