В какой области памяти хранятся примитивы java
Это глубокое погружение в управление памятью Java позволит расширить ваши знания о том, как работает куча, ссылочные типы и сборка мусора.
Вероятно, вы могли подумать, что если вы программируете на Java, то вам незачем знать о том, как работает память. В Java есть автоматическое управление памятью, красивый и тихий сборщик мусора, который работает в фоновом режиме для очистки неиспользуемых объектов и освобождения некоторой памяти.
Поэтому вам, как программисту на Java, не нужно беспокоиться о таких проблемах, как уничтожение объектов, поскольку они больше не используются. Однако, даже если в Java этот процесс выполняется автоматически, он ничего не гарантирует. Не зная, как устроен сборщик мусора и память Java, вы можете создать объекты, которые не подходят для сбора мусора, даже если вы их больше не используете.
Поэтому важно знать, как на самом деле работает память в Java, поскольку это дает вам преимущество в написании высокопроизводительных и оптимизированных приложений, которые никогда не будут аварийно завершены с ошибкой OutOfMemoryError . С другой стороны, когда вы окажетесь в плохой ситуации, вы сможете быстро найти утечку памяти.
Для начала давайте посмотрим, как обычно организована память в Java:
Структура памяти
Обычно память делится на две большие части: стек и куча. Имейте в виду, что размер типов памяти на этом рисунке не пропорционален реальному размеру памяти. Куча - это огромный объем памяти по сравнению со стеком.
Общие библиотеки
Здесь хранится нативный код для любых общих библиотек. Эта область памяти загружается операционной системой лишь один раз для каждого процесса.
Теперь давайте посмотрим, как исполняемая программа использует самые важные части памяти. Воспользуемся нижеприведённым кодом. Он не оптимизирован с точки зрения корректности, так что игнорируйте проблемы вроде ненужных промежуточных переменных, некорректных модификаторов и прочего. Его задача — визуализировать использование стека и кучи.
Здесь вы можете увидеть, как исполняется вышеприведённая программа и как используются стек и куча:
- Каждый вызов функции добавляется в стек потока исполнения в качестве фреймового блока.
- Все локальные переменные, включая аргументы и возвращаемые значения, сохраняются в стеке внутри фреймовых блоков функций.
- Все примитивные типы вроде int хранятся прямо в стеке.
- Все типы объектов вроде Employee, Integer или String создаются в куче, а затем на них ссылаются с помощью стековых указателей. Это верно и для статичных данных.
- Функции, которые вызываются из текущей функции, попадают наверх стека.
- Когда функция возвращает данные, её фрейм удаляется из стека.
- После завершения основного процесса объекты в куче больше не имеют стековых указателей и становятся потерянными (сиротами).
- Пока вы явно не сделаете копию, все ссылки на объекты внутри других объектов делаются с помощью указателей.
Давайте разберёмся с автоматическим управлением кучей, которое играет очень важную роль с точки зрения производительности приложения. Когда программа пытается выделить в куче больше памяти, чем доступно (в зависимости от значения Xmx ), мы получаем ошибки нехватки памяти.
JVM управляет куче с помощью сборки мусора. Чтобы освободить место для создания нового объекта, JVM очищает память, занятую потерянными объектами, то есть объектами, на которые больше нет прямых или опосредованных ссылок из стека.
Сборщик мусора в JVM отвечает за:
- Получение памяти от ОС и возвращение её ОС.
- Передачу выделенной памяти приложению по его запросу.
- Определение, какие части выделенной памяти ещё используются приложением.
- Затребование неиспользованной памяти для использования приложением.
Младший сборщик
Он поддерживает чистоту и компактность пространства молодого поколения. Запускается тогда, когда JVM не может получить в раю необходимую память для размещения нового объекта. Изначально все области кучи пусты. Рай заполняется первым, за ним область выживших, и в конце хранилище.
Здесь вы можете увидеть процесс работы этого сборщика:
- Допустим, в раю уже есть объекты (блоки с 01 по 06 помечены как используемые).
- Приложение создаёт новый объект (07).
- JVM пытается получить необходимую память в раю, но там уже нет места для размещения нового объекта, поэтому JVM запускает младший сборщик.
- Он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные — как мусор (потерянные).
- JVM случайно выбирает один блок из S0 и S1 в качестве «целевого» пространства (To Space), пусть это будет S0. Теперь сборщик перемещает все живые объекты в «целевое» пространство, которое было пустым, когда мы начали работу, и повышает их возраст на единицу.
- Затем сборщик очищает рай, и в нём выделяется память для нового объекта.
- Допустим, прошло какое-то время, и в раю стало больше объектов (блоки с 07 по 13 помечены как используемые).
- Приложение создаёт новый объект (14).
- JVM пытается получить в раю нужную память, но там нет свободного места для нового объекта, поэтому JVM снова запускает младший сборщик.
- Повторяется этап разметки, который охватывает и те объекты, что находятся в пространстве выживших в «целевом пространстве».
- Теперь JVM выбирает в качестве «целевого» свободный блок S1, а S0 становится «исходным». Сборщик перемещает все живые объекты из рая и «исходного» в «целевое» (S1), которое было пустым, и повысил возраст объектов на единицу. Поскольку некоторые объекты сюда не поместились, сборщик переносит их в «хранилище», ведь область выживших не может увеличиваться, и этот процесс называют преждевременным продвижением (premature promotion). Такое может происходить, даже если свободна одна из областей выживших.
- Теперь сборщик очищает рай и «исходное» пространство (S0), а новый объект размещается в раю.
- Так повторяется при каждой сессии младшего сборщика, выжившие перемещаются между S0 и S1, а их возраст увеличивается. Когда он достигает заданного «максимального порога», по умолчанию это 15, объект перемещается в «хранилище».
Метапространство
Это часть нативной памяти, по умолчанию у неё нет верхней границы. В более ранних версиях JVM эта память называлась пространством постоянного поколения (Permanent Generation (PermGen) Space). Загрузчики классов хранили в нём определения классов. Если это пространство растёт, то ОС может переместить хранящиеся здесь данные из оперативной в виртуальную память, что может замедлить работу приложения. Избежать этого можно, задав размер метапространства с помощью флагов XX:MetaspaceSize и -XX:MaxMetaspaceSize , в этом случае приложение может выдавать ошибки памяти.
Стек (Stack)
Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (также известных в Java как примитивные типы), которые содержат само значение, а не ссылку на объект из кучи.
Кроме того, переменные в стеке имеют определенную видимость, также называемую областью видимости. Используются только объекты из активной области. Например, предполагая, что у нас нет никаких глобальных переменных (полей) области видимости, а только локальные переменные, если компилятор выполняет тело метода, он может получить доступ только к объектам из стека, которые находятся внутри тела метода. Он не может получить доступ к другим локальным переменным, так как они не выходят в область видимости. Когда метод завершается и возвращается, верхняя часть стека выталкивается, и активная область видимости изменяется.
Возможно, вы заметили, что на картинке выше отображено несколько стеков памяти. Это связано с тем, что стековая память в Java выделяется для каждого потока. Следовательно, каждый раз, когда поток создается и запускается, он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.
Сборщики в JDK 11
JDK 11 — это текущая версия LTE. Ниже приведён список доступных в ней сборщиков мусора, и JVM выбирает по умолчанию один из них в зависимости от текущего оборудования и операционной системы. Мы всегда можем принудительно выбрать какой-либо сборщик с помощью переключателя -XX .
- Серийный сборщик: использует один поток, эффективен для приложений с небольшим количеством данных, наиболее удобен для однопроцессорных машин. Его можно выбрать с помощью -XX:+UseSerialGC .
- Параллельный сборщик: нацелен на высокую пропускную способность и использует несколько потоков, чтобы ускорить процесс сборки. Предназначен для приложений со средним или большим количеством данных, исполняемых на многопоточном/многопроцессорном оборудовании. Его можно выбрать с помощью -XX:+UseParallelGC .
- Сборщик Garbage-First (G1): работает по большей части многопоточно (то есть многопоточно выполняются только объёмные задачи). Предназначен для многопроцессорных машин с большим объёмом памяти, по умолчанию используется на большинстве современных компьютеров и ОС. Нацелен на короткие паузы и высокую пропускную способность. Его можно выбрать с помощью -XX:+UseG1GC .
- Сборщик Z: новый, экспериментальный, появился в JDK11. Это масштабируемый сборщик с низкой задержкой. Многопоточный и не останавливает исполнение потоков приложения, то есть не относится к stop-the-world. Предназначен для приложений, которым необходима низкая задержка и/или очень большая куча (на несколько терабайтов). Его можно выбрать с помощью -XX:+UseZGC .
Типы ссылок
Если вы внимательно посмотрите на изображение структуры памяти, вы, вероятно, заметите, что стрелки, представляющие ссылки на объекты из кучи, на самом деле относятся к разным типам. Это потому, что в языке программирования Java используются разные типы ссылок: сильные, слабые, мягкие и фантомные ссылки. Разница между типами ссылок заключается в том, что объекты в куче, на которые они ссылаются, имеют право на сборку мусора по различным критериям. Рассмотрим подробнее каждую из них.
1. Сильная ссылка
Это самые популярные ссылочные типы, к которым мы все привыкли. В приведенном выше примере со StringBuilder мы фактически храним сильную ссылку на объект из кучи. Объект в куче не удаляется сборщиком мусора, пока на него указывает сильная ссылка или если он явно доступен через цепочку сильных ссылок.
2. Слабая ссылка
Попросту говоря, слабая ссылка на объект из кучи, скорее всего, не сохранится после следующего процесса сборки мусора. Слабая ссылка создается следующим образом:
Хорошим вариантом использования слабых ссылок являются сценарии кеширования. Представьте, что вы извлекаете некоторые данные и хотите, чтобы они также были сохранены в памяти - те же данные могут быть запрошены снова. С другой стороны, вы не уверены, когда и будут ли эти данные запрашиваться снова. Таким образом, вы можете сохранить слабую ссылку на него, и в случае запуска сборщика мусора, возможно, он уничтожит ваш объект в куче. Следовательно, через некоторое время, если вы захотите получить объект, на который вы ссылаетесь, вы можете внезапно получить null значение. Хорошей реализацией сценариев кеширования является коллекция WeakHashMap . Если мы откроем WeakHashMap класс в Java API, мы увидим, что его записи фактически расширяют WeakReference класс и используют его поле ref в качестве ключа отображения ( Map) :
После сбора мусора ключа из WeakHashMap вся запись удаляется из карты.
3. Мягкая ссылка
Эти типы ссылок используются для более чувствительных к памяти сценариев, поскольку они будут собираться сборщиком мусора только тогда, когда вашему приложению не хватает памяти. Следовательно, пока нет критической необходимости в освобождении некоторого места, сборщик мусора не будет касаться легко доступных объектов. Java гарантирует, что все объекты, на которые имеются мягкие ссылки, будут очищены до того, как будет выдано исключение OutOfMemoryError . В документации Javadocs говорится, что «все мягкие ссылки на мягко достижимые объекты гарантированно очищены до того, как виртуальная машина выдаст OutOfMemoryError».
Подобно слабым ссылкам, мягкая ссылка создается следующим образом:
4. Фантомная ссылка
Используется для планирования посмертных действий по очистке, поскольку мы точно знаем, что объекты больше не живы. Используется только с очередью ссылок, поскольку .get() метод таких ссылок всегда будет возвращаться null . Эти типы ссылок считаются предпочтительными для финализаторов.
Стеки потоков исполнения
Это стековая область, в которой на один поток выделяется один стек. Здесь хранятся специфические для потоков статичные данные, в том числе фреймы методов и функций, указатели на объекты. Размер стековой памяти можно задать с помощью флага Xss .
Кеш кода
Здесь компилятор Just In Time (JIT) хранит скомпилированные блоки кода, к которым приходится часто обращаться. Обычно JVM интерпретирует байткод в нативный машинный код, однако код, скомпилированный JIT-компилятором, не нужно интерпретировать, он уже представлен в нативном формате и закеширован в этой области памяти.
Ссылки на String
Ссылки на тип String в Java обрабатываются немного по- другому. Строки неизменяемы, что означает, что каждый раз, когда вы делаете что-то со строкой, в куче фактически создается другой объект. Для строк Java управляет пулом строк в памяти. Это означает, что Java сохраняет и повторно использует строки, когда это возможно. В основном это верно для строковых литералов. Например:
При запуске этот код распечатывает следующее:
Strings are equal
Следовательно, оказывается, что две ссылки типа String на одинаковые строковые литералы фактически указывают на одни и те же объекты в куче. Однако это не действует для вычисляемых строк. Предположим, что у нас есть следующее изменение в строке // 1 приведенного выше кода.
Strings are different
В этом случае мы фактически видим, что у нас есть два разных объекта в куче. Если учесть, что вычисляемая строка будет использоваться довольно часто, мы можем заставить JVM добавить ее в пул строк, добавив .intern() метод в конец вычисляемой строки:
При добавлении вышеуказанного изменения создается следующий результат:
Куча (Heap)
Эта часть памяти хранит в памяти фактические объекты, на которые ссылаются переменные из стека. Например, давайте проанализируем, что происходит в следующей строке кода:
Ключевое слово new несет ответственность за обеспечение того, достаточно ли свободного места на куче, создавая объект типа StringBuilder в памяти и обращаясь к нему через «Builder» ссылки, которая попадает в стек.
Для каждого запущенного процесса JVM существует только одна область памяти в куче. Следовательно, это общая часть памяти независимо от того, сколько потоков выполняется. На самом деле структура кучи немного отличается от того, что показано на картинке выше. Сама куча разделена на несколько частей, что облегчает процесс сборки мусора.
Максимальные размеры стека и кучи не определены заранее - это зависит от работающей JVM машины. Позже в этой статье мы рассмотрим некоторые конфигурации JVM, которые позволят нам явно указать их размер для запускаемого приложения.
Старший сборщик
Следит за чистотой и компактностью пространства старого поколения (хранилищем). Запускается при одном из таких условий:
- Разработчик вызывает в программе System . gc() или Runtime.getRunTime().gc() .
- JVM решает, что в хранилище недостаточно памяти, потому что оно заполнено в результате прошлых сессий младшего сборщика.
- Если во время работы младшего сборщика JVM не может получить достаточно памяти в раю или области выживших.
- Если мы задали в JVM параметр MaxMetaspaceSize и для загрузки новых классов не хватает памяти.
- Допустим, прошло уже много сессий младшего сборщика и хранилище почти заполнено. JVM решает запустить старший сборщик.
- В хранилище он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные — как мусор (потерянные). Если старший сборщик запустили в ходе работы младшего сборщика, то его работа охватывает пространство молодого поколения (рай и область выживших) и хранилище.
- Сборщик убирает все потерянные объекты и возвращает память.
- Если в ходе работы старшего сборщика в куче не осталось объектов, JVM также возвращает память из метапространства, убирая из него загруженные классы, если это относится к полной сборке мусора.
Мы рассмотрели структуру и управление памятью JVM. Это не исчерпывающая статья, мы не говорили о многих более сложных концепциях и способах настройки под особые сценарии использования. Подробнее вы можете почитать здесь.
Но для большинства JVM-разработчиков (Java, Kotlin, Scala, Clojure, JRuby, Jython) этого объёма информации будет достаточно. Надеюсь, теперь вы сможете писать более качественный код, создавать более производительные приложения, избегая различных проблем с утечкой памяти.
3 ответа 3
Насколько я понимаю (знатоки Java пусть меня поправят), всё, что выделено при помощи new , лежит в heap-памяти. В частности, экземпляры классов тоже лежат в heap, вместе со всеми своими полями.
У класса самого по себе нету локальных переменных. Локальные переменные есть у его методов, эти переменные размещаются в стеке, когда метод вызывается.
- Если оптимизатор может доказать, что экземпляр класса не покидает пределов метода, он может переместить его в стек невидимым для программиста образом для эффективности
- Не знаю, имеет ли право компилятор неявно перемещать в heap локальные переменные, захваченные лямбда-функцией.
В Java
Ссылка на класс на стеке, сам экземпляр класса в heap'е. Кроме того, при включенном флаге -XX:+DoEscapeAnalysis и ключом -server JVM может размещать экземпляры классов на стеке, если на эти экземпляры нет ссылок за пределами блока (при этом оптимизируемый таким образом метод должен сначала откомпилироваться JIT). См. EscapeAnalysis.
@a_gura: формально, ссылка может попасть в heap, если она — поле другого класса. Я бы делал разделение по критерию поле/локальная переменная.
@VladD если ссылка - это поле другого класса, она всегда в heap'е (в рамках самого класса), если только это не локальная копия ссылки или не сработал escape analysis для содержащего ее класса.
Стэк/Stack - находится в оперативной памяти (RAM) хранит ссылки на объекты, а также примитивы.
Куча/Heap (также в RAM) - хранит все объекты (не ссылки).
Permanent generation используется только JVM для хранения необходимых данных, в том числе метаданные о созданных объектах.
Постоянное хранилище (ROM) - хранит строковые литералы и константы.
Внешнее хранилище - хранит потоковые объекты (поток в виде байтов) и долгоживущие (persistent) объекты (сохраняют свое состояние после закрытия программы)
Советы и приемы
Чтобы минимизировать объем памяти, максимально ограничьте область видимости переменных. Помните, что каждый раз, когда выскакивает верхняя область видимости из стека, ссылки из этой области теряются, и это может сделать объекты пригодными для сбора мусора.
Явно устанавливайте в null устаревшие ссылки. Это сделает объекты, на которые ссылаются, подходящими для сбора мусора.
Избегайте финализаторов (finalizer). Они замедляют процесс и ничего не гарантируют. Фантомные ссылки предпочтительны для работы по очистке памяти.
Не используйте сильные ссылки там, где можно применить слабые или мягкие ссылки. Наиболее распространенные ошибки памяти - это сценарии кэширования, когда данные хранятся в памяти, даже если они могут не понадобиться.
JVisualVM также имеет функцию создания дампа кучи в определенный момент, чтобы вы могли анализировать для каждого класса, сколько памяти он занимает.
Настройте JVM в соответствии с требованиями вашего приложения. Явно укажите размер кучи для JVM при запуске приложения. Процесс выделения памяти также является дорогостоящим, поэтому выделите разумный начальный и максимальный объем памяти для кучи. Если вы знаете его, то не имеет смысла начинать с небольшого начального размера кучи с самого начала, JVM расширит это пространство памяти. Указание параметров памяти выполняется с помощью следующих параметров:
Начальный размер кучи -Xms512m - установите начальный размер кучи на 512 мегабайт.
Максимальный размер кучи -Xmx1024m - установите максимальный размер кучи 1024 мегабайта.
Размер стека потоков -Xss1m - установите размер стека потоков равным 1 мегабайту.
Размер поколения -Xmn256m - установите размер поколения 256 мегабайт.
Если приложение Java выдает ошибку OutOfMemoryError и вам нужна дополнительная информация для обнаружения утечки, запустите процесс с –XX:HeapDumpOnOutOfMemory параметром, который создаст файл дампа кучи, когда эта ошибка произойдет в следующий раз.
Используйте опцию -verbose:gc , чтобы получить вывод процесса сборки мусора. Каждый раз, когда происходит сборка мусора, будет генерироваться вывод.
Стек (Stack)
Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (также известных в Java как примитивные типы), которые содержат само значение, а не ссылку на объект из кучи.
Кроме того, переменные в стеке имеют определенную видимость, также называемую областью видимости. Используются только объекты из активной области. Например, предполагая, что у нас нет никаких глобальных переменных (полей) области видимости, а только локальные переменные, если компилятор выполняет тело метода, он может получить доступ только к объектам из стека, которые находятся внутри тела метода. Он не может получить доступ к другим локальным переменным, так как они не выходят в область видимости. Когда метод завершается и возвращается, верхняя часть стека выталкивается, и активная область видимости изменяется.
Возможно, вы заметили, что на картинке выше отображено несколько стеков памяти. Это связано с тем, что стековая память в Java выделяется для каждого потока. Следовательно, каждый раз, когда поток создается и запускается, он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.
Сборщик мусора Mark & Sweep
JVM использует отдельный поток демона, который работает в фоне для сборки мусора. Этот процесс запускается при выполнении определённых условий. Сборщик Mark & Sweep обычно работает в два этапа, иногда добавляют третий, в зависимости от используемого алгоритма.
- Разметка: сначала сборщик определяет, какие объекты используются, а какие нет. Те, что используются или доступны для стековых указателей, рекурсивно помечаются как живые.
- Удаление: сборщик проходит по куче и убирает все объекты, которые не помечены как живые. Эти места в памяти помечаются как свободные.
- Сжатие: после удаления неиспользуемых объектов все выжившие объекты перемещают, чтобы они были вместе. Это уменьшает фрагментацию и повышает скорость выделения памяти для новых объектов.
JVM предлагает на выбор несколько разных алгоритмов сборки мусора, и в зависимости от вашего JDK может быть ещё больше вариантов (например, сборщик Shenandoah в OpenJDK). Авторы разных реализаций стремятся к разным целям:
- Пропускная способность: время, затраченное на сборку мусора, а не работу приложения. В идеале, пропускная способность должна быть высокой, то есть паузы на сборку мусора короткие.
- Длительность пауз: насколько долго сборщик мусора мешает исполнению приложения. В идеале, паузы должны быть очень короткими.
- Размер кучи: в идеале, должен быть маленьким.
Куча (heap)
Здесь JVM хранит объекты и динамические данные. Это самая крупная область памяти, в ней работает сборщик мусора. Размером кучи можно управлять с помощью флагов Xms (начальный размер) и Xmx (максимальный размер). Куча не передаётся виртуальной машине целиком, какая-то часть резервируется в качестве виртуального пространства, за счёт которого куча может в будущем расти. Куча делится на пространства «молодого» и «старого» поколения.
- Молодое поколение, или «новое пространство»: область, в которой живут новые объекты. Она делится на «рай» (Eden Space) и «область выживших» (Survivor Space). Областью молодого поколения управляет «младший сборщик мусора» (Minor GC), который также называют «молодым» (Young GC).
- Рай: здесь выделяется память, когда мы создаём новые объекты.
- Область выживших: здесь хранятся объекты, которые остались после работы младшего сборщика мусора. Область делится на две половины, S0 и S1.
Заключение
Знание того, как организована память, дает вам преимущество в написании хорошего и оптимизированного кода с точки зрения ресурсов памяти. Преимущество заключается в том, что вы можете настроить свою работающую JVM, предоставив различные конфигурации, наиболее подходящие для запуска вашего приложения. Выявление и устранение утечек памяти - это очень просто, если использовать правильные инструменты.
В этом руководстве мы рассмотрим плюсы и минусы использования примитивов в Java и соответствующих им классов-оберток.
В языке Java существует два типа переменных: примитивные, например int и boolean, а также ссылочные типы вроде Integer и Boolean (классы-обертки). Для каждого примитивного типа существует, соответствующий ему ссылочный тип.
Классы-обертки являются неизменяемыми: это означает, что после создания объекта его состояние (значение поля value) не может быть изменено; и задекларированы, как final (от этих классов невозможно наследоваться).
Процесс преобразования примитивных типов в ссылочные называется автоупаковкой (autoboxing), а обратный ему — автораспаковкой (unboxing).
Решение, переменные каких типов использовать, основывается на необходимой нам производительности приложения, объеме доступной памяти и значений по умолчанию с которыми мы собираемся работать.
В зависимости от реализации виртуальной машины, эти значения могут изменяться. Например, в виртуальной машине Oracle значения типа boolean сопоставляются со значениями 0 и 1 типа int (это связано с тем, что в VM нет инструкций для работы с булевыми значениями) и, как результат, занимают в памяти 32 бита.
Ссылочные переменные ссылаются на объекты, которые хранятся в куче, к ним мы имеем более медленный доступ (рекомендуем ознакомиться с понятиями Стек и Куча, для лучшего усвоения материала). Ссылочные типы имеют определенные накладные расходы относительно их примитивных аналогов.
Накладные расходы зависят от реализации конкретной JVM. Здесь мы приведем результаты для 64-х битной виртуальной машины со следующими параметрами:
В результате, один экземпляр ссылочного типа на данной JVM занимает 128 бит. За исключением Long и Double, которые занимают 192 бита:
- Boolean — 128 бит
- Byte — 128 бит
- Short, Character — 128 бит
- Integer, Float — 128 бит
- Long, Double — 192 бита
Обратите внимание, переменная типа Boolean занимает в 128 раз больше места чем соответствующий ей примитив, тогда как Integer занимает памяти как 4 int переменные.
который демонстрирует как массивы различных типов потребляют память (m) в зависимости от количества содержащихся элементов (e):
- long, double: m = 128 + 64e
- short, char: m = 128 + 64(e/4)
- byte, boolean: m) = 128 + 64(e/8)
- the rest: m = 128 + 64(e/2)
Это может показаться неожиданным, но массивы примитивных типов long и double занимают больше памяти чем их классы-обертки Long и Double соответственно.
Производительность Java кода, довольно тонкий вопрос, сильно зависящий от аппаратной части устройства на котором он исполняется, от различных оптимизационных процессов, выполняемых компилятором, от конкретной JVM, а также от других процессов, происходящих в операционной системе.
Мы уже упоминали ранее, что переменные примитивных типов живут в стеке, тогда как ссылочные переменные хранят ссылки на объекты расположенные в куче. Это важное свойство, определяющее скорость доступа к данным.
Чтобы продемонстрировать насколько операции над примитивными типами быстрее чем над их классами-обертками, создадим массив из миллиона элементов в котором все элементы одинаковы, кроме последнего. Затем мы попытаемся найти этот элемент и сравним результаты производительности работы массива переменных примитивных типов с массивом ссылочных переменных.
Мы используем известный инструмент тестирования производительности JMH, а результаты операции суммируем в диаграмме:
Как мы видим, даже выполнение такой простой операции требует значительно больше времени для классов-оберток, чем для их примитивных аналогов.
В случае более трудоемких операций, таких как сложение, умножение или деление разница в скорости может значительно увеличиться.
И также присутствуют ссылки на объекты другие ( в данном случае на объект такого же класса ). Собственно вопрос. Где будут храниться локальные переменные и ссылки класса выделенного из main. В Heap или Stack ? Спасибо .
@Alexandr Crospov эмм. а зачем такие тонкости знать простому девелоперу? Я бы понял, если бы вы писали компилятор или там что-то служебное для оси
@Barmaley, согласитесь, простому девелоперу полезно знать, откуда растут ноги у OutOfMemoryError, и почему часть из них просит Java heap, а другая - PermGen.
Процесс сборки мусора
Вне зависимости от того, какой выбран сборщик, в JVM используется два вида сборки — младший и старший сборщик.
Всё ещё ищете ответ? Посмотрите другие вопросы с метками java jvm или задайте свой вопрос.
Связанные
Похожие
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Site design / logo © 2022 Stack Exchange Inc; user contributions licensed under cc by-sa. rev 2022.5.11.42086
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
Привет, Хабр! Представляю вашему вниманию перевод первой части статьи «Java Memory Model» автора Jakob Jenkov.
Прохожу обучение по Java и понадобилось изучить статью Java Memory Model. Перевёл её для лучшего понимания, ну а чтоб добро не пропадало решил поделиться с сообществом. Думаю, для новичков будет полезно, и если кому-то понравится, то переведу остальное.
Первоначальная Java-модель памяти была недостаточно хороша, поэтому она была пересмотрена в Java 1.5. Эта версия модели все ещё используется сегодня (Java 14+).
Java-модель памяти, используемая внутри JVM, делит память на стеки потоков (thread stacks) и кучу (heap). Эта диаграмма иллюстрирует Java-модель памяти с логической точки зрения:
Каждый поток, работающий в виртуальной машине Java, имеет свой собственный стек. Стек содержит информацию о том, какие методы вызвал поток. Я буду называть это «стеком вызовов». Как только поток выполняет свой код, стек вызовов изменяется.
Стек потока содержит все локальные переменные для каждого выполняемого метода. Поток может получить доступ только к своему стеку. Локальные переменные, невидимы для всех других потоков, кроме потока, который их создал. Даже если два потока выполняют один и тот же код, они всё равно будут создавать локальные переменные этого кода в своих собственных стеках. Таким образом, каждый поток имеет свою версию каждой локальной переменной.
Все локальные переменные примитивных типов (boolean, byte, short, char, int, long, float, double) полностью хранятся в стеке потоков и не видны другим потокам. Один поток может передать копию примитивной переменной другому потоку, но не может совместно использовать примитивную локальную переменную.
Куча содержит все объекты, созданные в вашем приложении, независимо от того, какой поток создал объект. К этому относятся и версии объектов примитивных типов (например, Byte, Integer, Long и т.д.). Неважно, был ли объект создан и присвоен локальной переменной или создан как переменная-член другого объекта, он хранится в куче.
Ниже диаграмма, которая иллюстрирует стек вызовов и локальные переменные (они хранятся в стеках), а также объекты (они хранятся в куче):
Локальная переменная может быть примитивного типа, в этом случае она полностью хранится в стеке потока.
Локальная переменная также может быть ссылкой на объект. В этом случае ссылка (локальная переменная) хранится в стеке потоков, но сам объект хранится в куче.
Объект может содержать методы, и эти методы могут содержать локальные переменные. Эти локальные переменные также хранятся в стеке потоков, даже если объект, которому принадлежит метод, хранится в куче.
Переменные-члены объекта хранятся в куче вместе с самим объектом. Это верно как в случае, когда переменная-член имеет примитивный тип, так и в том случае, если она является ссылкой на объект.
Статические переменные класса также хранятся в куче вместе с определением класса.
К объектам в куче могут обращаться все потоки, имеющие ссылку на объект. Когда поток имеет доступ к объекту, он также может получить доступ к переменным-членам этого объекта. Если два потока вызывают метод для одного и того же объекта одновременно, они оба будут иметь доступ к переменным-членам объекта, но каждый поток будет иметь свою собственную копию локальных переменных.
Диаграмма, которая иллюстрирует описанное выше:
Два потока имеют набор локальных переменных. 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, который является примитивным типом. Поскольку эти переменные являются переменными-членами, они все еще хранятся в куче вместе с объектом. В стеке потоков хранятся только локальные переменные.
В серии статей я хочу опровергнуть заблуждения, связанные с управлением памятью, и глубже рассмотреть её устройство в некоторых современных языках программирования — Java, Kotlin, Scala, Groovy и Clojure. Надеюсь, эта статья поможет вам разобраться, что происходит под капотом этих языков. Сначала мы рассмотрим управление памятью в виртуальной машине Java (JVM), которая используется в Java, Kotlin, Scala, Clojure, Groovy и других языках. В первой статье я рассказал и разнице между стеком и кучей, что полезно для понимания этой статьи.
Сначала давайте посмотрим на структуру памяти JVM. Эта структура применяется начиная с JDK 11. Вот какая память доступна процессу JVM, она выделяется операционной системой:
Это нативная память, выделяемая ОС, и её размер зависит от системы, процессор и JRE. Какие области и для чего предназначены?
Процесс сборки мусора
Как обсуждалось ранее, в зависимости от типа ссылки, которую переменная из стека содержит на объект из кучи, в определенный момент времени этот объект становится подходящим для сборщика мусора.
Объекты, подходящие для сборки мусора
Например, все объекты, отмеченные красным цветом, могут быть собраны сборщиком мусора. Вы можете заметить, что в куче есть объект, который имеет строгие ссылки на другие объекты, которые также находятся в куче (например, это может быть список, который имеет ссылки на его элементы, или объект, имеющий два поля типа, на которые есть ссылки). Однако, поскольку ссылка из стека потеряна, к ней больше нельзя получить доступ, так что это тоже мусор.
Чтобы углубиться в детали, давайте сначала упомянем несколько вещей:
Этот процесс запускается автоматически Java, и Java решает, запускать или нет этот процесс.
На самом деле это дорогостоящий процесс. При запуске сборщика мусора все потоки в вашем приложении приостанавливаются (в зависимости от типа GC, который будет обсуждаться позже).
На самом деле это более сложный процесс, чем просто сбор мусора и освобождение памяти.
Несмотря на то, что Java решает, когда запускать сборщик мусора, вы можете явно вызвать System.gc() и ожидать, что сборщик мусора будет запускаться при выполнении этой строки кода, верно?
Это ошибочное предположение.
Вы только как бы просите Java запустить сборщик мусора, но, опять же, Java решать, делать это или нет. В любом случае явно вызывать System.gc() не рекомендуется.
Поскольку это довольно сложный процесс и может повлиять на вашу производительность, он реализован разумно. Для этого используется так называемый процесс «Mark and Sweep». Java анализирует переменные из стека и «отмечает» все объекты, которые необходимо поддерживать в рабочем состоянии. Затем все неиспользуемые объекты очищаются.
Так что на самом деле Java не собирает мусор. Фактически, чем больше мусора и чем меньше объектов помечены как живые, тем быстрее идет процесс. Чтобы сделать это еще более оптимизированным, память кучи на самом деле состоит из нескольких частей. Мы можем визуализировать использование памяти и другие полезные вещи с помощью JVisualVM, инструмента, поставляемого с Java JDK. Единственное, что вам нужно сделать, это установить плагин с именем Visual GC, который позволяет увидеть, как на самом деле структурирована память. Давайте немного увеличим масштаб и разберем общую картину:
Поколения памяти кучи
Когда объект создается, он размещается в пространстве Eden (1). Поскольку пространство Eden не такое уж большое, оно заполняется довольно быстро. Сборщик мусора работает в пространстве Eden и помечает объекты как живые.
Если объект выживает в процессе сборки мусора, он перемещается в так называемое пространство выжившего S0(2). Во второй раз, когда сборщик мусора запускается в пространстве Eden, он перемещает все уцелевшие объекты в пространство S1(3). Кроме того, все, что в настоящее время находится на S0(2), перемещается в пространство S1(3).
Если объект выживает в течение X раундов сборки мусора (X зависит от реализации JVM, в моем случае это 8), скорее всего, он выживет вечно и перемещается в пространство Old(4).
Принимая все сказанное выше, если вы посмотрите на график сборщика мусора (6), каждый раз, когда он запускается, вы можете увидеть, что объекты переключаются на пространство выживших и что пространство Эдема увеличивалось. И так далее. Старое поколение также может быть обработано сборщиком мусора, но, поскольку это большая часть памяти по сравнению с пространством Eden, это происходит не так часто. Метапространство (5) используется для хранения метаданных о ваших загруженных классах в JVM.
Представленное изображение на самом деле является приложением Java 8. До Java 8 структура памяти была немного другой. Метапространство на самом деле называется PermGen область. Например, в Java 6 это пространство также хранит память для пула строк. Поэтому, если в вашем приложении Java 6 слишком много строк, оно может аварийно завершить работу.
Типы сборщиков мусора
Фактически, JVM имеет три типа сборщиков мусора, и программист может выбрать, какой из них следует использовать. По умолчанию Java выбирает используемый тип сборщика мусора в зависимости от базового оборудования.
1. Serial GC (Последовательный сборщик мусора) - однониточный коллектор. В основном относится к небольшим приложениям с небольшим использованием данных. Можно включить, указав параметр командной строки: -XX:+UseSerialGC.
2. Parallel GC (Параллельный сборщик мусора) - даже по названию, разница между последовательным и параллельным будет заключаться в том, что параллельный сборщик мусора использует несколько потоков для выполнения процесса сбора мусора. Этот тип GC также известен как сборщик производительности. Его можно включить, явно указав параметр: -XX:+UseParallelGC.
3. Mostly concurrent GC (В основном параллельный сборщик мусора). Если вы помните, ранее в этой статье упоминалось, что процесс сбора мусора на самом деле довольно дорогостоящий, и когда он выполняется, все потоки приостанавливаются. Однако у нас есть в основном параллельный тип GC, который утверждает, что он работает одновременно с приложением. Однако есть причина, по которой он «в основном» параллелен. Он не работает на 100% одновременно с приложением. Есть период времени, на который цепочки приостанавливаются. Тем не менее, пауза делается как можно короче для достижения наилучшей производительности сборщика мусора. На самом деле существует 2 типа в основном параллельных сборщиков мусора:
3.1 Garbage First - высокая производительность с разумным временем паузы приложения. Включено с опцией: -XX:+UseG1GC.
3.2 Concurrent Mark Sweep (Параллельное сканирование отметок) - время паузы приложения сведено к минимуму. Он может быть использован с помощью опции: -XX:+UseConcMarkSweepGC . Начиная с JDK 9, этот тип GC объявлен устаревшим.
Читайте также: