В 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, вы правы! Это очень сложно и требует времени.
Но кучи большие. Гораздо больше, чем стеки. Мы можем получить от них много памяти, и они великолепны, когда во время компиляции мы не знаем, сколько памяти нам понадобится. Таким образом, мы компенсируем скорость для системы управляемой памяти, которая вежливо отказывает нам, вместо того, чтобы падать, когда мы пытаемся выделить что-то слишком большое.
Я надеюсь, что это поможет ответить на некоторые ваши вопросы. Пожалуйста, дайте мне знать, если вы хотите получить разъяснения по любому из вышеперечисленных.