Java что такое нативная память

Обновлено: 07.07.2024

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

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

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

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

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

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

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

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

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


Сначала давайте посмотрим на структуру памяти JVM. Эта структура применяется начиная с JDK 11. Вот какая память доступна процессу JVM, она выделяется операционной системой:


Это нативная память, выделяемая ОС, и её размер зависит от системы, процессор и JRE. Какие области и для чего предназначены?

Куча (heap)

Здесь JVM хранит объекты и динамические данные. Это самая крупная область памяти, в ней работает сборщик мусора. Размером кучи можно управлять с помощью флагов Xms (начальный размер) и Xmx (максимальный размер). Куча не передаётся виртуальной машине целиком, какая-то часть резервируется в качестве виртуального пространства, за счёт которого куча может в будущем расти. Куча делится на пространства «молодого» и «старого» поколения.

  • Молодое поколение, или «новое пространство»: область, в которой живут новые объекты. Она делится на «рай» (Eden Space) и «область выживших» (Survivor Space). Областью молодого поколения управляет «младший сборщик мусора» (Minor GC), который также называют «молодым» (Young GC).
    • Рай: здесь выделяется память, когда мы создаём новые объекты.
    • Область выживших: здесь хранятся объекты, которые остались после работы младшего сборщика мусора. Область делится на две половины, S0 и S1.

    Стеки потоков исполнения

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

    Метапространство

    Это часть нативной памяти, по умолчанию у неё нет верхней границы. В более ранних версиях JVM эта память называлась пространством постоянного поколения (Permanent Generation (PermGen) Space). Загрузчики классов хранили в нём определения классов. Если это пространство растёт, то ОС может переместить хранящиеся здесь данные из оперативной в виртуальную память, что может замедлить работу приложения. Избежать этого можно, задав размер метапространства с помощью флагов XX:MetaspaceSize и -XX:MaxMetaspaceSize , в этом случае приложение может выдавать ошибки памяти.

    Кеш кода

    Здесь компилятор Just In Time (JIT) хранит скомпилированные блоки кода, к которым приходится часто обращаться. Обычно JVM интерпретирует байткод в нативный машинный код, однако код, скомпилированный JIT-компилятором, не нужно интерпретировать, он уже представлен в нативном формате и закеширован в этой области памяти.

    Общие библиотеки

    Здесь хранится нативный код для любых общих библиотек. Эта область памяти загружается операционной системой лишь один раз для каждого процесса.

    Теперь давайте посмотрим, как исполняемая программа использует самые важные части памяти. Воспользуемся нижеприведённым кодом. Он не оптимизирован с точки зрения корректности, так что игнорируйте проблемы вроде ненужных промежуточных переменных, некорректных модификаторов и прочего. Его задача — визуализировать использование стека и кучи.


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

    • Каждый вызов функции добавляется в стек потока исполнения в качестве фреймового блока.
    • Все локальные переменные, включая аргументы и возвращаемые значения, сохраняются в стеке внутри фреймовых блоков функций.
    • Все примитивные типы вроде int хранятся прямо в стеке.
    • Все типы объектов вроде Employee, Integer или String создаются в куче, а затем на них ссылаются с помощью стековых указателей. Это верно и для статичных данных.
    • Функции, которые вызываются из текущей функции, попадают наверх стека.
    • Когда функция возвращает данные, её фрейм удаляется из стека.
    • После завершения основного процесса объекты в куче больше не имеют стековых указателей и становятся потерянными (сиротами).
    • Пока вы явно не сделаете копию, все ссылки на объекты внутри других объектов делаются с помощью указателей.

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

    JVM управляет куче с помощью сборки мусора. Чтобы освободить место для создания нового объекта, JVM очищает память, занятую потерянными объектами, то есть объектами, на которые больше нет прямых или опосредованных ссылок из стека.


    Сборщик мусора в JVM отвечает за:

    • Получение памяти от ОС и возвращение её ОС.
    • Передачу выделенной памяти приложению по его запросу.
    • Определение, какие части выделенной памяти ещё используются приложением.
    • Затребование неиспользованной памяти для использования приложением.

    Сборщик мусора Mark & Sweep

    JVM использует отдельный поток демона, который работает в фоне для сборки мусора. Этот процесс запускается при выполнении определённых условий. Сборщик Mark & Sweep обычно работает в два этапа, иногда добавляют третий, в зависимости от используемого алгоритма.

    • Разметка: сначала сборщик определяет, какие объекты используются, а какие нет. Те, что используются или доступны для стековых указателей, рекурсивно помечаются как живые.
    • Удаление: сборщик проходит по куче и убирает все объекты, которые не помечены как живые. Эти места в памяти помечаются как свободные.
    • Сжатие: после удаления неиспользуемых объектов все выжившие объекты перемещают, чтобы они были вместе. Это уменьшает фрагментацию и повышает скорость выделения памяти для новых объектов.

    JVM предлагает на выбор несколько разных алгоритмов сборки мусора, и в зависимости от вашего JDK может быть ещё больше вариантов (например, сборщик Shenandoah в OpenJDK). Авторы разных реализаций стремятся к разным целям:

    • Пропускная способность: время, затраченное на сборку мусора, а не работу приложения. В идеале, пропускная способность должна быть высокой, то есть паузы на сборку мусора короткие.
    • Длительность пауз: насколько долго сборщик мусора мешает исполнению приложения. В идеале, паузы должны быть очень короткими.
    • Размер кучи: в идеале, должен быть маленьким.

    Сборщики в JDK 11

    JDK 11 — это текущая версия LTE. Ниже приведён список доступных в ней сборщиков мусора, и JVM выбирает по умолчанию один из них в зависимости от текущего оборудования и операционной системы. Мы всегда можем принудительно выбрать какой-либо сборщик с помощью переключателя -XX .

    • Серийный сборщик: использует один поток, эффективен для приложений с небольшим количеством данных, наиболее удобен для однопроцессорных машин. Его можно выбрать с помощью -XX:+UseSerialGC .
    • Параллельный сборщик: нацелен на высокую пропускную способность и использует несколько потоков, чтобы ускорить процесс сборки. Предназначен для приложений со средним или большим количеством данных, исполняемых на многопоточном/многопроцессорном оборудовании. Его можно выбрать с помощью -XX:+UseParallelGC .
    • Сборщик Garbage-First (G1): работает по большей части многопоточно (то есть многопоточно выполняются только объёмные задачи). Предназначен для многопроцессорных машин с большим объёмом памяти, по умолчанию используется на большинстве современных компьютеров и ОС. Нацелен на короткие паузы и высокую пропускную способность. Его можно выбрать с помощью -XX:+UseG1GC .
    • Сборщик Z: новый, экспериментальный, появился в JDK11. Это масштабируемый сборщик с низкой задержкой. Многопоточный и не останавливает исполнение потоков приложения, то есть не относится к stop-the-world. Предназначен для приложений, которым необходима низкая задержка и/или очень большая куча (на несколько терабайтов). Его можно выбрать с помощью -XX:+UseZGC .

    Процесс сборки мусора

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

    Младший сборщик

    Он поддерживает чистоту и компактность пространства молодого поколения. Запускается тогда, когда JVM не может получить в раю необходимую память для размещения нового объекта. Изначально все области кучи пусты. Рай заполняется первым, за ним область выживших, и в конце хранилище.

    Здесь вы можете увидеть процесс работы этого сборщика:

    1. Допустим, в раю уже есть объекты (блоки с 01 по 06 помечены как используемые).
    2. Приложение создаёт новый объект (07).
    3. JVM пытается получить необходимую память в раю, но там уже нет места для размещения нового объекта, поэтому JVM запускает младший сборщик.
    4. Он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные — как мусор (потерянные).
    5. JVM случайно выбирает один блок из S0 и S1 в качестве «целевого» пространства (To Space), пусть это будет S0. Теперь сборщик перемещает все живые объекты в «целевое» пространство, которое было пустым, когда мы начали работу, и повышает их возраст на единицу.
    6. Затем сборщик очищает рай, и в нём выделяется память для нового объекта.
    7. Допустим, прошло какое-то время, и в раю стало больше объектов (блоки с 07 по 13 помечены как используемые).
    8. Приложение создаёт новый объект (14).
    9. JVM пытается получить в раю нужную память, но там нет свободного места для нового объекта, поэтому JVM снова запускает младший сборщик.
    10. Повторяется этап разметки, который охватывает и те объекты, что находятся в пространстве выживших в «целевом пространстве».
    11. Теперь JVM выбирает в качестве «целевого» свободный блок S1, а S0 становится «исходным». Сборщик перемещает все живые объекты из рая и «исходного» в «целевое» (S1), которое было пустым, и повысил возраст объектов на единицу. Поскольку некоторые объекты сюда не поместились, сборщик переносит их в «хранилище», ведь область выживших не может увеличиваться, и этот процесс называют преждевременным продвижением (premature promotion). Такое может происходить, даже если свободна одна из областей выживших.
    12. Теперь сборщик очищает рай и «исходное» пространство (S0), а новый объект размещается в раю.
    13. Так повторяется при каждой сессии младшего сборщика, выжившие перемещаются между S0 и S1, а их возраст увеличивается. Когда он достигает заданного «максимального порога», по умолчанию это 15, объект перемещается в «хранилище».

    Старший сборщик

    Следит за чистотой и компактностью пространства старого поколения (хранилищем). Запускается при одном из таких условий:

    • Разработчик вызывает в программе System . gc() или Runtime.getRunTime().gc() .
    • JVM решает, что в хранилище недостаточно памяти, потому что оно заполнено в результате прошлых сессий младшего сборщика.
    • Если во время работы младшего сборщика JVM не может получить достаточно памяти в раю или области выживших.
    • Если мы задали в JVM параметр MaxMetaspaceSize и для загрузки новых классов не хватает памяти.
    1. Допустим, прошло уже много сессий младшего сборщика и хранилище почти заполнено. JVM решает запустить старший сборщик.
    2. В хранилище он рекурсивно проходит по графу объектов начиная со стековых указателей и помечает используемые объекты как (используемая память), остальные — как мусор (потерянные). Если старший сборщик запустили в ходе работы младшего сборщика, то его работа охватывает пространство молодого поколения (рай и область выживших) и хранилище.
    3. Сборщик убирает все потерянные объекты и возвращает память.
    4. Если в ходе работы старшего сборщика в куче не осталось объектов, JVM также возвращает память из метапространства, убирая из него загруженные классы, если это относится к полной сборке мусора.

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

    Но для большинства JVM-разработчиков (Java, Kotlin, Scala, Clojure, JRuby, Jython) этого объёма информации будет достаточно. Надеюсь, теперь вы сможете писать более качественный код, создавать более производительные приложения, избегая различных проблем с утечкой памяти.

    Всем привет! Перевод сегодняшнего материала мы хотим приурочить к запуску нового потока по курсу «Разработчик Java», который стартует уже завтра. Что ж начнём.

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


    Два вида памяти

    JVM разделяет память на две основные категории: «кучу» (heap) и «не кучу» (non-heap). Куча — это часть памяти JVM, с которой разработчики наиболее знакомы. Здесь хранятся объекты, созданные приложением. Они остаются там до тех пор, пока не будут убраны сборщиком мусора. Как правило, размер кучи, которую использует приложение, изменяется в зависимости от текущей нагрузки.

    Память вне кучи делится на несколько областей. В HotSpot для изучения областей этой памяти можно использовать механизм Native memory tracking (NMT). Обратите внимание, что, хотя NMT не отслеживает использование всей нативной памяти (например, не отслеживается выделение нативной памяти сторонним кодом), его возможностей достаточно для большинства типичных приложений на Spring. Для использования NMT запустите приложение с параметром -XX:NativeMemoryTracking=summary и с помощью jcmd VM.native_memory summary посмотрите информацию об используемой памяти.

    Давайте посмотрим использование NMT на примере нашего старого друга Petclinic. Диаграмма ниже показывает использование памяти JVM по данным NMT (за вычетом собственного оверхеда NMT) при запуске Petclinic с максимальным размером кучи 48 МБ ( -Xmx48M ):


    Как вы видите, на память вне кучи приходится большая часть используемой памяти JVM, причем память кучи составляет только одну шестую часть от общего объёма. В этом случае это примерно 44 МБ (из которых 33 МБ использовалось сразу после сборки мусора). Использование памяти вне кучи составило в сумме 223 МБ.

    Области нативной памяти

    Compressed class space (область сжатых указателей): используется для хранения информации о загруженных классах. Ограничивается параметром MaxMetaspaceSize . Функция количества классов, которые были загружены.

    Примечание переводчика

    Почему-то автор пишет про «Compressed class space», а не про всю область «Class». Область «Compressed class space» входит в состав области «Сlass», а параметр MaxMetaspaceSize ограничивает размер всей области «Class», а не только «Compressed class space». Для ограничения «Compressed class space» используется параметр CompressedClassSpaceSize .

    Отсюда:
    If UseCompressedOops is turned on and UseCompressedClassesPointers is used, then two logically different areas of native memory are used for class metadata…
    A region is allocated for these compressed class pointers (the 32-bit offsets). The size of the region can be set with CompressedClassSpaceSize and is 1 gigabyte (GB) by default…
    The MaxMetaspaceSize applies to the sum of the committed compressed class space and the space for the other class metadata

    Если включен параметр UseCompressedOops и используется UseCompressedClassesPointers , тогда для метаданных классов используется две логически разные области нативной памяти…

    Для сжатых указателей выделяется область памяти (32-битные смещения). Размер этой области может быть установлен CompressedClassSpaceSize и по умолчанию он 1 ГБ…
    Параметр MaxMetaspaceSize относится к сумме области сжатых указателей и области для других метаданных класса.

    • Thread (потоки): память, используемая потоками в JVM. Функция количества запущенных потоков.
    • Code cache (кэш кода): память, используемая JIT для его работы. Функция количества классов, которые были загружены. Ограничивается параметром ReservedCodeCacheSize . Можно уменьшить настройкой JIT, например, отключив многоуровневую компиляцию (tiered compilation).
    • GC (сборщик мусора): хранит данные, используемые сборщиком мусора. Зависит от используемого сборщика мусора.
    • Symbol (символы): хранит такие символы, как имена полей, сигнатуры методов и интернированные строки. Чрезмерное использование памяти символов может указывать на то, что строки слишком интернированы.
    • Internal (внутренние данные): хранит прочие внутренние данные, которые не входят ни в одну из других областей.

    По сравнению с кучей, память вне кучи меньше изменяется под нагрузкой. Как только приложение загрузит все классы, которые будут использоваться и JIT полностью прогреется, всё перейдет в устойчивое состояние. Чтобы увидеть уменьшение использования области Compressed class space, загрузчик классов, который загрузил классы, должен быть удален сборщиком мусора. Это было распространено в прошлом, когда приложения развертывались в контейнерах сервлетов или серверах приложений (загрузчик классов приложения удалялся сборщиком мусора, когда приложение удалялось с сервера приложений), но с современными подходами к развертыванию приложений это случается редко.

    Настройка JVM

    Настроить JVM для эффективного использования доступной оперативной памяти непросто. Если вы запустите JVM с параметром -Xmx16M и ожидаете, что будет использоваться не более 16 МБ памяти, то вас ждёт неприятный сюрприз.

    Интересной областью памяти JVM является кэш кода JIT. По умолчанию HotSpot JVM будет использовать до 240 МБ. Если кэш кода слишком мал, в JIT может не хватить места для хранения своих данных, и в результате будет снижена производительность. Если кэш слишком велик, то память может быть потрачена впустую. При определении размера кэша важно учитывать его влияние как на использование памяти, так и на производительность.

    К счастью, команда CloudFoundry обладает обширными знаниями о распределении памяти в JVM. Если вы загружаете приложения в CloudFoundry, то сборщик (build pack) автоматически применит эти знания для вас. Если вы не используете CloudFoudry или хотели бы больше понять о том, как настроить JVM, то рекомендуется прочитать описание третьей версии Java buildpack’s memory calculator.

    Что это значит для Spring

    Команда Spring проводит много времени, думая о производительности и использовании памяти, рассматривая возможность использования памяти как в куче, так и вне кучи. Один из способов ограничить использование памяти вне кучи — это делать части фреймворка максимально универсальными. Примером этого является использование Reflection для создания и внедрения зависимостей в бины вашего приложения. Благодаря использованию Reflection количество кода фреймворка, который вы используете, остается постоянным, независимо от количества бинов в вашем приложении. Для оптимизации времени запуска мы используем кэш в куче, очищая этот кэш после завершения запуска. Память кучи может быть легко очищена сборщиком мусора, чтобы предоставить больше доступной памяти вашему приложению.

    На сервере запустил веб-приложение на Java и Tomcat с подключением к MySQL. Для ограничения памяти задал следующие опции:

    Согласно моим ожиданиям, приложении должно занимать в памяти 104Мб (с учётом 64 потоков, реально меньше).

    Но после запуска java-процесс занимает примерно 200Мб (судя по полю RES утилиты top). Через несколько часов ничего неделания, приложение уже занимает больше 300Мб.

    На что расходуются эти 200Мб памяти и можно ли их как-то ограничить средствами Java?

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

    Обновление

    Снял дамп, после нескольких минут активной работы с приложением. При снятии дампа процесс java занимал 300Мб памяти, согласно статистике Tomcat'а использовано было всего 64Мб (Heap + PermGen + CodeCache). Открыл дамп в jVisualVM и там указано "Total bytes: 31 966 394". Подозреваю, что анализом одного лишь heap не обойтись.

    Что интересно под виндой в диспетчере задач java-процесса занимает мало памяти - порядка 64Мб, как и положено. Может всё-таки дело не в утечках, а в какой-то области памяти типа кеша, про которую я ещё не знаю.


    49.4k 72 72 золотых знака 249 249 серебряных знаков 480 480 бронзовых знаков


    Вопрос сколько памяти на самом деле занимет программа вообще крайне не однозначен. Достаточно сказать, что в эти "RES" попадут страницы всех системных .so, к которым обращалась программа. Поскольку это общий ресурс (делится на всех в системе), то можно было бы и не учитывать его, только вот "отделить мух от котлет" задешево в *nix не получается. Спасибо за комментарий. Но если это системные библиотеки, значит есть способ посмотреть, какие из них вызываются процессом, и определить виновника такого большого потребления памяти. Вы не знаете случайно такой способ? Почитайте в man proc разделы о /proc/[pid]/maps, /proc/[pid]/statm, /proc/[pid]/status и анализируйте поведение своего процесса / (а вообще, чего еще можно ожидать от Java :)?) / Сразу хочу сказать, что в реальности такой анализ -- дело неблагодарное, поскольку (если в системе в целом нет проблем с памятью) Вас может волновать лишь активный набор страниц да cache miss rate (а вот как стандартными средствами вытащить статистику по ним, я не знаю)

    Вы не можете контролировать то, что хотите контролировать. -Xmx влияет только на Java Heap и не влияет на потребление памяти JVM нативными средствами, которое зависит от реализации JVM.

    Обслуживание динамической памяти Java и сборщика мусора требует использования нативной памяти, на которое вы не можете повлиять.

    More native memory is required to maintain the state of the memory-management system maintaining the Java heap. Data structures must be allocated to track free storage and record progress when collecting garbage. The exact size and nature of these data structures varies with implementation, but many are proportional to the size of the heap.

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

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

    Bytecode compilation uses native memory (in the same way that a static compiler such as gcc requires memory to run), but both the input (the bytecode) and the output (the executable code) from the JIT must also be stored in native memory. Java applications that contain many JIT-compiled methods use more native memory than smaller applications.

    При компиляции байт-кода используется нативная память (точно так же, как статический компилятор, например gсc, использует память в процессе работы), да и как входные (байт-код) так и выходные данные (машинный код) JIT-компилятора приходится хранить в нативной памяти. То есть Java-приложения, содержащие много компилируемых JIT-ом методов требуют больше нативной памяти.

    Загрузчики классов используют нативную память.

    Java applications are composed of classes that define object structure and method logic. They also use classes from the Java runtime class libraries (such as java.lang.String) and may use third-party libraries. These classes need to be stored in memory for as long as they are being used. How classes are stored varies by implementation.

    Java-приложения состоят из классов, которые определяют структуру и поведение объектов. Классы могут предоставляться как JRE (например java.lang.String ) так и сторонними библиотеками. Эти классы нужно хранить в памяти все время, пока они используются. Способ хранения в классов в памяти, зависит от реализации JVM.

    И это не еще затрагивая потоки. Главное, что нужно понять: параметр -Xmx не контролирует всю выделяемую для JVM память, он влияет только на доступную внутри JVM динамическую память (Java Heap), но далеко не все хранится в ней.

    JVM (виртуальная машина Java) действует как механизм времени выполнения для запуска приложений Java. JVM - это то, что фактически вызывает метод main, присутствующий в коде Java. JVM является частью JRE (Java Runtime Environment).

    Java-приложения называются WORA (Write Once Run Anywhere, Пиши однажды запускай везде). Это означает, что программист может разрабатывать код Java в одной системе и ожидать, что он будет работать в любой другой системе с поддержкой Java без каких-либо настроек. Это все возможно благодаря JVM.

    Когда мы компилируем файл .java, компилятор Java генерирует файлы .class (содержащие байт-код) с такими же именами классов, которые присутствуют в файле .java. Этот файл .class проходит различные этапы, когда мы его запускаем. Эти шаги вместе описывают всю JVM.


    Подсистема загрузчика классов (Class Loader Subsystem)

    В основном подсистема загрузчика классов отвечает за три вида деятельности.

    • загрузка (Loading)
    • связывание (Linking)
    • инициализация (Initialization)

    Загрузка (Loading): загрузчик классов читает файл .class, генерирует соответствующие двоичные данные и сохраняет их в области методов. Для каждого файла .class JVM хранит следующую информацию в области методов.

    • Полностью определенное имя загруженного класса и его непосредственного родительского класса.
    • Является ли файл .class связанным с Class или Interface или Enum
    • Информация о модификаторах, переменных и методах и т. д.

    После загрузки файла .class JVM создает объект типа Class для представления этого файла в памяти кучи (heap). Обратите внимание, что этот объект имеет тип Class, предопределенный в пакете java.lang. Этот объект класса может использоваться программистом для получения информации уровня класса, такой как имя класса, имя родителя, методы и информация о переменной и т. д. Чтобы получить эту ссылку на объект, мы можем использовать метод getClass() класса Object.

    Примечание. Для каждого загруженного файла .class создается только один объект класса.

    Связывание (Linking): выполняет проверку, подготовку и (необязательно) разрешение.

    • Проверка (Verification): обеспечивает правильность файла .class, то есть проверяет, правильно ли отформатирован этот файл и создан ли он корректным компилятором или нет. Если проверка не удалась, мы получаем исключение времени исполнения java.lang.VerifyError.
    • Подготовка (Preparation): JVM выделяет память для переменных класса и инициализирует память значениями по умолчанию.
    • Разрешение (Resolution): это процесс замены символьных ссылок типа непосредственными ссылками. Это делается путем поиска в области метода, чтобы найти ссылку на объект.

    Инициализация (Initialization): на этом этапе всем статическим переменным присваиваются их значения, определенные в коде и статическом блоке (если есть). Это выполняется сверху вниз в классе и от родителя к потомку в иерархии классов.

    В общем, есть три загрузчика классов:

    Примечание: JVM следует принципу делегирования-иерархии для загрузки классов. Загрузчик классов системы делегирует запрос на загрузку в загрузчик классов расширения и загрузчик классов расширения делегирует запрос в загрузчик класса начальной загрузки. Если класс найден в пути начальной загрузки, класс загружается, в противном случае запрос снова передается загрузчику классов расширения, а затем загрузчику классов системы. Наконец, если загрузчик классов системы не может загрузить класс, мы получаем исключение java.lang.ClassNotFoundException во время выполнения.


    Память JVM

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

    Область кучи (heap): информация обо всех объектах хранится в области кучи. Существует также одна область кучи на JVM. Это также общий ресурс.

    Область стека: для каждого потока (thread) JVM создает один стек времени исполнения, который хранится здесь. Каждый блок этого стека называется активационной записью/кадром стека, в котором хранятся вызовы методов. Все локальные переменные этого метода хранятся в соответствующем кадре. После завершения потока стек его выполнения будет уничтожен JVM. Это не общий ресурс.

    Регистры компьютера: хранить адрес текущей инструкции исполнения потока. Очевидно, что каждый поток имеет отдельные регистры компьютера.

    Стеки нативного метода: для каждого потока создается отдельный нативный стек. Он хранит информацию о нативных методах.


    Среда исполнения

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

    • Интерпретатор: он интерпретирует байт-код построчно, а затем выполняет. Недостатком здесь является то, что когда один метод вызывается несколько раз, требуется интерпретация каждый раз.
    • Just-In-Time компилятор (JIT): используется для повышения эффективности интерпретатора. Он компилирует весь байт-код и заменяет его на нативный код, поэтому всякий раз, когда интерпретатор видит повторяющиеся вызовы методов, JIT предоставляет прямой нативный код для этой части, поэтому повторная интерпретация не требуется, таким образом, эффективность повышается.
    • Сборщик мусора (Garbage Collector): уничтожает объекты, на которые нет ссылок.

    Нативный интерфейс Java (JNI)

    Это интерфейс, который взаимодействует с библиотеками нативных методов и предоставляет нативные библиотеки (C, C++), необходимые для выполнения. Это позволяет JVM вызывать библиотеки C/C++ и вызываться библиотеками C/C++, которые могут быть специфичными для аппаратного обеспечения.

    Библиотеки нативных методов

    Это коллекция нативных библиотек (C, C++), которые требуются для механизма исполнения.

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