Чи ініціалізація об’єктів у Java "Foo f = new Foo ()" по суті однакова, як використання malloc для вказівника на C?


9

Я намагаюся зрозуміти фактичний процес створення об'єктів на Java - і я думаю, що інші мови програмування.

Чи було б неправильним вважати, що ініціалізація об’єктів у Java така ж, як і коли ви використовуєте malloc для структури в C?

Приклад:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

Це чому, як кажуть, предмети знаходяться на купі, а не на стеці? Тому що вони по суті є лише вказівниками на дані?


Об'єкти створюються на купі для керованих мов, таких як c # / java. У cpp ви можете так само добре створювати об'єкти на стеці
bas

Чому творці Java / C # вирішили зберігати виключно об’єкти в купі?
Жуль

Я думаю , що для простоти. Зберігання об'єктів у стеку та передача їх на рівень глибше, передбачає копіювання об'єкта на стек, що включає конструкторів копіювання. Я не відповів Google на правильну відповідь, але впевнений, що ви можете знайти собі більш задоволену відповідь (або хтось інший розробить це побічне запитання)
bas

@Jules об’єкти в java ще можуть бути "розкладені" під час виконання (викликаються scalar-replacement) просто простими полями, які живуть лише на стеку; але це щось JITробить, ні javac.
Євген

"Heap" - це лише назва набору властивостей, пов'язаних з виділеними об'єктами / пам'яттю. У C / C ++ ви можете вибрати з двох різних наборів властивостей, званих "стек" і "купа", в C # і Java, всі виділення об'єктів мають однакову задану поведінку, що йде під назвою "heap", що не мається на увазі, що ці властивості такі ж, як і для "купи" C / C ++, насправді вони не є. Це не означає, що реалізація не може мати різні стратегії управління об'єктами, це означає, що ці стратегії не мають значення для логіки програми.
Холгер

Відповіді:


5

В C malloc()виділяє область пам’яті в купу і повертає на неї вказівник. Це все, що ви отримуєте. Пам'ять неініціалізована, і ви не маєте гарантії, що це все нулі чи щось інше.

У Java виклик newробить розподіл на основі купи так само malloc(), але ви також отримуєте тону додаткової зручності (або накладні витрати, якщо хочете). Наприклад, не потрібно чітко вказувати кількість байтів, які потрібно виділити. Компілятор розраховує це для вас на основі типу об'єкта, який ви намагаєтеся виділити. Крім того, викликаються конструктори об'єктів (яким ви можете передавати аргументи, якщо ви хочете контролювати, як відбувається ініціалізація). Після newповернення вам гарантовано буде об’єкт, який ініціалізується.

Але так, наприкінці дзвінка і результат, malloc()і newпросто вказівки на деякий фрагмент даних на основі купи.

Друга частина вашого запитання задає питання про відмінності між стеком і групою. Набагато вичерпніші відповіді можна знайти, взявши курс (або прочитавши книгу про) проекту компілятора. Курс з операційних систем також буде корисним. Також є численні запитання та відповіді на SO про стеки та купи.

Сказавши це, я дам загальний огляд, сподіваюся, не надто багатослівний і спрямований на пояснення відмінностей на досить високому рівні.

По суті, основна причина наявності двох систем управління пам'яттю, тобто купа та стека, - це ефективність . Вторинна причина полягає в тому, що кожен краще вирішує певні типи проблем, ніж інші.

Стеки мені дещо простіше зрозуміти як поняття, тому я починаю з стеків. Розглянемо цю функцію в C ...

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

Сказане здається досить відвертим. Визначимо функцію з ім'ям add()і передаємо в лівому і правому додаваннях. Функція додає їх і повертає результат. Будь ласка, ігноруйте всі кращі регістри, такі як переповнення, які можуть виникнути, і на даний момент це не є предметом обговорення.

Призначення add()функції здається досить простим, але що ми можемо сказати про його життєвий цикл? Особливо потребує використання пам'яті?

Найголовніше, що компілятор апріорі знає (тобто під час компіляції), наскільки великі типи даних і скільки буде використано. lhsІ rhsаргументи sizeof(int), 4 байта кожен. Змінна resultтакож sizeof(int). Компілятор може сказати, що add()функція використовує 4 bytes * 3 intsабо загалом 12 байт пам'яті.

Коли add()функція викликається, апаратний регістр під назвою покажчик стека матиме в ній адресу, яка вказує на верхню частину стека. Для виділення пам'яті, яку add()функція повинна запускати, все, що потрібно ввести код функції, - це видати одну єдину інструкцію мови збірки, щоб зменшити значення регістру вказівника стека на 12. При цьому він створює сховище в стеку на три ints, по одному для кожного lhs, rhsі result. Отримати необхідний простір пам’яті, виконавши одну інструкцію, - це величезна виграш у швидкості, тому що одні інструкції, як правило, виконуються за один тактовий годинник (1 мільярд секунди 1 процесор 1 ГГц).

Також, з точки зору компілятора, він може створити карту змінних, яка виглядає жахливо, як індексація масиву:

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

Знову ж таки, все це дуже швидко.

Коли add()функція закінчується, вона повинна очищатись. Це робиться шляхом віднімання 12 байтів з регістра вказівника стека. Це схоже на дзвінок, free()але він використовує лише одну інструкцію процесора, і він займає лише одну галочку. Це дуже, дуже швидко.


Тепер розглянемо розподіл на основі купи. Це грає, коли ми апріорі не знаємо, скільки пам'яті нам знадобиться (тобто ми дізнаємося про це лише під час виконання).

Розглянемо цю функцію:

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;
}

Зауважте, що addRandom()функція не знає під час компіляції, яким буде значення countаргументу. Через це не має сенсу намагатися визначитись так, arrayяк ми, якби ставити його на стек, як це:

int array[count];

Якщо countвін величезний, це може призвести до того, що наш стек стає занадто великим і замінить інші сегменти програми. Коли ця переповнення стека трапиться, програма завершиться (або гірше).

Тож у тих випадках, коли ми не знаємо, скільки пам'яті нам знадобиться до часу виконання, ми використовуємо malloc(). Тоді ми можемо просто запитати кількість потрібних нам байтів, коли нам це потрібно, і malloc()перевіримо, чи може він продати стільки байтів. Якщо це можливо, чудово, ми повертаємо його назад, якщо ні, то отримуємо вказівник NULL, який повідомляє нам, що виклик malloc()не вдався. Примітно, що програма не виходить з ладу! Звичайно, ви як програміст можете вирішити, що ваша програма не може працювати, якщо розподіл ресурсів не вдається, але ініційоване програмуванням припинення відрізняється від помилкової аварії.

Тож тепер ми повинні повернутися, щоб подивитися на ефективність. Розподільник стеків дуже швидкий - одна інструкція розподілити, одна інструкція розібрати, і це робиться компілятором, але пам’ятайте, що стек призначений для таких речей, як локальні змінні відомого розміру, тому він, як правило, досить малий.

З іншого боку, розподільник купи на кілька порядків повільніше. Він повинен здійснити пошук у таблицях, щоб побачити, чи є у нього достатньо вільної пам'яті, щоб можна було продати об'єм пам'яті, який хоче користувач. Після оновлення пам'яті він повинен оновити ці таблиці, щоб переконатися, що ніхто більше не може використовувати цей блок (ця бухгалтерія може зажадати від розподільника резервувати пам'ять для себе на додаток до того, що планує продавати). Алокатор повинен використовувати стратегії блокування, щоб переконатися, що він поширює пам'ять безпечним способом. І коли нарешті пам’ятьfree()d, що відбувається в різний час і в непередбачуваному порядку, як правило, алокатор повинен знаходити суміжні блоки і зшивати їх назад, щоб виправити фрагмент купи. Якщо це здається, що для виконання всього цього знадобиться більше однієї інструкції процесора, ви маєте рацію! Це дуже складно і потребує певного часу.

Але купи великі. Набагато більше, ніж стеки. Ми можемо отримати від них багато пам’яті, і вони чудові, коли ми не знаємо, коли компілювати, скільки пам’яті нам знадобиться. Таким чином, ми торгуємо швидкістю для керованої системи пам’яті, яка ввічливо відхиляє нас, а не дає збоїв, коли ми намагаємося виділити щось занадто велике.

Я сподіваюся, що це допоможе відповісти на деякі ваші запитання. Будь ласка, дайте мені знати, чи хочете ви пояснити щось із вищезазначеного.


int- це не 8 байт на 64-бітній платформі. Це все-таки 4. Поряд з цим, компілятор дуже ймовірно оптимізує третій intз стека в регістр повернення. Насправді, два аргументи також, ймовірно, є в регістрах на будь-якій 64-бітній платформі.
СС Енн

Я відредагував свою відповідь, щоб видалити твердження про 8-байт intна 64-бітних платформах. Ви вірні, що intна Java залишається 4-байтний. Однак я залишив решту своєї відповіді, тому що я вважаю, що потрапляння в оптимізацію компілятора ставить візок перед конем. Так, ви також правильно в цих питаннях , але питання вимагає уточнення щодо стеків проти купи. RVO, аргумент, що проходить через регістри, усунення коду тощо, перевантажує основні поняття і перешкоджає розумінню основ.
пар.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.