В какой области памяти хранятся примитивы java

Обновлено: 07.07.2024

Изучите, как работает память стека и пространство кучи и когда их следует использовать для разработки лучших программ Java.

1. введение

Чтобы запустить приложение оптимальным образом, JVM делит память на память стека и кучи. Всякий раз, когда мы объявляем новые переменные и объекты, вызываем новый метод, объявляем Строку или выполняем аналогичные операции, JVM выделяет память для этих операций либо из памяти стека, либо из пространства кучи.

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

2. Стековая память в Java

Стековая память в Java используется для статического выделения памяти и выполнения потока. Он содержит примитивные значения, специфичные для метода, и ссылки на объекты, находящиеся в куче, на которые ссылаются из метода.

Когда метод завершает выполнение, соответствующий кадр стека сбрасывается, поток возвращается к вызывающему методу, и пространство становится доступным для следующего метода.

2.1. Основные характеристики стековой памяти

Помимо того, что мы обсуждали до сих пор, ниже приведены некоторые другие особенности стековой памяти:

  • Он растет и сжимается по мере вызова и возврата новых методов соответственно
  • Переменные внутри стека существуют только до тех пор, пока работает метод, который их создал
  • Он автоматически выделяется и освобождается, когда метод завершает выполнение
  • Если эта память заполнена, Java выдает java.lang.StackOverflowError
  • Доступ к этой памяти осуществляется быстро по сравнению с памятью кучи
  • Эта память является потокобезопасной, так как каждый поток работает в своем собственном стеке

3. Пространство кучи в Java

Пространство кучи в Java используется для динамического выделения памяти для объектов Java и классов JRE во время выполнения . Новые объекты всегда создаются в пространстве кучи, а ссылки на эти объекты хранятся в памяти стека.

Эти объекты имеют глобальный доступ и могут быть доступны из любой точки приложения.

Эта модель памяти далее разбивается на более мелкие части, называемые поколениями, это:

  1. Молодое поколение – именно здесь выделяются и стареют все новые объекты. Незначительная сборка мусора происходит, когда это заполняется
  2. Старое или арендованное поколение – здесь хранятся давно сохранившиеся объекты. Когда объекты хранятся в Молодом поколении, устанавливается пороговое значение для возраста объекта, и когда это пороговое значение достигнуто, объект перемещается в старое поколение
  3. Постоянная генерация – это состоит из метаданных JVM для классов среды выполнения и методов приложения

Эти различные части также обсуждаются в этой статье – Разница между JVM, JRE и JDK.

Мы всегда можем манипулировать размером памяти кучи в соответствии с нашими требованиями. Для получения дополнительной информации посетите эту связанную статью Baeldung .

3.1. Основные характеристики кучной памяти Java

Помимо того, что мы обсуждали до сих пор, ниже приведены некоторые другие особенности пространства кучи:

  • Доступ к нему осуществляется с помощью сложных методов управления памятью, которые включают в себя Молодое поколение, Старое или Штатное поколение и Постоянное поколение
  • Если пространство кучи заполнено, Java выдает java.lang.OutOfMemoryError
  • Доступ к этой памяти относительно медленнее, чем к стековой памяти
  • Эта память, в отличие от стека, не освобождается автоматически. Ему нужен сборщик мусора, чтобы освободить неиспользуемые объекты, чтобы сохранить эффективность использования памяти
  • В отличие от стека, куча не является потокобезопасной и должна быть защищена путем правильной синхронизации кода

4. Пример

Основываясь на том, что мы узнали до сих пор, давайте проанализируем простой Java-код и оценим, как здесь управляется память:

Давайте проанализируем это шаг за шагом:

При вводе метода main() в памяти стека будет создано пространство для хранения примитивов и ссылок этого метода

  • Примитивное значение integer id будет храниться непосредственно в памяти стека
  • Ссылочная переменная person типа Person также будет создана в памяти стека, которая будет указывать на фактический объект в куче

Вызов параметризованного конструктора Person(int, String) из main() выделит дополнительную память поверх предыдущего стека. Это будет хранить:

Привет, Хабр! Представляю вашему вниманию перевод первой части статьи «Java Memory Model» автора Jakob Jenkov.

Прохожу обучение по Java и понадобилось изучить статью Java Memory Model. Перевёл её для лучшего понимания, ну а чтоб добро не пропадало решил поделиться с сообществом. Думаю, для новичков будет полезно, и если кому-то понравится, то переведу остальное.

Первоначальная Java-модель памяти была недостаточно хороша, поэтому она была пересмотрена в Java 1.5. Эта версия модели все ещё используется сегодня (Java 14+).

Java-модель памяти, используемая внутри JVM, делит память на стеки потоков (thread stacks) и кучу (heap). Эта диаграмма иллюстрирует Java-модель памяти с логической точки зрения:

image

Каждый поток, работающий в виртуальной машине Java, имеет свой собственный стек. Стек содержит информацию о том, какие методы вызвал поток. Я буду называть это «стеком вызовов». Как только поток выполняет свой код, стек вызовов изменяется.

Стек потока содержит все локальные переменные для каждого выполняемого метода. Поток может получить доступ только к своему стеку. Локальные переменные, невидимы для всех других потоков, кроме потока, который их создал. Даже если два потока выполняют один и тот же код, они всё равно будут создавать локальные переменные этого кода в своих собственных стеках. Таким образом, каждый поток имеет свою версию каждой локальной переменной.

Все локальные переменные примитивных типов (boolean, byte, short, char, int, long, float, double) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.

Куча содержит все объекты, созданные в вашем приложении, независимо от того, какой поток создал объект. К этому относятся и версии объектов примитивных типов (например, Byte, Integer, Long и т.д.). Неважно, был ли объект создан и присвоен локальной переменной или создан как переменная-член другого объекта, он хранится в куче.

Ниже диаграмма, которая иллюстрирует стек вызовов и локальные переменные (они хранятся в стеках), а также объекты (они хранятся в куче):

image

Локальная переменная может быть примитивного типа, в этом случае она полностью хранится в стеке потока.

Локальная переменная также может быть ссылкой на объект. В этом случае ссылка (локальная переменная) хранится в стеке потоков, но сам объект хранится в куче.

Объект может содержать методы, и эти методы могут содержать локальные переменные. Эти локальные переменные также хранятся в стеке потоков, даже если объект, которому принадлежит метод, хранится в куче.

Переменные-члены объекта хранятся в куче вместе с самим объектом. Это верно как в случае, когда переменная-член имеет примитивный тип, так и в том случае, если она является ссылкой на объект.

Статические переменные класса также хранятся в куче вместе с определением класса.

К объектам в куче могут обращаться все потоки, имеющие ссылку на объект. Когда поток имеет доступ к объекту, он также может получить доступ к переменным-членам этого объекта. Если два потока вызывают метод для одного и того же объекта одновременно, они оба будут иметь доступ к переменным-членам объекта, но каждый поток будет иметь свою собственную копию локальных переменных.

Диаграмма, которая иллюстрирует описанное выше:

image

Два потока имеют набор локальных переменных. Local Variable 2 указывает на общий объект в куче (Object 3). Каждый из потоков имеет свою копию локальной переменной со своей ссылкой. Их ссылки являются локальными переменными и поэтому хранятся в стеках потоков. Тем не менее, две разные ссылки указывают на один и тот же объект в куче.

Обратите внимание, что общий Object 3 имеет ссылки на Object 2 и Object 4 как переменные-члены (показано стрелками). Через эти ссылки два потока могут получить доступ к Object 2 и Object 4.

На диаграмме также показана локальная переменная (Local variable 1). Каждая её копия содержит разные ссылки, которые указывают на два разных объекта (Object 1 и Object 5), а не на один и тот же. Теоретически оба потока могут обращаться как к Object 1, так и к Object 5, если они имеют ссылки на оба этих объекта. Но на диаграмме выше каждый поток имеет ссылку только на один из двух объектов.

Итак, мы посмотрели иллюстрацию, теперь давайте посмотрим, как тоже самое выглядит в Java-коде:

Метод run() вызывает methodOne(), а methodOne() вызывает methodTwo().

methodOne() объявляет примитивную локальную переменную (localVariable1) типа int и локальную переменную (localVariable2), которая является ссылкой на объект.

Каждый поток, выполняющий методOne(), создаст свою собственную копию localVariable1 и localVariable2 в своих соответствующих стеках. Переменные localVariable1 будут полностью отделены друг от друга, находясь в стеке каждого потока. Один поток не может видеть, какие изменения вносит другой поток в свою копию localVariable1.

Каждый поток, выполняющий методOne(), также создает свою собственную копию localVariable2. Однако две разные копии localVariable2 в конечном итоге указывают на один и тот же объект в куче. Дело в том, что localVariable2 указывает на объект, на который ссылается статическая переменная sharedInstance. Существует только одна копия статической переменной, и эта копия хранится в куче. Таким образом, обе копии localVariable2 в конечном итоге указывают на один и тот же экземпляр MySharedObject. Экземпляр MySharedObject также хранится в куче. Он соответствует Object 3 на диаграмме выше.

Обратите внимание, что класс MySharedObject также содержит две переменные-члены. Сами переменные-члены хранятся в куче вместе с объектом. Две переменные-члены указывают на два других объекта Integer. Эти целочисленные объекты соответствуют Object 2 и Object 4 на диаграмме.

Также обратите внимание, что methodTwo() создает локальную переменную с именем localVariable1. Эта локальная переменная является ссылкой на объект типа Integer. Метод устанавливает ссылку localVariable1 для указания на новый экземпляр Integer. Ссылка будет храниться в своей копии localVariable1 для каждого потока. Два экземпляра Integer будут сохранены в куче и, поскольку метод создает новый объект Integer при каждом выполнении, два потока, выполняющие этот метод, будут создавать отдельные экземпляры Integer. Они соответствуют Object 1 и Object 5 на диаграмме выше.

Обратите также внимание на две переменные-члены в классе MySharedObject типа long, который является примитивным типом. Поскольку эти переменные являются переменными-членами, они все еще хранятся в куче вместе с объектом. В стеке потоков хранятся только локальные переменные.

речь идет не о том, идут ли примитивы в стек или кучу, а о том, где они сохраняются в фактической физической ОЗУ.

взять простой пример:

Я знаю 5 хранится в блоке памяти.

моя область интересов-где хранится переменная "a"?

связанные с этим дополнительные вопросы: где это происходит, когда "a" связывается с блоком памяти, который содержит примитивное значение 5? Есть ли другой блок памяти, созданный для хранения "a"? Но это будет выглядеть так, как будто a является указателем на объект, но это примитивный тип, участвующий здесь.

допустим у вас есть функция foo() :

затем, когда компилятор компилирует эту функцию, он создаст инструкции байт-кода, которые оставляют 4 байта места в стеке всякий раз, когда эта функция вызывается. Имя "a" полезно только вам-компилятору, он просто создает для него место, запоминает, где это место находится, и везде, где он хочет использовать значение вместо этого он вставляет ссылки на место памяти, зарезервированное для этого значения.

если вы не знаете, как работает стек, он работает следующим образом: каждая программа имеет по крайней мере один поток, и каждый поток имеет ровно один стек. Стек представляет собой непрерывный блок памяти (который также может расти при необходимости). Первоначально стек пуст, пока не будет вызвана первая функция в вашей программе. Затем, когда вызывается ваша функция, ваша функция выделяет место в стеке для себя, для все его локальные переменные, для его возвращаемых типов и т. д.

когда функция main вызов другой функции foo , вот один пример того, что может произойти (здесь есть пара упрощающих белых лжи):

  • main хочет передать параметры foo . Он выталкивает эти значения в верхнюю часть стека таким образом, что foo будет точно знать, где они будут помещены ( main и foo передаст параметры в последовательном путь.)
  • main нажимает адрес, куда должно вернуться выполнение программы после foo сделано. Это увеличивает указатель стека.
  • main звонки foo .
  • , когда foo запускается, он видит, что стек в настоящее время находится по адресу X
  • foo хочет выделить 3 int переменные в стеке, поэтому ему нужно 12 байтов.
  • foo будет использовать X + 0 для первого int, X + 4 для второго int, X + 8 для третий.
    • компилятор может вычислить это во время компиляции, и компилятор может полагаться на значение регистра указателя стека (ESP в системе x86), и поэтому код сборки, который он записывает, делает такие вещи, как "хранить 0 в адресе ESP + 0", "хранить 1 в адресе ESP + 4" и т. д.
    • foo знает, сколько параметров он принимает (скажем, 3), поэтому он знает, что, скажем, X - 8 - первый, X - 12-второй, а X-16-третий.

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

    я знаю, что это объяснение немного размывает линию между x86 и java, но я надеюсь, что это поможет проиллюстрировать, как на самом деле работает оборудование.

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

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

    вот где приходит куча. Обычно (но не всегда) одна программа имеет ровно одну кучу. Куча называется кучей, потому что это просто большая куча памяти.

    использовать память в куче, вы должны вызвать процедуры распределения-процедуры, которые находят неиспользуемое пространство и дают его вам, и процедуры, которые позволяют вам вернуть пространство, которое вы выделили, но больше не используете. Распределитель памяти получает большие страницы памяти из операционной системы, а затем раздает отдельные маленькие биты тому, что ему нужно. Он отслеживает, что ОС дала ему, и из этого, что она дала остальной части программы. Когда программа запрашивает память кучи, она ищет самый маленький кусочек памяти, который у него есть, который соответствует потребностям, помечает этот кусок как выделяемый и передает его обратно остальной части программы. Если у него больше нет свободных кусков, он может попросить операционную систему больше страниц памяти и выделить оттуда (до некоторого предела).

    в таких языках, как C, те процедуры выделения памяти, которые я упомянул, обычно называются malloc() попросить памяти и free() , чтобы вернуть его.

    Java с другой стороны у hand нет явного управления памятью, как у C, вместо этого у него есть сборщик мусора - вы выделяете любую память, которую хотите, а затем, когда вы закончите, вы просто прекратите ее использовать. Среда выполнения Java будет отслеживать, какую память вы выделили, и будет сканировать вашу программу, чтобы узнать, не используете ли вы все свои выделения больше и автоматически освобождаете эти куски.

    Итак, теперь мы знаем, что память выделяется в куче или в стеке, что происходит, когда я создаю закрытую переменную в классе?

    откуда берется это воспоминание? Ответ-куча. У вас есть код, который создает новый

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

    вопрос, который вы задали, очень связан с языком программирования и тем, как операционная система имеет дело с процессом и переменными.

    это очень интересно, потому что, когда я был в своем университете, изучая C и c++, я сталкиваюсь с тем же вопросом, что и вы. после прочтения некоторых ASM код, составленный GCC , у меня есть немного о понимании с этим, давайте обсудим об этом, если какая-либо проблема, прокомментируйте ее и позвольте мне узнать больше об этом.

    на мой взгляд, имя переменной не будет сохранено, а значение переменной будет сохранено, потому что в ASM код, нет реального variable name за исключением cache name короче говоря, вся так называемая переменная - это просто off set С stack или heap .
    что, я думаю, является подсказкой для моего обучения, так как ASM интернет-с имени переменной, другие язык может иметь ту же стратегию.
    Они просто хранят off set для реального места для хранения данных.
    приведем пример, скажем переменную name a расположенный по адресу @1000 типа этого a является целым числом, таким образом, в памяти address

    что @1000-это off set где хранятся реальные данные.

    как вы можете видеть, что данные помещаются в реальном off set для этого.
    В моем понимании процесса, что все переменная будет заменена на " адрес "этой" переменной "в начале процесса, что означает, что в то время как CPU имеет дело только с" адресом", который уже выделен в памяти.
    давайте еще раз рассмотрим эту процедуру: что вы определили
    int a=5; print(a);
    после компиляции программа переносится в другой формат (все по моему воображению):

    в то время как в ситуации процесса, который реально выполняется, я думаю, что память будет такой:

    поскольку память процесса выделяется CPU, @2000 это off set этого имени переменной, что означает name будет заменен только адресом памяти, затем будет читать данные 5 с этого адреса, а затем выполнить команду печати.

    переосмысливать

    после завершения моего письма, я обнаружил, что это довольно трудно представить другими людьми, мы можем обсудить его, если какие-либо проблемы или ошибки я сделал.

    Теперь предположим, что у меня есть класс, имеющий свойство, скажем, "int [] dealCodes" (массив примитива int). В соответствии с управлением памятью после инициализации кода сделки в памяти будет непрерывное выделение памяти (total_elements * 4 байта). Итак, если размер массива равен 10, тогда в памяти JVM будет выделено 40 байтов.

    У меня вопрос: в какой области будут выделены эти 40 байт (куча или стек)?

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

    Также хочу узнать о подобном сценарии, когда массив содержит ссылки (например, массив типа Employees). Думаю, в этом случае все будет в районе кучи. Поскольку это ссылки, массив будет содержать 4 байта для каждой ссылки (32-битная система), и эти ссылки будут указывать на объекты различного размера. Распределение памяти массива будет рассчитываться на основе ссылочного размера, а не размера объекта.

    Пожалуйста, помогите мне прояснить вышеупомянутые 2 пункта.

    2 ответа

    Объекты всегда выделяются в куче, поэтому ваш dealCodes будет выделена только там, но общая выделенная память превышает 40 байт.

    То же самое относится и к массиву объектов Employee , за исключением того, что каждый элемент массива теперь является ссылкой на объект Employee, поэтому массив Shallow Heap , занимаемый служащими, по-прежнему составляет 56 байт, а Retained Heap зависит от размера каждого объекта Employee.

    Вы можете использовать VisualVM в каталоге JDK_HOME / bin, чтобы сделать снимок вашей программы / приложения, увидеть объем памяти, занятой каждым объектом, как мелких, так и сохраненных размеров кучи.

    1. У меня вопрос: в какой области будут выделены эти 40 байт (куча или стек)?

    Обратите внимание, что будет выделено более 40 байтов, так как есть некоторые накладные расходы на заголовок объекта + атрибут длины массива.

    Если вас это интересует, вы можете поиграть с Java Object Layout

    Также хочу узнать о подобном сценарии, когда массив содержит ссылки (например, массив типа Employees). Думаю, в этом случае все будет в районе кучи. Поскольку это ссылки, массив будет содержать 4 байта для каждой ссылки (32-битная система), и эти ссылки будут указывать на объекты различного размера.

    Размер ссылок зависит от архитектуры, 32 бита VS 64 бита и возможного использования CompressedOops .

    Читайте также: