Ile "ważą" obiekty ?

0

Od jakiegoś czasu pracuję nad projektem programu, który będzie musiał poradzić sobie z obróbką sporej ilości obiektów. Obiekty będą miały takie składowe jak: rodzaj, nazwa, opis, tabela serii ... i jeszcze parę innych. Aby operacje na nich zajmowały jak najmniej czasu pomyślałem, że można by np. w polu rodzaj (ok 2-3 rodzai) zamiast Stringa użyć byte, tam gdzie int, może da radę zastosować short itd. Problem jest ze Stringami i ogólnie obiektami bo nie mam pojęcia ile takie coś zajmuje bajtów ? czy zależy to od długości/wielkości (chyba powinno), ale gdzieś czytałem, że referencje mają stałą wielkość 32 albo 64 b (w zależności od systemu), więc wtedy nie byłoby różnicy czy mam Stringa str = ""; czy str = "abcd... i jeszcze milion znaków"; Oprócz tego pozostaje jeszcze kwestia MVJ - czy nie podmienia typów na inne jeżeli jest to opłacalne i możliwe ? Szukałem w dokumentacji informacji na temat sposobu reprezentacji obiektów ale nie wyczytałem tam tego co mnie interesuje były tylko reprezentacje typów podstawowych, że np. byte - jest na 8 bitach. short na 16 itp. Nasuwa to wniosek, że nie ma takiej informacji ponieważ "waga" obiektu czy raczej referencji zależy od wielkości klasy, ale to wszystko tylko moje domysły i wróżenie z fusów. Prosiłbym o rozwianie moich wątpliwości :)

1

ale gdzieś czytałem, że referencje mają stałą wielkość 32 albo 64 b (w zależności od systemu), więc wtedy nie byłoby różnicy czy mam Stringa str = ""; czy str = "abcd... i jeszcze milion znaków";

Referencja to 32 lub 64-bitowy (w zależności od systemu) wskaźnik; wskazuje on miejsce położenia obiektu w pamięci.
String str = "abcd"; zajmuje (w uproszczeniu): rozmiar_klasy(String) + 4 bajtów, a String str="1234567890"; już rozmiar_klasy(String) + 10, gdzie rozmiar_klasy to suma rozmiaru wszystkich jej pól.
Zatem jest różnica.

Aby operacje na nich zajmowały jak najmniej czasu pomyślałem, że można by np. w polu rodzaj (ok 2-3 rodzai) zamiast Stringa użyć byte, tam gdzie int, może da radę zastosować short itd.

Zmieniając int -> byte zyskasz najwyżej 3 bajty więcej wolnej pamięci per obiekt, więc o ile nie tworzysz milionów takich obiektów w jednej chwili, byłoby to nieopłacalne.
Jeżeli liczysz na jakikolwiek wzrost prędkości z takiej zmiany, będzie on naprawdę niewielki (ułamki milisekund, jeżeli w ogóle).

0

Rozumiem, dzięki za wyjaśnienie :) Gdyby jednak ktoś chciał jeszcze coś dodać to proszę się nie krępować, chętnie się douczę :)

1

Poczytaj o wzorcu projektowym "Pyłek". Być może jest to rozwiązanie Twoich problemów. Mi raz z outOfMemoryException zeszło do kilku MB :)
http://pl.wikipedia.org/wiki/Pyłek_(wzorzec_projektowy)

3

Każdy obiekt sklada się z:

  • niejawnego identyfikatora/ referencji do klasy,
  • niejawnego pola do użycia przez JVM,
  • opcjonalnie: zestawu prymitywów,
  • opcjonalnie: zestawu referencji,
    Niejawne pola zajmują chyba jakieś 8 bajtów na typowej JVM (czyli taki jest minimalny rozmiar obiektu), a rozmiar samych obiektów jest zaokrąglany w górę do wielokrotności 8 bajtów.

Prymityw to np int, byte, short, char, float, double, ale już nie Object, String, Integer, czy dowolny obiekt dziedziczący po Object (także przechodnio, tzn prymityw instanceof Object nie kompiluje się). Prymityw jest bytem samym w sobie, tzn pamięć którą zajmuje zawiera wartość tego prymitywu.

Referencja to inny twór. Jest to obszar pamięci (w zasadzie to pewna liczba) przechowujący adres do obiektu. Rozmiar referencji jest generalnie stały, natomiast rozmiar obiektu wskazywanego przez referencję może być dowolny (dodatni, podzielny przez 8 bajtów).

W Javie nie operuje się na obiektach bezpośrednio, tzn nie można np przesłać obiektu do metody. Przesyła się jedynie prymitywy albo referencje. Co ważne - przesyła się je wszystkie przez wartość, tzn jeśli prześlemy prymityw to zostanie skopiowana jego wartość i jeśli mamy parametr metody typu prymityw i zmienimy wartość tego parametru to w funkcji wywołującej wartość oryginalnego prymitywu się nie zmieni. W przypadku referencji jest dokładnie analogicznie. Gdy podmienimy parametr typu referencyjnego to oryginalna referencja w procedurze wywołującej dalej będzie wskazywać na ten sam obiekt na który wskazywała. Trzeba natomiast zwrócić uwagę na to, że jeśli mamy wiele referencji do tego samego obiektu, to zmiana tego obiektu poprzez dowolną referencję będzie widoczna poprzez wszystkie te referencje. Wniosek jest taki, że jeśli chcesz zaemulować przesyłanie referencji czy prymitywów przez referencję to musisz je opakować w obiekt. Mając wiele referencji do konkretnego obiektu zawierającego referencję (tudzież prymityw) pozwoli na zobaczenie zmiany tejże opakowanej referencji (tudzież prymitywu) z wielu miejsc.

Rozmiar obiektu jest pojęciem nie do końca jednoznacznym. Przykład tutaj: http://www.yourkit.com/docs/80/help/sizes.jsp Shallow size oznacza rozmiar prymitywów i referencji należących do obiektu bezpośrednio, a więc jest to rozmiar samego obiektu bez obiektów przez niego wskazywanych. Retained size to z kolei rozmiar wszystkich obiektów, które są dostępne, pośrednio lub bezpośrednio, tylko z danego obiektu plus rozmiar danego obiektu - inaczej mówiąc jest to ilość pamięci, która może być zwolniona przy zwalnianiu pamięci danego obiektu (zakładając, że do grafu referencji nie dojdą nowe połączenia).

0
Wibowit napisał(a):

Każdy obiekt sklada się z:

  • niejawnego identyfikatora/ referencji do klasy,
  • niejawnego pola do użycia przez JVM,
  • opcjonalnie: zestawu prymitywów,
  • opcjonalnie: zestawu referencji,
    Niejawne pola zajmują chyba jakieś 8 bajtów na typowej JVM (czyli taki jest minimalny rozmiar obiektu), a rozmiar samych obiektów jest zaokrąglany w górę do wielokrotności 8 bajtów.

Sprawdziłem, ile zajmują przykładowe obiekty na OpenJdk 7 32-bit za pomocą:
http://docs.oracle.com/javase[...]ectSize%28java.lang.Object%29

  1. Instancja "pustej" klasy zajmuje 8 bajtów (class Foo {})

  2. Jeżeli dołożymy jakieś pole typu prymitywnego, to zwiększy się do 16 bajtów.
    np. class Foo {boolean b;}
    Każdy z booleanów zajmuje 1 bajt. Możemy dołożyć więc do 8 booleanów i cały czas instancja będzie mieć 16 bajty,
    np. class Foo {boolean b1;boolean b2;boolean b3;boolean b4;boolean b5;boolean b6;boolean b7;boolean b8}
    Dołożenie dziewiątego booleana zwiększy instancję do 24 bajtów.

  3. Referencja zajmuje 4 bajty (na 32 bitowym JVM).
    np. class Foo {Foo f1; Foo f2;}
    zajmuje 16 bajtów. Dołożenie "Foo f3" zwiększy rozmiar do 24 bajtów.

P.S. Ciekawe, że boolean zajmuje tyle samo co byte. Teoretycznie mógłby zajmować 1 bit, nie 1 bajt.

0

Pusta klasa ma 8 bajtów ponieważ jedyne co zawiera, to niejawny identyfikator/referencję potrzebną VM do jej identyfikacji oraz do tablicy metod wirtualnych tego obiektu (tu nie jestem pewien czy są to dwie wartości 32-bit czy jedna 32-bit z wyrównaniem do 64-bit, a w przyszłości jedna 64-bit - nie chce mi się sprawdzać). Wszystkie pola obiektów są wyrównywane co 64-bity, czyli 8 bajtów. Chyba traf chce, że wszystkie dane przesyłane są we współczesnych komputerach w paczkach po 64-bity, czyli właśnie 8 bajtów. ;-)
Powodem jest po prostu wydajność - najmniejsze opóźnienia są przy dostępie do podwójnego 32-bitowego słowa maszynowego, którego adres jest wielokrotnością 8 (zwykle 9-12 ns na maszynach z DDR3). Co prawda na maszynach z pamięcią w trybie dual channel przesyłane jest na raz 128-bitów, ale to szczegół, który sprawi, że za X dekad standardem może stać się wyrównywanie do 16 bajtów. Ostatnimi komputerami w których miało sens wyrównywanie do 4 bajtów to były maszyny z procesorami Intel 80386 (lub starszymi) i pamięciami SDR, czyli dzisiaj antyki.
Stąd dołożenie jakiegokolwiek pojedynczego pola musi zwiększyć obiekt o kolejne 8 bajtów - chyba, że kolejne z nich zmieszczą się w tym rozmiarze. Dlatego obiekt 16-bajtowy może mieć jawne, np.: 2 pola int, 4 pola char lub short, 8 pól byte, 2 referencje po 32-bit lub jedną 64-bit. Tak więc obiekt zawierający kolejne dwa jawne pola tablicy np. byte[] lub Object[] lub każdą kombinację referencji i dowolnego innego pola też powinien zajmować 16 bajtów (w Javie 32-bit).

@__krzysiek85
Powodem dla którego boolean zajmuje 8-bitów, a nie 1-bit jest również wydajność. Chodzi o to, że dzięki temu boolean może być zmienną, na której operacje przypisania oraz odczytu są na współczesnych CPU wciąż operacjami atomowymi. Dzięki temu zmiennych tego typu nie trzeba synchronizować (co najwyżej dorzucić volatile dla uwidocznienia zmian w innych wątkach). W ten sposób +boolean może trzymać stan obiektów bez potrzeby ich specjalnej synchronizacji (lub tej pochodzącej z samego języka). Drugą korzyścią jest łatwa możliwość niesynchronizowanego eksportu informacji do obiektu Boolean, który jest z kolei niemutowalny. Gdyby boolean był pakowany po 8 w bajcie, to operacja odczytu musiałaby być operacją nieatomową, a tym samym poddawana bardzo kosztownej czasowo synchronizacji. Nie można więc było podjąć lepszej decyzji niż upakować 1 bit w jednym bajcie, a dane pakowane - którym synchronizacja nie jest potrzebna - wrzucić do zwykłego obiektu jako paczki słów maszynowych o najszybszym dostępie, czyli long[] (elementy po 64-bit).

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0