В этом разделе мы поговорим о том, как различные элементы программы сохраняются в памяти PC. Поскольку данное пособие посвящено Котлину как JVM-языку, они сохраняются в памяти JVM как виртуального компьютера. Тем не менее, организация памяти JVM имеет немало общего с организацией памяти настоящего компьютера. Поговорим об этом подробнее.

Биты, байты и килобайты

Единицей измерения информации (например, хранящейся в памяти или на диске) является бит — элементарная ячейка, имеющая всего два состояния (0 и 1, либо true и false). Биты объединяеются в байты — в одном байте 23 = 8 бит. Байты, в свою очередь, объединяются в килобайты (Кб) — в одном килобайте 210 = 1024 байт. Обратите внимание — не 1000 байт, а именно 1024 байт, поскольку компьютеру удобнее, чтобы данные размерности были степенями двойки. В свою очередь, в одном мегабайте (Мб) 1024 Кб, в одном гигабайте (Гб) 1024 Мб, в одном терабайте (Тб) 1024 Гб.

Системы счисления

При хранении чисел в компьютере используется не привычная человеку десятичная система счисления, а более удобная для компьютера двоичная система. При этом используется всего две цифры: 0 и 1. К примеру, число 75 в двоичной системе представляется как 1001011 = 1 х 64 + 0 х 32 + 0 х 16 + 1 x 8 + 0 x 4 + 1 x 2 + 1 x 1.

Поскольку двоичная запись числа является очень длинной, иногда (для удобства связи между человеком и компьютером) программисты применяют восьмеричную системму счисления (в ней восемь цифр от 0 до 7) и шестнадцатеричную систему счисления (в ней, помимо традиционных цифр от 0 до 9, используются цифры a = 10, b = 11, c = 12, d = 13, e = 14, f = 15). Удобство состоит в том, что одной восьмеричной цифре соответствует ровно три двоичных — к примеру, 5 = 101, а одной шестнадцатеричной — ровно четыре двоичных, к примеру, b = 1011. 75 представляется как 113 в восьмеричной и как 4b в двоичной системе счисления.

Шестнадцатеричную константу в Котлине можно записать с префиксом 0x: к примеру, 0x4b. Восьмеричные константы в Котлине не поддерживаются, а двоичные начинаются с префикса 0b: к примеру, 0b1001011. Подробнее о системах счисления и способах перевода чисел из одной системы в другую можно прочитать в статьях Википедии «Система счисления» и «Позиционная система счисления».

Целые числа

Целые числа типа Int занимают в памяти четыре байта, или 32 бита, при хранении используется двоичная система счисления. Модуль числа занимает 31 бит из 32, старший бит занимает знак (он равен 0 для положительных и -1 для отрицательных чисел). Модуль положительных чисел хранится обычным образом, а отрицательных — в так называемом дополнительном коде. При этом вместо модуля числа |x| в памяти хранится значение 231 - |x|, например, 231 - 1 для числа -1 или 0 для числа -231. Использование дополнительного кода позволяет упростить операции сложения и вычитания чисел для компьютера.

Помимо типа Int, в Котлине имеется ещё три целочисленных типа, а именно:

  • Byte — целое число от -128 до 127, занимает один байт;
  • Short — целое число от -32768 до 32767, занимает два байта;
  • Long — целое число от -263 до 263 - 1, занимает восемь байт.

Подобные целые числа устроены по тому же принципу, что и числа типа Int. Следует иметь в виду, что при работе с типом Long нельзя использовать литералы типа Int:

Вместо них используются литералы типа Long с суффиксом L.

Как правило, в программах используется именно тип Int для хранения целых чисел. Исключения составляют следующие случаи:

  • когда диапазон типа Int недостаточен для целей программы, следует использовать Long;
  • когда требуется сохранить большое (измеряемое, минимум, десятками тысяч) количество чисел с маленьким диапазоном — следует использовать Byte или Short для экономии памяти.

Вещественные числа

Вещественные числа хранятся в памяти в так называемом экспоненциальном формате M * 2E. При этом часть бит выделяется на хранение мантиссы M (она находится в ограниченном диапазоне, обычно от 0.5 до 1), а другая часть бит — на хранение порядка E. Как мантисса, так и порядок хранятся в двоичной системе счисления, причём один бит всегда выделяется под знак.

В Котлине имеется два вещественных типа. Один из них, уже известный нам Double, занимает в памяти восемь байт. При этом 53 бита выделяется на мантиссу, а 11 бит на порядок. Более короткий тип Float занимает четыре байта: 24 бита на мантиссу и 8 бит на порядок.

Вещественные литералы по умолчанию имеют тип Double. Для того, чтобы создать литерал типа Float, необходимо использовать суффикс F:

Тип Double является рекомендуемым для хранения вещественных чисел. Тип Float следует использовать, если чисел требуется много, а его точности достаточно.

Символы

Символы в Котлине используют тип Char и занимают в памяти два байта. Вместо символа в памяти хранится его код — номер данного символа в таблице кодировки Юникод. Тип Char поддерживает только символы из так называемого базового многоязыкового диапазона (Basic Multilingual Plane), номера которых находятся в интервале от 0 до 216 — 1. Дополнительные символы с номерами более 216 — 1 могут быть использованы следующим образом:

  • для хранения кода символа может быть использован тип Int;
  • для представления такого символа внутри строки используются два элемента типа Char, идущих подряд.

Напомним, что символьные константы в Котлине записываются в одинарных кавычках. Для представления специальных символов используется экранирование, например:

  • '\t' — табуляция;
  • '\n' — новая строка;
  • '\r' — возврат каретки (этот и предыдущий символ остались от эпохи пишущих машинок, которым, чтобы начать вывод с новой строки, было необходимо выполнить две операции — возврат каретки к началу строки и перевод каретки на новую строку);
  • '\'' — одинарная кавычка;
  • '\"' — двойная кавычка;
  • '\\' — обратный слэш;
  • '\$' — доллар.

Символы, отсутствующие на клавиатуре, могут быть также заданы с помощью шестнадцатеричного номера в таблице Юникод, например, '\uFF00' — символ с номером FF00.

Значения и ссылки

В Котлине существует два способа хранения переменных (параметров) в памяти JVM: хранение значений и хранение ссылок. В любом из этих способов для переменной выделяется ячейка памяти, размер которой зависит от типа переменной, но не превышает 8 байт.

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

При хранении ссылок в ячейку переменной помещается ссылка, при этом значение (содержимое) переменной хранится в специальном участке памяти JVM — куче (heap). Каждому используемому участку памяти кучи соответствует определённый номер, и как раз этот номер и используется в качестве ссылки. То есть, при хранении ссылок для чтения значения переменной необходимо выполнить не одно, а два действия:

  • прочитать номер участка в куче из ячейки переменной;
  • по этому номеру обратиться к куче и прочитать значение переменной.

Хранение ссылок используется для всех составных и нестандартных типов, в частности, для строк, массивов, списков. При изменении переменной в результате выполнения оператора вроде v = …​ изменяется ссылка. Например:

Обратите внимание, что после выполнения трёх приведённых операторов в участке кучи с номером 2 хранится список [4, 5], но ни одна переменная не хранит ссылку на этот участок. Подобный участок через некоторое время будет найден и уничтожен специальной программой JVM — сборщиком мусора, он же Garbage Collector.

Такие типы, как String или List, не предполагают возможность изменения содержимого переменной. Опять-таки при попытке выполнить оператор вида s = …​ изменится ссылка. Например:

При сложении a и b будет создана новая строка AlphaBeta и размещена в участке памяти с номером 3. После этого номер 3 будет записан в переменную b. Отметьте, что c по-прежнему хранит номер 2, а a — номер 1.

Особенно интересна ситуация с типом MutableList, который позволяет изменять и содержимое переменной тоже. Например:

После выполнения оператора b[2] = 5 участок памяти с номером 1 будет хранить список [1, 2, 5]. Поскольку в переменной a хранится тот же номер 1, то вывод на консоль a[2] приведёт к выводу числа 5, хотя раньше этот элемент списка хранил значение 3.

Подобный принцип используют и функции, имеющие параметр с типом MutableList:

При вызове invertPositives номер 1 будет переписан из аргумента a в параметр list. После этого функция invertPositives изменит содержимое списка, используя данный номер, и вызов println(a) выведет [-1, -2, -3] на консоль.

Таким образом, имея дело с типами, хранящимися по ссылке (чаще говорят проще — ссылочные типы), стоит различать действия со ссылками и действия со значениями. К примеру, присваивание name = …​ — это всегда действие со ссылкой. С другой стороны, вызов функции вроде list.isEmpty() или индексация вроде list[i]list[j] = i — это действия с содержимым, причём, некоторые из этих действий только читают содержимое переменной, а некоторые другие — изменяют его.

С учётом этого различия в Котлине определено две разных операции сравнения на равенство: уже известная нам ==и новая ===. Операция a == b — это сравнение содержимого на равенство, которое обычно выполняется с помощью вызова функции a.equals(b) — про неё мы поговорим в разделе 9. Операция a === b — это сравнение ссылок на равенство, для которого не имеет значения, одинаковое содержимое у переменных или нет, важно только, чтобы оно находилось в участке кучи с одинаковым номером. Например:

Здесь a и b имеют одно и то же содержимое, но находятся в участках кучи с разными номерами. Операция !=обратна операции == (сравнение содержимого на неравенство), а операция !==, соответственно — обратна операции === (сравнение ссылок на неравенство).

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

Организация памяти JVM

При запуске программы, написанной на Java или Котлине, все элементы программы хранятся в различных участках памяти виртуальной машины Java. Технически, JVM память может быть разбита на четыре участка:

  • участок для хранения функций (хранит байт-коды всех имеющихся в программе функций);
  • участок для хранения констант (хранит строковые литералы и значения переменных, известные во время компиляции)
  • куча (хранит значения для переменных ссылочного типа)
  • стек (хранит локальные переменные и параметры — ссылки, если их тип ссылочный и значения в противном случае)

Участки для хранения функций и констант во время выполнения программы не меняются или почти не меняются. В куче по мере необходимости (при создании или изменении строк, списков, …​) создаются новые участки памяти. Эти участки разрушаются сборщиком мусора, когда их перестают использовать.

Стек по принципу работы подобен магазину автомата. Рассмотрим пример:

Здесь изначально стек хранил только параметр args, объявленный в функции main и содержащий ссылку на массив. После вызова foo в стеке появляется её параметр n, а после вызова bar — её параметры x и y. Поскольку все эти параметры целые, хранятся их значения, а не ссылки. Затем определяется промежуточная переменная z, которая тоже попадает в стек.

По окончании работы функции bar из стека удаляются верхние переменные xyz, а после окончания работы foo — также переменная n. Таким образом, стек делается больше или меньше в процессе работы функций.

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