Czy inicjalizacje obiektów w Javie „Foo f = new Foo ()” są zasadniczo takie same jak przy użyciu malloc jako wskaźnika w C?

9

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?

Jules
źródło
Obiekty są tworzone na stercie dla zarządzanych języków, takich jak c # / java. W cpp możesz również tworzyć obiekty na stosie
bas
Dlaczego twórcy Java / C # zdecydowali się przechowywać wyłącznie obiekty na stercie?
Jules
I pomyśleć dla uproszczenia. Przechowywanie obiektów na stosie i przekazywanie ich głębiej wymaga kopiowania obiektu na stos, co wiąże się z kopiowaniem konstruktorów. Nie szukałem poprawnej odpowiedzi w Google, ale jestem pewien, że sam znajdziesz bardziej satysfakcjonującą odpowiedź (lub ktoś inny opracuje to pytanie poboczne)
bas
Obiekty @Jules w Javie nadal mogą być „dekomponowane” w czasie wykonywania (wywoływane scalar-replacement) na zwykłe pola, które żyją tylko na stosie; ale to jest coś, JITco nie javac.
Eugene
„Sterta” to tylko nazwa zestawu właściwości związanych z przydzielonymi obiektami / pamięcią. W C / C ++ możesz wybierać z dwóch różnych zestawów właściwości, zwanych „stosami” i „stertą”, w C # i Javie wszystkie przydziały obiektów mają to samo określone zachowanie, które nosi nazwę „sterty”, co nie sugerują, że te właściwości są takie same jak dla „sterty” C / C ++, w rzeczywistości nie są. Nie oznacza to, że implementacje nie mogą mieć różnych strategii zarządzania obiektami, implikuje to, że strategie te są nieistotne dla logiki aplikacji.
Holger,

Odpowiedzi:

5

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 newdokonuje 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). Po newpowrocie masz zagwarantowany obiekt, który został zainicjowany.

Ale tak, pod koniec rozmowy zarówno wynik malloc() i newwskaź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 ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

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. lhsI rhsargumenty są sizeof(int)4 bajty każda. Zmienna resultjest 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 trzech ints, po jednym dla każdego lhs, rhsiresult . 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:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

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ę:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

Zauważ, że addRandom()funkcja nie wie w czasie kompilacji, jaka będzie wartość countargumentu. Z tego powodu nie ma sensu próbować zdefiniować arraytak, jak byśmy to zrobili, gdybyśmy umieszczali go na stosie:

int array[count];

Jeśli countjest 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, i malloc()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łanie malloc()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ę pojawifree()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.

par
źródło
intnie ma 8 bajtów na platformie 64-bitowej. Nadal jest 4. Wraz z tym kompilator najprawdopodobniej zoptymalizuje trzeci intze stosu do rejestru zwrotnego. W rzeczywistości te dwa argumenty prawdopodobnie znajdują się w rejestrach na dowolnej platformie 64-bitowej.
SS Anne,
Zedytowałem odpowiedź, aby usunąć oświadczenie o 8 bajtach intna platformach 64-bitowych. Masz rację, że intw 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.
par