Пул констант

  • Tutorial
Многие знают, что в каждом .class-файле есть замечательная структура данных, которая называется пулом констант. Но далеко не каждый Java-разработчик, глядя на исходник, сможет даже примерно оценить, сколько констант будет создано в пуле.

Возьмём, к примеру, такой код:

System.out.println("Hello World!");

Он транслируется в три инструкции байткода: getstatic (для загрузки статического поля System.out), ldc (для загрузки константной строки «Hello World!») и invokevirtual (для выполнения виртуальной функции println). Попробуйте прикинуть, сколько констант нужно для того, чтобы этот код работал.

Оказывается, эта строка использует 14 констант, которые схематично можно изобразить таким образом:

Как мы видим, большинство констант являются ссылками на другие константы. У каждой константы есть тип, значение и номер, по которому на неё можно сослаться. Вот базовые типы, которые ни на что не ссылаются:
  • Integer — целое число. На них ссылаются из кода, там, где число потребовалось. Числа от -32768 до 32767 компилятор сюда не складывает: их можно явно указать в коде с помощью инструкций iconst_x, bipush и sipush.
  • Long — длинное целое. По историческим причинам занимает две позиции в пуле констант (то есть у следующей константы номер на два больше). Сэкономить можно только на 0L и 1L с помощью lconst_x; 2L уже попадает в пул.
  • Float — вещественное одинарной точности. Специальные инструкции fconst_x есть для 0.0f, 1.0f и 2.0f; остальные используемые числа попадают в пул.
  • Double — вещественное двойной точности. Как и Long, занимает две позиции в пуле. Только числа 0.0 и 1.0 можно использовать без пула с помощью инструкций dconst_x.
  • Utf8 — строковой литерал в кодировке Utf8. Максимальная длина не превышает 65535 байт (символов, конечно, может быть меньше). Как раз на эти литералы ссылаются из других констант чаще всего.

А вот некоторые из ссылочных типов:
  • String — строка. Содержит ссылку на константу типа Utf8. Если вы используете в коде строку, вы ссылаетесь на константу типа String. Выглядит как лишняя сущность, но есть.
  • NameAndType — константа, описывающая имя и тип поля или метода. Содержит две ссылки на Utf8-константы с именем и типом.
  • Class — константа, описывающая имя класса или интерфейса. Содержит ссылку на Utf8-константу с внутренним именем класса.
  • Fieldref — константа, описывающая конкретное поле конкретного класса. Содержит ссылку на Class-константу и ссылку на NameAndType-константу.
  • MethodRef, InterfaceMethodRef — константы, описывающие конкретный метод класса или интерфейса. Содержат ссылки на Class-константу и NameAndType-константу.

Для обращения к полю вам нужно не только его имя, но и полное имя класса, где поле объявлено, а также тип поля. Для обращения к методу вам нужен класс, имя и сигнатура метода. К счастью, вся сигнатура кодируется в одну строку вне зависимости от количества параметров метода: примитивные типы кодируются одной буквой (например, D = double), объекты — буквой L, за которой следует полное имя класса и точка с запятой, а один уровень массива добавляет квадратную скобку. В скобочках идут типы аргументов, а за ними — возвращаемый тип (V — это void). Например, сигнатура (IDLjava/lang/Thread;)Ljava/lang/Object; будет у такого метода:
Object m(int i, double d, Thread t) {...}

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

Если вы автоматически генерируете Java-код, помните, что максимальный номер константы в пуле не превышает 65535, после этого будет ошибка компиляции. Этого не такое большое число, учитывая огромное количество ссылок и то, что long и double занимают по две позиции. Знание устройства пула поможет вам контролировать его заполнение при генерации кода.

Подробнее с устройством пула констант можно ознакомиться в §4.4 спецификации виртуальной машины Java.
Поделиться публикацией

Похожие публикации

Комментарии 16

    +9
    Люблю такого типа статьи, которые не надо в избранное добавлять с мыслями: «Ой как интересно, надо будет когда-нибудь почитать».
    Короткая, понятная.
    Прочитал за 4 минуты и узнал кое-что новое. Спасибо автору.
      +1
      А как Вы (или, кто то другой) эту схему нарисовали?
        0
        Я, в Microsoft Powerpoint.
          0
          Подозреваю, что имелось в виду несколько другое: откуда получили информацию, изображенную на картинке. Как можно посмотреть содержимое пула констант для своего кода?
            +7
            Есть стандартная тулза javap. Запускайте javap -v <имя-класса>.
        0
        Добрый день!
        Вы написали:
        Если вы автоматически генерируете Java-код, помните, что максимальный номер константы в пуле не превышает 65535, после этого будет ошибка компиляции.

        Это означает, что всего констант в классе может быть не больше 65535?
          +3
          В одном классе — да. В приложении, конечно, нет.
          +1
          Не понял, а массив как преставляется (и инициализируется)? Загрузкой по одной константе из пула конструктором?
            +1
            Присоединяюсь к вопросу. Был случай, когда не удалось создать большой массив с инициализацией при создании. Судя по всему как раз из за этого.
              +3
              Ответил ниже. Скорее всего, вы напоролись на ограничение на размер метода (который тоже 65535, в этом случае компилятор напишет Code too large). Добавление одной записи в double-массив занимает 6 байт байткода для индексов 0-5 (когда iconst_x можно использовать), 7 для индексов 6-127 (когда bipush можно использовать) и 8 для индексов 128-32767 (когда sipush можно использовать). Можно прикинуть, что в конструктор влезет инициализация массива немногим больше 8000 элементов. Тут в размер метода вперёд упрётесь, чем в пул констант.
              +1
              Именно загрузкой по одной константе или в <init> (то есть приписывается к конструктору) или в <clinit> (для статических массивов). Пусть в классе объявлено поле вида:
              double a[] = {1.0,2.0,3.0,4.0,5.0,6.0};

              Тогда фактически к каждому (!) конструктору приклеивается кусок кода вида
              a = new double[6];
              a[0] = 1.0;
              a[1] = 2.0;
              a[2] = 3.0;
              a[3] = 4.0;
              a[4] = 5.0;
              a[5] = 6.0;

              По факту байткод немножко короче, потому что используется инструкция dup вместо того, чтобы в каждой строчке заново делать getfield, но суть такая же.
              –2
              String — строка. Содержит ссылку на константу типа Utf8

              А не UTF-16, не?
              +1
              А мне вот интересно, почему на картинке номера констант идут в каком-то странном порядке, с пропусками? Где константа #1 и остальные пропущенные? Или они тратятся на описание метода, класса и пакета, в котором зашит этот код?
                +2
                Да, так и есть. Вот полный код и пул:
                public class Hello {
                  public static void main(String... args) {
                    System.out.println("Hello World!");
                  }
                }


                   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
                   #2 = Fieldref           #16.#17        //  java/lang/System.out:Ljava/io/PrintStream;
                   #3 = String             #18            //  Hello World!
                   #4 = Methodref          #19.#20        //  java/io/PrintStream.println:(Ljava/lang/String;)V
                   #5 = Class              #21            //  Hello
                   #6 = Class              #22            //  java/lang/Object
                   #7 = Utf8               <init>
                   #8 = Utf8               ()V
                   #9 = Utf8               Code
                  #10 = Utf8               LineNumberTable
                  #11 = Utf8               main
                  #12 = Utf8               ([Ljava/lang/String;)V
                  #13 = Utf8               SourceFile
                  #14 = Utf8               Hello.java
                  #15 = NameAndType        #7:#8          //  "<init>":()V
                  #16 = Class              #23            //  java/lang/System
                  #17 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
                  #18 = Utf8               Hello World!
                  #19 = Class              #26            //  java/io/PrintStream
                  #20 = NameAndType        #27:#28        //  println:(Ljava/lang/String;)V
                  #21 = Utf8               Hello
                  #22 = Utf8               java/lang/Object
                  #23 = Utf8               java/lang/System
                  #24 = Utf8               out
                  #25 = Utf8               Ljava/io/PrintStream;
                  #26 = Utf8               java/io/PrintStream
                  #27 = Utf8               println
                  #28 = Utf8               (Ljava/lang/String;)V


                У класса Hello генерируется конструктор по умолчанию (имя конструктора — #7, сигнатура — #8; для объявления метода NameAndType не нужен), который вызывает конструктор родительского класса Object, используя #1 (отсюда и #6, #7, #8, #15, #22). #9 и #10 — названия атрибутов для хранения собственно байткода и таблицы номеров строк (для отладки или распечатки стектрейсов исключений). #11 и #12 нужны для объявления метода main(), #13 — название атрибута с названием исходного файла, а #14 — значение этого атрибута (тоже для отладки и стектрейсов). #21 — имя класса.

                Если найду время, напишу более подробный пост про устройство .class-файлов.
                  +3
                  Спасибо за развёрнутый ответ, всё объяснено просто и доходчиво. А подробный пост был бы очень интересен.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое