Próbuję zrozumieć rzeczywisty proces tworzenia obiektów w Javie - i przypuszczam, że inne języki programowania.
Czy błędem byłoby założenie, że inicjalizacja obiektu w Javie jest taka sama, jak w przypadku użycia malloc dla struktury w C?
Przykład:
Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));
Czy dlatego mówi się, że obiekty znajdują się na stosie, a nie na stosie? Ponieważ są to zasadniczo wskaźniki do danych?
scalar-replacement
) na zwykłe pola, które żyją tylko na stosie; ale to jest coś,JIT
co niejavac
.Odpowiedzi:
W C
malloc()
przydziela region pamięci w stercie i zwraca do niego wskaźnik. To wszystko, co dostajesz. Pamięć jest niezainicjowana i nie masz gwarancji, że są to zera lub cokolwiek innego.W Javie wywoływanie
new
dokonuje alokacji stosu, podobnie jakmalloc()
, ale zyskujesz także mnóstwo dodatkowej wygody (lub obciążenia, jeśli wolisz). Na przykład nie musisz jawnie określać liczby bajtów do przydzielenia. Kompilator oblicza to dla Ciebie na podstawie typu obiektu, który próbujesz przydzielić. Dodatkowo wywoływane są konstruktory obiektów (do których można przekazywać argumenty, jeśli chcesz kontrolować sposób inicjowania). Ponew
powrocie masz zagwarantowany obiekt, który został zainicjowany.Ale tak, pod koniec rozmowy zarówno wynik
malloc()
inew
wskaźniki są po prostu wskazówkami na jakąś część danych opartych na stercie.Druga część twojego pytania dotyczy różnic między stosem a stertą. O wiele bardziej wyczerpujące odpowiedzi można znaleźć, biorąc udział w kursie poświęconym projektowaniu kompilatora (lub czytając książkę na jego temat). Pomocny byłby również kurs na temat systemów operacyjnych. Istnieje również wiele pytań i odpowiedzi na temat stosów i stosów.
Powiedziawszy to, dam ogólny przegląd, mam nadzieję, że nie jest zbyt gadatliwy i ma na celu wyjaśnienie różnic na dość wysokim poziomie.
Zasadniczo głównym powodem posiadania dwóch systemów zarządzania pamięcią, tj. Sterty i stosu, jest wydajność . Drugim powodem jest to, że każdy z nich jest lepszy w przypadku niektórych rodzajów problemów niż drugi.
Stosy są nieco łatwiejsze do zrozumienia jako koncepcja, więc zaczynam od stosów. Rozważmy tę funkcję w C ...
Powyższe wydaje się dość proste. Definiujemy funkcję o nazwie
add()
i przekazujemy w lewym i prawym uzupełnieniu. Funkcja dodaje je i zwraca wynik. Zignoruj wszystkie przypadki skrajnych przypadków, takie jak przepełnienia, które mogą wystąpić, w tym momencie nie jest to istotne dla dyskusji.The
add()
funkcji wydaje się dość prosty, ale co możemy powiedzieć o jej cyklu życia? Zwłaszcza potrzeba wykorzystania pamięci?Co najważniejsze, kompilator wie a priori (tj. W czasie kompilacji), jak duże są typy danych i ile będzie używanych.
lhs
Irhs
argumenty sąsizeof(int)
4 bajty każda. Zmiennaresult
jest równieżsizeof(int)
. Kompilator może stwierdzić, żeadd()
funkcja go używa4 bytes * 3 ints
lub w sumie 12 bajtów pamięci.Po
add()
wywołaniu funkcji rejestr sprzętowy zwany wskaźnikiem stosu będzie miał w nim adres wskazujący na górę stosu. Aby przydzielić pamięć, którąadd()
musi uruchomić funkcja, wszystko, co musi zrobić kod wprowadzania funkcji, to wydać jedną instrukcję języka asemblera, aby zmniejszyć wartość rejestru wskaźnika stosu o 12. W ten sposób tworzy miejsce na stosie dla trzechints
, po jednym dla każdegolhs
,rhs
iresult
. Uzyskanie potrzebnej przestrzeni pamięci przez wykonanie pojedynczej instrukcji jest ogromną wygraną pod względem szybkości, ponieważ pojedyncze instrukcje mają tendencję do wykonywania w jednym takcie zegara (1 miliardowa sekunda procesora 1 GHz).Ponadto, z widoku kompilatora, może utworzyć mapę do zmiennych, które wyglądają okropnie podobnie jak indeksowanie tablicy:
Znowu wszystko to jest bardzo szybkie.
Po
add()
wyjściu z funkcji należy wyczyścić. Odbywa się to poprzez odjęcie 12 bajtów od rejestru wskaźnika stosu. Jest podobny do wywołania,free()
ale korzysta tylko z jednej instrukcji procesora i wymaga tylko jednego tiku. Jest bardzo, bardzo szybki.Teraz rozważ alokację na stercie. To wchodzi w grę, gdy nie wiemy a priori, ile pamięci będziemy potrzebować (tj. Dowiemy się o tym tylko w czasie wykonywania).
Rozważ tę funkcję:
Zauważ, że
addRandom()
funkcja nie wie w czasie kompilacji, jaka będzie wartośćcount
argumentu. Z tego powodu nie ma sensu próbować zdefiniowaćarray
tak, jak byśmy to zrobili, gdybyśmy umieszczali go na stosie:Jeśli
count
jest ogromny, może spowodować, że nasz stos będzie zbyt duży i zastąpi inne segmenty programu. Gdy ten stos się przepełni nastąpi , program ulega awarii (lub gorzej).Tak więc w przypadkach, gdy nie wiemy, ile pamięci potrzebujemy do czasu wykonania, używamy
malloc()
. Następnie możemy po prostu poprosić o liczbę bajtów, której potrzebujemy, gdy jej potrzebujemy, imalloc()
sprawdzimy, czy może sprzedać tyle bajtów. Jeśli to możliwe, świetnie, odzyskujemy go, jeśli nie, otrzymujemy wskaźnik NULL, który mówi nam, że wywołaniemalloc()
nie powiodło się. Warto zauważyć, że program nie ulega awarii! Oczywiście jako programista możesz zdecydować, że Twój program nie będzie mógł działać, jeśli alokacja zasobów nie powiedzie się, ale zakończenie inicjowane przez programistę różni się od fałszywej awarii.Więc teraz musimy wrócić, aby spojrzeć na wydajność. Alokator stosu jest super szybki - jedna instrukcja do alokacji, jedna instrukcja do dezalokacji i jest wykonywana przez kompilator, ale pamiętaj, że stos jest przeznaczony do takich zmiennych lokalnych o znanym rozmiarze, więc wydaje się być dość mały.
Z drugiej strony alokator sterty jest wolniejszy o kilka rzędów wielkości. Musi przejrzeć tabele, aby sprawdzić, czy ma wystarczającą ilość wolnej pamięci, aby móc sprzedać ilość pamięci, jakiej chce użytkownik. Musi zaktualizować te tabele po tym, jak vending pamięci, aby upewnić się, że nikt inny nie może użyć tego bloku (ta księgowość może wymagać od alokatora zarezerwowania pamięci dla siebie oprócz tego, co planuje sprzedać). Alokator musi zastosować strategie blokowania, aby upewnić się, że pamięć jest zabezpieczona w sposób bezpieczny dla wątków. A kiedy pamięć wreszcie się pojawi
free()
d, co dzieje się w różnym czasie i zazwyczaj w nieprzewidywalnej kolejności, alokator musi znaleźć ciągłe bloki i połączyć je z powrotem, aby naprawić fragmentację hałdy. Jeśli brzmi to tak, jakbyś potrzebował więcej niż jednej instrukcji procesora, aby to wszystko osiągnąć, masz rację! To bardzo skomplikowane i zajmuje trochę czasu.Ale stosy są duże. Dużo większy niż stosy. Możemy uzyskać od nich dużo pamięci i są świetne, gdy nie wiemy w czasie kompilacji, ile pamięci będziemy potrzebować. Wymieniamy więc prędkość na system pamięci zarządzanej, który grzecznie odmawia nam zamiast zawieszać się, gdy próbujemy przydzielić coś zbyt dużego.
Mam nadzieję, że to pomoże odpowiedzieć na niektóre twoje pytania. Daj mi znać, jeśli chcesz wyjaśnić którekolwiek z powyższych.
źródło
int
nie ma 8 bajtów na platformie 64-bitowej. Nadal jest 4. Wraz z tym kompilator najprawdopodobniej zoptymalizuje trzeciint
ze stosu do rejestru zwrotnego. W rzeczywistości te dwa argumenty prawdopodobnie znajdują się w rejestrach na dowolnej platformie 64-bitowej.int
na platformach 64-bitowych. Masz rację, żeint
w Javie pozostają 4 bajty. Pozostałą resztę odpowiedzi zostawiłem jednak, ponieważ uważam, że optymalizacja kompilatora stawia wózek przed koniem. Tak, masz rację również w tych punktach, ale pytanie wymaga wyjaśnienia na temat stosów vs. stosów. RVO, przekazywanie argumentów przez rejestry, eliminacja kodu itp. Przeciąża podstawowe pojęcia i przeszkadza w zrozumieniu podstaw.