Я пытаюсь понять реальный процесс создания объектов в Java - и я предполагаю, что другие языки программирования.
Было бы неправильно предполагать, что инициализация объекта в Java такая же, как и при использовании malloc для структуры в C?
Пример:
Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));
Поэтому говорят, что объекты находятся в куче, а не в стеке? Потому что они просто указатели на данные?
scalar-replacement
) на простые поля, которые живут только в стеке; но это то, чтоJIT
делает, а неjavac
.Ответы:
В C
malloc()
выделяет область памяти в куче и возвращает указатель на нее. Это все, что вы получаете. Память не инициализирована, и у вас нет гарантии, что это все нули или что-то еще.В Java, вызов
new
делает распределение на основе кучи точно так жеmalloc()
, но вы также получаете массу дополнительных удобств (или накладных расходов, если хотите). Например, вам не нужно явно указывать количество байтов для выделения. Компилятор выяснит это для вас в зависимости от типа объекта, который вы пытаетесь выделить. Кроме того, вызываются конструкторы объектов (которым вы можете передавать аргументы, если хотите контролировать процесс инициализации). Приnew
возврате вы гарантированно получите инициализированный объект.Но да, в конце вызова и результат,
malloc()
иnew
просто указатели на некоторый кусок данных на основе кучи.Вторая часть вашего вопроса касается различий между стеком и кучей. Гораздо более полные ответы можно найти, пройдя курс по (или читая книгу) о дизайне компилятора. Курс по операционным системам также был бы полезен. Есть также многочисленные вопросы и ответы на SO о стеках и кучах.
Сказав это, я дам общий обзор, который, я надеюсь, не слишком многословен и имеет целью объяснить различия на достаточно высоком уровне.
По сути, основная причина наличия двух систем управления памятью, то есть кучи и стека, заключается в эффективности . Вторая причина заключается в том, что каждый из них лучше справляется с определенными типами проблем, чем другой.
Стеки мне легче понять как концепцию, поэтому я начну со стеков. Давайте рассмотрим эту функцию в C ...
Вышеизложенное кажется довольно простым. Мы определяем функцию с именем
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 ГГц процессоре).Кроме того, с точки зрения компилятора он может создать карту для переменных, которая выглядит очень похоже на индексирование массива:
Опять же, все это очень быстро.
Когда
add()
функция выходит, она должна очиститься. Это делается путем вычитания 12 байтов из регистра указателя стека. Это похоже на вызов,free()
но он использует только одну инструкцию процессора и занимает всего один тик. Это очень, очень быстро.Теперь рассмотрим распределение на основе кучи. Это вступает в игру, когда мы априори не знаем, сколько памяти нам понадобится (т.е. мы узнаем об этом только во время выполнения).
Рассмотрим эту функцию:
Обратите внимание, что
addRandom()
функция не знает во время компиляции, каким будет значениеcount
аргумента. Из-за этого нет смысла пытаться определить,array
как если бы мы помещали его в стек, например:Если
count
он велик, это может привести к тому, что наш стек станет слишком большим и перезапишет другие сегменты программы. Когда происходит переполнение стека , ваша программа падает (или хуже).Таким образом, в случаях, когда мы не знаем, сколько памяти нам понадобится до времени выполнения, мы используем
malloc()
. Затем мы можем просто запросить количество байтов, которое нам нужно, когда нам это нужно, иmalloc()
проверим, может ли оно продать столько байтов. Если это возможно, отлично, мы получаем его обратно, если нет, мы получаем указатель NULL, который сообщает нам, что вызовmalloc()
fail. Примечательно, что программа не падает! Конечно, вы, как программист, можете решить, что ваша программа не может быть запущена, если выделение ресурсов завершится неудачно, но завершение, инициированное программистом, отличается от ложного сбоя.Так что теперь мы должны вернуться, чтобы посмотреть на эффективность. Распределитель стека очень быстрый - одна инструкция для выделения, одна инструкция для освобождения, и это делается компилятором, но помните, что стек предназначен для таких вещей, как локальные переменные известного размера, поэтому он обычно довольно мал.
Распределитель кучи, с другой стороны, на несколько порядков медленнее. Он должен выполнить поиск в таблицах, чтобы увидеть, достаточно ли у него свободной памяти, чтобы иметь возможность продавать объем памяти, который требуется пользователю. Он должен обновить эти таблицы после продажи памяти, чтобы никто другой не смог использовать этот блок (эта бухгалтерия может потребовать, чтобы распределитель резервировал память для себя в дополнение к тому, что он планирует продавать). Распределитель должен использовать стратегии блокировки, чтобы гарантировать, что он продает память потокобезопасным способом. И когда память наконец
free()
d, что происходит в разное время и обычно в непредсказуемом порядке, распределитель должен находить смежные блоки и объединять их вместе для восстановления фрагментации кучи. Если это звучит так, как будто для выполнения всего этого потребуется более одной инструкции CPU, вы правы! Это очень сложно и требует времени.Но кучи большие. Гораздо больше, чем стеки. Мы можем получить от них много памяти, и они великолепны, когда во время компиляции мы не знаем, сколько памяти нам понадобится. Таким образом, мы компенсируем скорость для системы управляемой памяти, которая вежливо отказывает нам, вместо того, чтобы падать, когда мы пытаемся выделить что-то слишком большое.
Я надеюсь, что это поможет ответить на некоторые ваши вопросы. Пожалуйста, дайте мне знать, если вы хотите получить разъяснения по любому из вышеперечисленных.
источник
int
не является 8 байтами на 64-битной платформе. Это все еще 4. Наряду с этим, компилятор, весьма вероятно, оптимизирует третийint
из стека в регистр возврата. Фактически, эти два аргумента также могут быть в регистрах на любой 64-битной платформе.int
s на 64-битных платформах. Вы правы, чтоint
в Java остается 4 байта. Однако, я оставил остаток от моего ответа, потому что я считаю, что оптимизация компилятора ставит корзину впереди лошади. Да, вы также правы в этих вопросах , но вопрос требует уточнения стеков и куч. RVO, передача аргументов через регистры, исключение кода и т. Д. Перегружают основные концепции и мешают пониманию основ.