Являются ли инициализация объекта в 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.
Евгений,
«Куча» - это просто имя для набора свойств, связанных с выделенными объектами / памятью. В C / C ++ вы можете выбрать один из двух различных наборов свойств, называемых «стек» и «куча», в C # и Java все распределения объектов имеют одинаковое заданное поведение, которое идет под именем «куча», что не подразумевают, что эти свойства такие же, как для «кучи» 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 ГГц процессоре).

Кроме того, с точки зрения компилятора он может создать карту для переменных, которая выглядит очень похоже на индексирование массива:

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()fail. Примечательно, что программа не падает! Конечно, вы, как программист, можете решить, что ваша программа не может быть запущена, если выделение ресурсов завершится неудачно, но завершение, инициированное программистом, отличается от ложного сбоя.

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

Распределитель кучи, с другой стороны, на несколько порядков медленнее. Он должен выполнить поиск в таблицах, чтобы увидеть, достаточно ли у него свободной памяти, чтобы иметь возможность продавать объем памяти, который требуется пользователю. Он должен обновить эти таблицы после продажи памяти, чтобы никто другой не смог использовать этот блок (эта бухгалтерия может потребовать, чтобы распределитель резервировал память для себя в дополнение к тому, что он планирует продавать). Распределитель должен использовать стратегии блокировки, чтобы гарантировать, что он продает память потокобезопасным способом. И когда память наконецfree()d, что происходит в разное время и обычно в непредсказуемом порядке, распределитель должен находить смежные блоки и объединять их вместе для восстановления фрагментации кучи. Если это звучит так, как будто для выполнения всего этого потребуется более одной инструкции CPU, вы правы! Это очень сложно и требует времени.

Но кучи большие. Гораздо больше, чем стеки. Мы можем получить от них много памяти, и они великолепны, когда во время компиляции мы не знаем, сколько памяти нам понадобится. Таким образом, мы компенсируем скорость для системы управляемой памяти, которая вежливо отказывает нам, вместо того, чтобы падать, когда мы пытаемся выделить что-то слишком большое.

Я надеюсь, что это поможет ответить на некоторые ваши вопросы. Пожалуйста, дайте мне знать, если вы хотите получить разъяснения по любому из вышеперечисленных.

паритет
источник
intне является 8 байтами на 64-битной платформе. Это все еще 4. Наряду с этим, компилятор, весьма вероятно, оптимизирует третий intиз стека в регистр возврата. Фактически, эти два аргумента также могут быть в регистрах на любой 64-битной платформе.
SS Anne
Я отредактировал свой ответ, чтобы удалить утверждение о 8-байтовых ints на 64-битных платформах. Вы правы, что intв Java остается 4 байта. Однако, я оставил остаток от моего ответа, потому что я считаю, что оптимизация компилятора ставит корзину впереди лошади. Да, вы также правы в этих вопросах , но вопрос требует уточнения стеков и куч. RVO, передача аргументов через регистры, исключение кода и т. Д. Перегружают основные концепции и мешают пониманию основ.
пар