Programmering

Sizeof til Java

26. december 2003

Spørgsmål: Har Java en operatør som størrelse () i C?

EN: Et overfladisk svar er, at Java ikke leverer noget lignende C's størrelse af (). Lad os dog overveje hvorfor en Java-programmør vil muligvis lejlighedsvis have det.

En C-programmør administrerer selv de fleste datastrukturhukommelsesallokeringer og størrelse af () er uundværlig for at kende hukommelsesblokstørrelser at allokere. Derudover kan C-hukommelsesallokatorer lide malloc () gør næsten intet for så vidt angår initialisering af objekt: en programmør skal indstille alle objektfelter, der er pegepinde, til yderligere objekter. Men når alt er sagt og kodet, er C / C ++ hukommelsesallokering ret effektiv.

Til sammenligning er Java-objektallokering og -konstruktion bundet sammen (det er umuligt at bruge en allokeret, men ikke-initialiseret objektforekomst). Hvis en Java-klasse definerer felter, der henviser til yderligere objekter, er det også almindeligt at indstille dem ved konstruktionstidspunktet. Tildeling af et Java-objekt tildeler derfor ofte mange sammenkoblede objektforekomster: en objektgraf. Sammen med automatisk affaldsindsamling er dette alt for praktisk og kan få dig til at føle, at du aldrig behøver at bekymre dig om Java-hukommelsesallokeringsoplysninger.

Selvfølgelig fungerer dette kun til enkle Java-applikationer. Sammenlignet med C / C ++ har ækvivalente Java-datastrukturer tendens til at optage mere fysisk hukommelse. I virksomhedssoftwareudvikling er det en almindelig begrænsning at komme tæt på den maksimale tilgængelige virtuelle hukommelse på nutidens 32-bit JVM'er. En Java-programmør kunne således drage fordel af størrelse af () eller noget lignende for at holde øje med, om hans datastrukturer bliver for store eller indeholder hukommelsesflaskehalse. Heldigvis giver Java-refleksion dig mulighed for at skrive et sådant værktøj ganske let.

Før jeg fortsætter, vil jeg give afkald på nogle hyppige, men forkerte svar på denne artikels spørgsmål.

Fejlfinding: Sizeof () er ikke nødvendig, fordi Java-basistypestørrelser er faste

Ja, en Java int er 32 bits i alle JVM'er og på alle platforme, men dette er kun et sprogsspecifikationskrav til programmør-opfattelig bredde på denne datatype. Sådan en int er i det væsentlige en abstrakt datatype og kan sikkerhedskopieres af f.eks. et 64-bit fysisk hukommelsesord på en 64-bit maskine. Det samme gælder for ikke-primitive typer: Java-sprogspecifikationen siger intet om, hvordan klassefelter skal justeres i fysisk hukommelse, eller at en række booleanere ikke kunne implementeres som en kompakt bitvektor inde i JVM.

Fejlfinding: Du kan måle et objekts størrelse ved at serieisere det til en byte-strøm og se på den resulterende strømlængde

Årsagen til, at dette ikke fungerer, er, at serialiseringslayoutet kun er en fjernbetjening af det sande layout i hukommelsen. En nem måde at se det på er ved at se på hvordan Snors bliver serieliseret: i hukommelsen hver char er mindst 2 byte, men i serieform Snors er UTF-8-kodet, og alt ASCII-indhold tager så halvt så meget plads.

En anden arbejdsmetode

Du kan muligvis huske "Java Tip 130: Kender du din datastørrelse?" der beskrev en teknik baseret på oprettelse af et stort antal identiske klasseinstanser og omhyggelig måling af den resulterende stigning i den anvendte JVM-bunkestørrelse. Når det er relevant, fungerer denne idé meget godt, og jeg vil faktisk bruge den til at starte den alternative tilgang i denne artikel.

Bemærk, at Java Tip 130's Størrelse af klasse kræver en hvilende JVM (således at bunkeaktiviteten kun skyldes objektallokeringer og affaldssamlinger, der kræves af måletråden) og kræver et stort antal identiske objektforekomster. Dette fungerer ikke, når du vil dimensionere et enkelt stort objekt (måske som en del af en fejlfindingssporingsoutput), og især når du vil undersøge, hvad der faktisk gjorde det så stort.

Hvad er størrelsen på et objekt?

Diskussionen ovenfor fremhæver et filosofisk punkt: da du normalt beskæftiger dig med objektgrafer, hvad er definitionen af ​​en objektstørrelse? Er det bare størrelsen på den objektforekomst, du undersøger, eller størrelsen på hele datagrafen, der er rodfæstet ved objektforekomsten? Sidstnævnte er det, der normalt betyder mere i praksis. Som du skal se, er tingene ikke altid så klare, men til at begynde med kan du følge denne tilgang:

  • En objektforekomst kan (omtrent) dimensioneres ved at samle alle dens ikke-statiske datafelter (inklusive felter defineret i superklasser)
  • I modsætning til f.eks. C ++ har klassemetoder og deres virtualitet ingen indflydelse på objektstørrelsen
  • Klasse-supergrænseflader har ingen indflydelse på objektstørrelsen (se noten i slutningen af ​​denne liste)
  • Den fulde objektstørrelse kan opnås som en lukning over hele objektgrafen, der er rodfæstet ved startobjektet
Bemærk: Implementering af enhver Java-grænseflade markerer blot den pågældende klasse og tilføjer ikke data til dens definition. Faktisk validerer JVM ikke engang, at en interfaceimplementering giver alle de metoder, der kræves af grænsefladen: dette er strengt kompilatorens ansvar i de nuværende specifikationer.

For at starte processen, bruger jeg til primitive datatyper fysiske størrelser målt ved Java Tip 130's Størrelse af klasse. Som det viser sig, for almindelige 32-bit JVM'er en almindelig java.lang.Objekt tager 8 byte, og de grundlæggende datatyper har normalt mindst fysisk størrelse, der kan imødekomme sprogkravene (undtagen boolsk tager en hel byte):

 // java.lang.Object shell-størrelse i byte: offentlig statisk endelig int OBJECT_SHELL_SIZE = 8; offentlig statisk endelig int OBJREF_SIZE = 4; offentlig statisk endelig int LONG_FIELD_SIZE = 8; offentlig statisk endelig int INT_FIELD_SIZE = 4; offentlig statisk endelig int SHORT_FIELD_SIZE = 2; offentlig statisk endelig int CHAR_FIELD_SIZE = 2; offentlig statisk endelig int BYTE_FIELD_SIZE = 1; offentlig statisk endelig int BOOLEAN_FIELD_SIZE = 1; offentlig statisk endelig int DOUBLE_FIELD_SIZE = 8; offentlig statisk endelig int FLOAT_FIELD_SIZE = 4; 

(Det er vigtigt at indse, at disse konstanter ikke er hårdkodet for evigt og skal måles uafhængigt for en given JVM.) Naturligvis forsømmer naiv totalering af objektfeltstørrelser problemer med hukommelsesjustering i JVM. Hukommelsesjustering betyder noget (som vist for eksempel for primitive array-typer i Java Tip 130), men jeg synes, det er urentabelt at jage efter sådanne detaljer på lavt niveau. Ikke kun er sådanne detaljer afhængige af JVM-forhandleren, de er ikke under programmørens kontrol. Vores mål er at få et godt gæt på objektets størrelse og forhåbentlig få en anelse om, når et klassefelt kan være overflødigt; eller hvornår et felt skal være dovet befolket; eller når en mere kompakt indlejret datastruktur er nødvendig osv. For absolut fysisk præcision kan du altid gå tilbage til Størrelse af klasse i Java Tip 130.

For at hjælpe med at profilere, hvad der udgør en objektforekomst, beregner vores værktøj ikke kun størrelsen, men bygger også en nyttig datastruktur som et biprodukt: en graf bestående af IObjectProfileNodes:

interface IObjectProfileNode {Objektobjekt (); Strengnavn (); int størrelse (); int refcount (); IObjectProfileNode forælder (); IObjectProfileNode [] børn (); IObjectProfileNode shell (); IObjectProfileNode [] sti (); IObjectProfileNode rod (); int-vejlængde (); boolsk travers (INodeFilter filter, INodeVisitor besøgende); String dump (); } // Slutningen af ​​grænsefladen 

IObjectProfileNodes er sammenkoblet næsten nøjagtigt på samme måde som den originale objektgraf med IObjectProfileNode.object () returnere det virkelige objekt, som hver node repræsenterer. IObjectProfileNode.size () returnerer den samlede størrelse (i byte) af objektets undertråd, der er rodfæstet ved den nodes objektforekomst. Hvis en objektforekomst linker til andre objekter via ikke-nulige forekomstfelter eller via referencer indeholdt i matrixfelter, så IObjectProfileNode.children () vil være en tilsvarende liste over underordnede grafknudepunkter, sorteret i faldende størrelsesrækkefølge. Omvendt for hver anden knude end den startende, IObjectProfileNode.parent () returnerer sin forælder. Hele samlingen af IObjectProfileNodes skiver og indskærer således det originale objekt og viser, hvordan datalagring er opdelt i det. Desuden er navne på grafknudepunkterne afledt af klassefelterne og undersøger en knuds sti i grafen (IObjectProfileNode.path ()) giver dig mulighed for at spore ejerskabslinkene fra den oprindelige objektforekomst til ethvert internt stykke data.

Du har måske bemærket, mens du læste det foregående afsnit, at ideen indtil videre stadig har en vis tvetydighed. Hvis du, mens du krydser objektgrafen, støder på den samme objektforekomst mere end én gang (dvs. mere end et felt et eller andet sted i grafen peger på det), hvordan tildeler du dets ejerskab (den overordnede markør)? Overvej dette kodestykke:

 Objekt obj = ny streng [] {ny streng ("JavaWorld"), ny streng ("JavaWorld")}; 

Hver java.lang.Streng instans har et internt typefelt char [] det er det aktuelle strengindhold. Den måde, hvorpå Snor copy constructor fungerer i Java 2 Platform, Standard Edition (J2SE) 1.4, begge Snor forekomster inde i ovenstående array deler det samme char [] array indeholdende {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} tegnsekvens. Begge strenge ejer dette array lige meget, så hvad skal du gøre i tilfælde som dette?

Hvis jeg altid vil tildele en enlig forælder til en grafknude, har dette problem intet universelt perfekt svar. Imidlertid kunne mange sådanne forekomster i praksis spores tilbage til en enkelt "naturlig" forælder. En sådan naturlig rækkefølge af links er normalt kortere end de andre, mere kredsløbsruter. Tænk på data, der vises af instansfelter, der hører mere til den instans end til noget andet. Tænk på poster i en matrix som mere tilhørende selve den matrix. Således, hvis en intern objektforekomst kan nås via flere stier, vælger vi den korteste sti. Hvis vi har flere stier af samme længde, vælger vi bare den første opdagede. I værste fald er dette en så god generisk strategi som nogen.

At tænke på grafgennemgange og korteste stier skal ringe på en klokke på dette tidspunkt: bredde-første søgning er en grafgennemgangsalgoritme, der garanterer at finde den korteste sti fra startknudepunktet til enhver anden tilgængelig grafknude.

Efter alle disse forberedelser er her en lærebogsimplementering af en sådan grafgennemgang. (Nogle detaljer og hjælpemetoder er udeladt; se denne artikels download for alle detaljer.):