Programování

Velikost pro Javu

26. prosince 2003

Otázka: Má Java operátor jako sizeof () v jazyce C?

A: Povrchní odpovědí je, že Java neposkytuje nic jako C velikost(). Uvažujme však proč programátor Java by to občas mohl chtít.

Programátor jazyka C spravuje většinu přidělení paměti datové struktury sám a velikost() je nepostradatelný pro znalost velikosti alokačních bloků paměti. Navíc alokátory paměti typu C mají rádi malloc () pokud jde o inicializaci objektů, nedělejte téměř nic: programátor musí nastavit všechna pole objektů, která jsou ukazateli na další objekty. Ale když je vše řečeno a kódováno, alokace paměti C / C ++ je docela efektivní.

Pro srovnání jsou alokace a konstrukce objektů Java svázány dohromady (není možné použít přidělenou, ale neinicializovanou instanci objektu). Pokud třída Java definuje pole, která jsou odkazy na další objekty, je také běžné je nastavit v době výstavby. Přiřazení objektu Java proto často přiděluje řadu vzájemně propojených instancí objektu: graf objektu. Spolu s automatickým sběrem odpadků je to až příliš pohodlné a můžete se cítit, jako byste se nikdy nemuseli starat o podrobnosti přidělení paměti Java.

Samozřejmě to funguje pouze pro jednoduché Java aplikace. Ve srovnání s C / C ++ mají ekvivalentní datové struktury Java tendenci zabírat více fyzické paměti. Při vývoji podnikového softwaru je přiblížení se maximální dostupné virtuální paměti na dnešních 32bitových JVM běžným omezením škálovatelnosti. Programátor jazyka Java by tedy mohl těžit velikost() nebo něco podobného, ​​aby dohlížel na to, zda jsou jeho datové struktury příliš velké nebo obsahují úzká místa paměti. Naštěstí Java reflexe umožňuje napsat takový nástroj docela snadno.

Než budu pokračovat, upustím od některých častých, ale nesprávných odpovědí na otázku tohoto článku.

Fallacy: Sizeof () není nutný, protože velikosti základních typů Java jsou opraveny

Ano, Java int je 32 bitů ve všech JVM a na všech platformách, ale toto je pouze požadavek jazykové specifikace pro vnímatelný programátorem šířka tohoto datového typu. Takový int je v podstatě abstraktní datový typ a lze jej zálohovat, řekněme, 64bitovým slovem fyzické paměti na 64bitovém stroji. To samé platí i pro neprprimativní typy: Specifikace jazyka Java neříká nic o tom, jak by měla být pole třídy zarovnána ve fyzické paměti, nebo že pole booleanů nemohlo být implementováno jako kompaktní bitvector uvnitř JVM.

Klam: Velikost objektu můžete měřit jeho serializací do bajtového proudu a sledováním výsledné délky proudu

Důvod, proč to nefunguje, je ten, že rozložení serializace je pouze vzdáleným odrazem skutečného rozložení v paměti. Jedním ze způsobů, jak to snadno zjistit, je pohled na to, jak na to Tětivaje serializováno: v paměti každý char je alespoň 2 bajty, ale v serializované podobě Tětivas jsou kódovány UTF-8, takže veškerý obsah ASCII zabírá polovinu místa.

Další pracovní přístup

Možná si vzpomínáte na „Java Tip 130: Znáte svou velikost dat?“ který popsal techniku ​​založenou na vytvoření velkého počtu instancí identické třídy a pečlivém měření výsledného zvýšení velikosti haldy použité v JVM. Pokud je to možné, tato myšlenka funguje velmi dobře a ve skutečnosti ji použiji k zavedení alternativního přístupu v tomto článku.

Všimněte si, že Java Tip 130's Velikost třída vyžaduje klidový JVM (aby aktivita haldy byla způsobena pouze přidělením objektů a uvolněním paměti požadovaným měřicím vláknem) a vyžaduje velké množství instancí identických objektů. To nefunguje, pokud chcete změnit velikost jednoho velkého objektu (možná jako součást výstupu trasování ladění), a zvláště když chcete prozkoumat, co ho vlastně tak zvětšilo.

Jaká je velikost objektu?

Výše uvedená diskuse zdůrazňuje filozofický bod: jaká je definice velikosti objektu vzhledem k tomu, že se obvykle zabýváte grafy objektů? Je to jen velikost instance objektu, který zkoumáte, nebo velikost celého datového grafu zakořeněného v instanci objektu? V praxi obvykle záleží na tom druhém. Jak uvidíte, věci nejsou vždy tak jednoznačné, ale pro začátečníky se můžete řídit tímto přístupem:

  • Instanci objektu lze (přibližně) dimenzovat sečtením všech jeho nestatických datových polí (včetně polí definovaných v nadtřídách)
  • Na rozdíl od řekněme C ++ nemají třídní metody a jejich virtualizace žádný vliv na velikost objektu
  • Superrozhraní třídy nemají žádný vliv na velikost objektu (viz poznámka na konci tohoto seznamu)
  • Plnou velikost objektu lze získat jako uzavření celého grafu objektu zakořeněného na počátečním objektu
Poznámka: Implementace libovolného rozhraní Java pouze označí dotyčnou třídu a nepřidá k její definici žádná data. Ve skutečnosti JVM ani neověří, že implementace rozhraní poskytuje všechny metody vyžadované rozhraním: to je striktně odpovědnost kompilátoru v aktuálních specifikacích.

Chcete-li zavést proces, pro primitivní datové typy používám fyzické velikosti měřené pomocí Java Tip 130 Velikost třída. Jak se ukázalo, pro běžné 32bitové JVM prosté java.lang.Object zabírá 8 bajtů a základní datové typy mají obvykle minimální fyzickou velikost, která dokáže vyhovět jazykovým požadavkům (kromě booleovský zabírá celý bajt):

 // java.lang.Object velikost shellu v bajtech: public static final int OBJECT_SHELL_SIZE = 8; public static final int OBJREF_SIZE = 4; public static final int LONG_FIELD_SIZE = 8; public static final int INT_FIELD_SIZE = 4; public static final int SHORT_FIELD_SIZE = 2; public static final int CHAR_FIELD_SIZE = 2; public static final int BYTE_FIELD_SIZE = 1; public static final int BOOLEAN_FIELD_SIZE = 1; public static final int DOUBLE_FIELD_SIZE = 8; public static final int FLOAT_FIELD_SIZE = 4; 

(Je důležité si uvědomit, že tyto konstanty nejsou napevno zakódovány a musí být pro daný JVM nezávisle měřeny.) Samozřejmě naivní součet velikostí pole objektu zanedbává problémy se zarovnáním paměti v JVM. Na zarovnání paměti záleží (jak je ukázáno například u primitivních typů polí v Java Tip 130), ale myslím si, že je nerentabilní honit se za takovými nízkoúrovňovými detaily. Nejen, že takové podrobnosti závisí na prodejci JVM, nejsou pod kontrolou programátora. Naším cílem je získat dobrý odhad velikosti objektu a doufejme, že získáme vodítko, když pole třídy může být nadbytečné; nebo kdy má být pole líně osídleno; nebo když je nutná kompaktnější vnořená datová struktura atd. Pro absolutní fyzickou přesnost se můžete vždy vrátit zpět na Velikost třída v Java Tip 130.

Abychom pomohli profilovat, co tvoří instanci objektu, náš nástroj nejen vypočítá velikost, ale také vytvoří užitečnou datovou strukturu jako vedlejší produkt: graf složený z IObjectProfileNodes:

interface IObjectProfileNode {Object object (); Název řetězce (); int size (); int refcount (); IObjectProfileNode parent (); IObjectProfileNode [] děti (); IObjectProfileNode shell (); IObjectProfileNode [] cesta (); IObjectProfileNode root (); int délka cesty (); boolovský traverz (filtr INodeFilter, návštěvník INodeVisitor); Výpis řetězce (); } // Konec rozhraní 

IObjectProfileNodes jsou propojeny téměř přesně stejným způsobem jako původní graf objektu, s IObjectProfileNode.object () vrácení skutečného objektu, který každý uzel představuje. IObjectProfileNode.size () vrací celkovou velikost (v bajtech) podstromu objektu zakořeněného v instanci objektu daného uzlu. Pokud instance objektu odkazuje na jiné objekty prostřednictvím nenulových polí instance nebo prostřednictvím odkazů obsažených v polích pole, pak IObjectProfileNode.children () bude odpovídající seznam uzlů podřízeného grafu seřazených podle velikosti. Naopak pro každý jiný než počáteční uzel IObjectProfileNode.parent () vrací svého rodiče. Celá kolekce IObjectProfileNodes tedy krájí a krájí původní objekt a ukazuje, jak je v něm úložiště dat rozděleno. Názvy uzlů grafu jsou dále odvozeny z polí třídy a zkoumáním cesty uzlu v grafu (IObjectProfileNode.path ()) umožňuje sledovat vlastnické odkazy z původní instance objektu na jakoukoli interní část dat.

Možná jste si při čtení předchozího odstavce všimli, že tento nápad má dosud určitou nejednoznačnost. Pokud se při procházení grafem objektu setkáte se stejnou instancí objektu více než jednou (tj. Více než jedno pole někde v grafu na ni ukazuje), jak přiřadíte jeho vlastnictví (nadřazený ukazatel)? Zvažte tento fragment kódu:

 Objekt obj = new String [] {new String ("JavaWorld"), new String ("JavaWorld")}; 

Každý řetězec java.lang instance má vnitřní pole typu char [] to je skutečný obsah řetězce. Způsob, jakým Tětiva copy constructor funguje v prostředí Java 2 Platform, Standard Edition (J2SE) 1.4, obojí Tětiva instance uvnitř výše uvedeného pole budou sdílet stejné char [] pole obsahující {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'} posloupnost znaků. Oba řetězce vlastní toto pole stejně, tak co byste měli dělat v takových případech?

Pokud vždy chci přiřadit jediného rodiče uzlu grafu, pak tento problém nemá univerzálně dokonalou odpověď. V praxi však lze mnoho takových instancí objektů vysledovat zpět k jedinému „přirozenému“ rodiči. Taková přirozená posloupnost odkazů je obvykle kratší než ostatní, oklikější trasy. Přemýšlejte o datech, na která pole instance poukazují, že patří více této instanci než cokoli jiného. Přemýšlejte o tom, že položky v poli patří více do samotného pole. Pokud tedy lze interní instanci objektu dosáhnout několika cestami, zvolíme nejkratší cestu. Pokud máme několik cest stejné délky, dobře, stačí vybrat první objevenou. V nejhorším případě je to stejně dobrá obecná strategie jako každá jiná.

Přemýšlení o procházení grafů a nejkratších cestách by v tomto okamžiku mělo zazvonit: vyhledávání na prvním místě je algoritmus pro procházení grafů, který zaručuje nalezení nejkratší cesty od počátečního uzlu po jakýkoli jiný dosažitelný uzel grafu.

Po všech těchto úvodních ukázkách je zde implementace učebnice takového přechodu grafu. (Některé podrobnosti a pomocné metody byly vynechány; další podrobnosti najdete v tomto článku ke stažení.):