Java где хранятся примитивы
Перейти к содержимому

Java где хранятся примитивы

  • автор:

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

В Java примитивы и ссылки на объекты хранятся в стэке, а объекты в куче. Предположим есть объект user класса User , у которого имеются поля int age и String name . На вершину стэка ложиться ссылка на user , сам объект user хранится в куче. А где будет храниться его поле age и ссылка на name ? Так же в стэке, сверху над ссылкой на сам user ? В том смысле, что когда завершится функция, которая создавала user — то указатель стэка должен сместиться вниз, и все эти переменные — ссылка на user , примитив int age и ссылка на name должны исчезнуть из памяти.

Отслеживать
задан 11 мая 2019 в 21:40
Тимур Баймагамбетов Тимур Баймагамбетов
461 1 1 золотой знак 3 3 серебряных знака 12 12 бронзовых знаков

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

В Java примитивы и ссылки на объекты хранятся в стэке, а объекты в куче.

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

Важно понимать разницу между переменной и полем.

class User < private int age = 42; private String name = "John Doe"; public void someMethod() < int someVar = 2; User user = new User(); >> 

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

someVar и user — это переменные и они хранятся в стеке. В первой хранится значение 2, во втором ссылка на объект класса User .

В том смысле, что когда завершится функция, которая создавала user — то указатель стэка должен сместиться вниз, и все эти переменные — ссылка на user, примитив int age и ссылка на name должны исчезнуть из памяти.

При завершении метода someMethod кадр стека будет уничтожен и вместе с ним перестанут существовать переменные someVar и user . Но сам объект user останется в куче до следующего вызова сборщика мусора. Сборщик мусора при запуске проверит все существующие кадры стека, не найдёт в них ссылки на объект user и только тогда удалит его.

Java-модель памяти (часть 1)

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

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

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

Внутренняя Java-модель памяти

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-коде:

Public class MyRunnable implements Runnable() < public void run() < methodOne(); >public void methodOne() < int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //. do more with local variables. methodTwo(); >public void methodTwo() < Integer localVariable1 = new Integer(99); //. do more with local variable. >>
public class MySharedObject < //статическая переменная, указывающая на экземпляр MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); // переменные-члены, указывающие на два объекта в куче public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member2 = 67890; >

Метод 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, который является примитивным типом. Поскольку эти переменные являются переменными-членами, они все еще хранятся в куче вместе с объектом. В стеке потоков хранятся только локальные переменные.

На какие области делится память JVM?

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

Stack – место под примитивы и ссылки на объекты (но не сами объекты). Хранит локальные переменные и возвращаемые значения функций. Здесь же хранятся ссылки на объекты пока те конструируются. Все данные в стеке – GC roots. Освобождается сразу на выходе из функции. Принадлежит потоку, размер по-умолчанию указывается параметром виртуальной машины -Xss , но при создании потока программно можно указать отличное значение. Подробнее.
PermGen – В этой области хранятся загруженные классы (экземпляры класса Class ). Здесь же с Java 7 хранится пул строк. Изначально размера -XX:PermSize , растет динамически до -XX:MaxPermSize . Не считается частью кучи.
Metaspace – с Java 8 заменяет permanent generation. Отличие в том, что по умолчанию metaspace ограничен только размерами доступной на машине памяти, но так же как PermGen может быть ограничен, параметром -XX:MaxMetaspaceSize .
Heap – куча, вся managed-память, в которой хранятся все пользовательские объекты. Все следующие разделы – части кучи. Параметры -Xms , -Xmn и -Xmx устанавливают начальный, минимальный и максимальный размеры хипа соответственно.
Eden, New Generation, Old Generation и другие – специфичные для сборщика мусора части кучи, поколения. Могут быть разные, но общий подход сохраняется: долго живущий объект постепенно двигается во всё более старое поколение; сборка мусора в разных поколениях происходит раздельно; чем поколение старше, тем сборка в нём реже, но и дороже. Подробнее.

Хотя устройство памяти – это детали реализации виртуальной машины, для Java-разработчика знания о них несут практическую пользу. Эти знания необходимы для передачи правильных значений параметров JVM, что в свою очередь спасает от просадок производительности GC и остановок с OutOfMemoryError .

Вопросы собеседований по Java Core. Часть 2.

Продолжаем нашу рубрику вопросов с собеседований и начнем с продолжения последнего вопроса первой части.

  • Области памяти (heap, stack, metaspace, codecache)
  • Какой контракт между методами equals и hashCode
  • Что такое пул строк (String pool)

Области памяти (heap, stack, metaspace)

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

  • heap– еще называют кучей, основной сегмент памяти, используется для выделения памяти под объекты и JRE классы.
    Создается heap при запуске виртуальной машины из оперативной памяти.
    Создание нового объекта происходит в heap, здесь работает сборщик мусора (Garbage Collector).
  • metaspace– хранятся метаданные о классе и статические поля: там хранятся либо примитивы, либо ссылки на объекты/массивы, которые сами по себе аллоцированы в heap. Tcnm dозможность динамически расширятся, ограничение по умолчанию только размером нативной памяти. Опционально можно задать размер через аргумент -XX:MaxMetaspaceSize. На продакшен серверах желательно всегда задавать размер Metaspace.
  • stack– стековая память в Java работает по схеме LIFO: всякий раз, когда вызывается метод, в памяти стека создается новый блок, который содержит примитивы и ссылки на другие объекты в методе. Каждый поток имеет свой стек, примитивы и ссылки на локальные переменные хранятся в стеке. Как только метод заканчивает работу, блок также перестает использоваться, тем самым предоставляя доступ для следующего метода. Объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков. При переполнении стека мы получаем StackOverflowError
  • native method stack — редко о нем встречается информация в интернете, но для нативных методов выделяется отдельный стек.
  • codecache— виртуальная машина Java генерирует собственный код и сохраняет его в области памяти, называемой codecache.

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

Какой контракт между методами equals и hashCode

Это один из вопросов, к которому в основном подходят после вопроса о методах класса Object.

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

Важно запомнить, что от вас в основном хотят слышать на собеседовании

Если объекты equals, то их hashCode совпадает, но, если объекты имеют одинаковый хэшкод они необязательно equals.

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

Еще один важный момент — свойство транзитивно, т.е. если A equals B, B equals C, то A equals C и все их хэшкоды совпадают.

Эти 2 метода рассматривают в паре, потому что они используются в коллекциях, основанных на хэш-таблицах, таких как HashMap, например.

Подробнее операции в коллекциях мы рассмотрим позднее.

Где же хранится хэшкод объекта, если его не переопределять? Есть такое понятие как identityHashCode — это хэшкод по умолчанию для объекта, его также можно получить, даже переопределив метод, с помощью

 System.identityHashCode(Object o)

этот hashCode хранится в заголовке объекта.

Вычисляется он один раз и при последующих попытках получить его больше не изменяется, даже при изменении состояния нашего объекта. Т.к. hashCode и identityHashCode имеют реализацию на нативном языке, сказать точно на основе чего он вычисляется трудно, но где-то встречал что он связан с ячейкой памяти, в которой хранится объект.

Что же будет, если вызвать hashCode у null — на выходе получим 0. Это еще один из вопросов с собеседования.

Что такое пул строк (String pool)

В целях оптимизации расходов памяти для строк в Java в области памяти heap выделено место для так называемого пула строк. Согласитесь, что строки — это наиболее часто используемые объекты в нашей программе. Этот механизм стал как плюсом, так и минусом — ведь теперь мы вынуждены сравнивать строки через equals, который сначала сравнивает длину строки, а затем посимвольно, что увеличивает наши затраты времени на эту операцию, ведь длина строки ограничена максимальным значением Integer — 2 147 483 647.

В целях эксперимента можете создать массив строк такой длины и попробовать проверить их на равенство.

Это работает пока мы не создадим строки с помощью оператора new, который без исключения создает новую строку, не глядя в пул (в прямом смысле слова)

String name = "Vlad"; String sameName = "Vlad"; String another = new String("Vlad"); name == sameName -> true name == another -> false

Но выход и из этой ситуации был найден — интернирование строк. С помощью метода intern() проверяется есть ли строка в пуле строк — если есть, то возвращается ссылка на нее, если нет, то создается новая строка в пуле и опять же возвращается ссылка на нее.

Что это нам дает — теперь мы можем сравнивать строки с помощью оператора == , что на порядок быстрее посимвольного сравнения.

Для оптимального использования интернирования нам нужно задать размера пула строк с помощью параметра запуска -XX:StringTableSize

Benchmark Mode Cnt Score Error Units StringPerfomance.compareNotSameStringWithEquals ss 50 1,494 ± 0,249 ms/op StringPerfomance.compareNotSameStringWithOperator ss 50 1,061 ± 0,204 ms/op StringPerfomance.compareSameStringInPoolWithOperator ss 50 0,938 ± 0,057 ms/op StringPerfomance.compareSameStringWithEquals ss 50 0,852 ± 0,061 ms/op StringPerfomance.stringInternIfBeInStringPool ss 50 38,038 ± 2,458 ms/op StringPerfomance.stringInternIfNotInStringPool ss 50 38,746 ± 2,455 ms/op

Для наглядности провел benchmark тест на производительность операций, где

WithOperator — это сравнение через ==

WithEquals — через equals

Длина строки была взята — 61 символ. Время (Score) указано в миллисекундах.

Собственно какой вывод следует из этого — интернирование строк — трудозатратная и не всегда применимая в конкретном случае операция.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *