Найти в Дзене

Познаем VM: Управление памятью в Java

Управление памятью в Java
Теперь попытаемся понять, как реализуются разные кусочки этих управляемых контекстов на примере работающей системы (на примере Java).
Разработка (и понимание) любой системы должно начинаться с понимания требований к этой системе (то есть, что система вообще делает). Почитав часть JVMSpec, то мы не увидим там особого описания желаемых алгоритмов GC или чего-то подобного.

Управление памятью в Java

Теперь попытаемся понять, как реализуются разные кусочки этих управляемых контекстов на примере работающей системы (на примере Java).

Разработка (и понимание) любой системы должно начинаться с понимания требований к этой системе (то есть, что система вообще делает). Почитав часть JVMSpec, то мы не увидим там особого описания желаемых алгоритмов GC или чего-то подобного. Но там есть определенный контракт, накладывающий ограничения на то, что любой memory manager для Java обязан делать.

Память всегда выделяется под какой-то объект. У любой памяти есть некоторая типизационная информация: у malloc такой интерфейс, что он не подразумевает, что ту память, которую мы выделили, у нее есть какая-то объектно-ориентированная метаинформация (какой-то тип или что-то еще), мы можем выделить кусок памяти, и это будет просто кусок памяти.

В Java память всегда выделена под некоторый типизированный объект. Это значит, что где-то должна храниться метаинформация (объект класса, который описыыает данный объект). Не может быть ad-hoc объекта. В JS любой объект -- словарь с динамическим содержимым, и привязка к какой-то метаинформации, информации о типах, достаточно условно. Даже объектно-ориентированный, managed рантайм не обязан быть типизированным.

В Java у памяти всегда есть некоторое значение по умолчанию. В том же машинном коде, если мы посмотрим на содержимое памяти, то, что там записано, мы понятия не имеем. В случае JVM память всегда инициализируется в некоторое значение по умолчанию (null для объектных ссылок, false для boolean, нулевой символ для char и т.д.) Для эффективности реализации сделано так, чтобы реальное физ представление в памяти дефолтного значения всегда было одно и то же (нулевые биты), хотя понятно, что в реальной интерпретации для float это будет 0.0, и так далее. Представление информации может быть разным для разных типов, но побитовое представление всегда нулевое.

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

Абсолютно корректная реализация JVM -- такая, которая вообще не освобождает память. Если у компьютера бесконечная память, то можно написать JVm вообще без GC, то есть, будет простой memory manager, который при необходимости выделить выделяет, но ничего не удаляет.

Ошибкой является отождествление времени жизни объекта с жизненным циклом программы. Объект может удалиться непонятно когда. В языке Java есть метод Object.finalize(), который можно перекрыть и который должен вызываться при достижении объектом конца своей жизни. Кое-кто помещает туда освобождение независимых ресурсов (например, закрытие файловых дескрипторов). Это логическая ошибка, ведущая к тонким и неприятным моментам.

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

Запрещен алиасинг -- нельзя одну и ту же память интерпретировать несколькими способами (вот в C++ существует union).

Кроме этого, у объектов есть такое понятие, как Object identity -- операция сравнения. Операция == -- сравнение объектных ссылок. Это сложная операция, подразумевающая, что у объекта есть собственное существование, идентичность, то место, где объект хранится. Это нетривиальное свойство. На целочисленных значениях == означает совсем другое: что число, хранящееся внутри объекта, равно другому числу. То есть, в Java есть равенство по значению, а есть равенство по identity -- по тому месту, где хранится объект, и это тоже контракт memory manager'a.

Еще один контракт -- это наличие операции hashCode. Возможность реализации конкретного hashCode -- тоже нетривиальная операция. Зачем он нужен? В системах типа C++ есть неявное стабильное свойство типа address -- место в памяти. В Java такого места, которое всегда одинаково, на самом деле нет, стандарт не подразумевает наличие вот такого свойства, так как объекты могут быть перемещены GC. Поэтому пришлось придумать то, как создать и сохранить в объекте hashCode. У каждого объекта есть identityHashCode, это некоторой стабильный идентификатор, который при существовании объекта не меняется даже при перемещении между поколениями -- уникальный номер объекта.

Следующей свойство языка Java, выгодно отличающее его от плюсов -- при недостатке памяти бросается определенная ошибка OOME, и ее можно перехватить и что-то сделать. (В плюсах был бы краш и грустное сообщение). OOME может включить GC (?)

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

Жизнь объекта:

  • Начинается с того, что это просто участок в некотором heap-e -- месте памяти, которое тем или иным образом выделено менеджером виртуальной машины для того, чтобы выделять в нем память. Для самого heap-а память обычно выделяется прямым запросом у ОС достаточно большого региона. Ключики типа -Xmx регулируют, сколько именно памяти нужно попросить у ОС.
  • После того, как нам поступил запрос на выделение объекта, аллокатор (одна из частей memory mamager-a) выделяет требуемый кусок (обычно немножко выровненный) и заполняет его нулями -- это уже вход для следующей фазы.
  • Записать в этот объект некоторый заголовок (типизационная информация -- ссылка на объект класса, именно она позволяет делать проверки типа instanceof -- выяснять у объекта, какого он собственно типа).
  • Инициализация объекта вызовом конструктора
  • Полноценная жизнь объекта, он участвует во всяких взаимодействиях
  • Объект когда-либо может стать недостижимым. (обсудим, что это, чуть позже)
  • Объект переходит в финализированное состояние
  • Объект во время финализации может перейти в состояние "живое"
  • Но вообще после этого объект находится в фантомном состоянии (точка реального невозврата) и существует исключительно как ключ для информирования фантомных референсов о том, что подлежащий объект уже полностью умер.
  • Когда все эти танцы завершились, он снова становится просто участком в heap-е, могущий стать новым объектом следующих аллокаций.

Познаем VM: Управление памятью в Java