Post

Java Internals: строки, кеши и скрытые оптимизации памяти

Java Internals: строки, кеши и скрытые оптимизации памяти

Когда мы думаем о Java, в первую очередь вспоминаем синтаксис, библиотеки и фреймворки. Но под капотом JVM есть масса оптимизаций и скрытых механизмов, которые напрямую влияют на то, как работает наш код.

Некоторые из них программист замечает только в крайних случаях — например, когда Integer ведет себя неожиданно, строки сравниваются через ==, а volatile вдруг ускоряет или замедляет приложение.

Интернирование строк

Строки в Java — это особый тип данных. Они неизменяемые (immutable) и очень часто используются в программах. Чтобы уменьшить расход памяти, JVM применяет String Pool — область памяти, где хранятся уникальные строковые литералы.

Пример:

1
2
3
4
5
6
7
8
9
10
11
public class StringPoolDemo {
    public static void main(String[] args) {
        String a = "hello";
        String b = "hello";
        String c = new String("hello");

        System.out.println(a == b); // true — обе строки из пула
        System.out.println(a == c); // false — c создан через new
        System.out.println(a.equals(c)); // true — значения совпадают
    }
}

Что здесь важно:

  • Литералы ("hello") всегда попадают в пул строк;
  • оператор == сравнивает ссылки, а не содержимое;
  • метод equals() сравнивает именно содержимое строк.

Если явно вызвать intern(), JVM вернет ссылку на строку из пула:

1
2
String d = c.intern();
System.out.println(a == d); // true

Практический вывод

Для часто повторяющихся строк (например, идентификаторов, кодов, ключей) интернирование позволяет экономить память и ускорять сравнение. Но злоупотреблять им не стоит: пул хранится в heap, и при массовом использовании можно получить обратный эффект — OutOfMemoryError: Java heap space.

Кеширование оберток (Integer, Long, Boolean, Character)

Java активно использует автобоксинг — автоматическое преобразование примитивов в объектные типы (int → Integer, long → Long и тд). Чтобы не создавать каждый раз новый объект, JVM кеширует некоторые значения оберток.

Пример с Integer

1
2
3
4
5
6
7
8
9
10
11
12
public class WrapperCacheDemo {

    public static void main(String[] args) {
        Integer a = Integer.valueOf(127);
        Integer b = Integer.valueOf(127);
        Integer c = Integer.valueOf(128);
        Integer d = Integer.valueOf(128);

        System.out.println(a == b); // true — кеш
        System.out.println(c == d); // false — новые объекты
    }
}

Почему так работает?

  • JVM кеширует Integer в диапазоне [-128, 127].
  • Для этих чисел valueOf() всегда возвращает один и тот же объект.
  • За пределами диапазона создаются новые объекты.

Важно понимать: при автобоксинге (Integer x = 127;) не вызывается new Integer(127). Компилятор транслирует это в вызов Integer.valueOf(127), который использует внутренний кеш:

1
2
3
4
5
public static Integer valueOf(int i) {
    if (i >= -128 && i <= 127)
        return IntegerCache.cache[i + 128];
    return new Integer(i);
}

Поэтому для значений в пределах диапазона [-128..127] мы всегда получаем один и тот же объект.

Другие типы

  • Boolean.valueOf(true/false) — всегда возвращает два объекта из кеша.
  • Byte.valueOf() — кеширует весь диапазон [-128, 127].
  • Character.valueOf() — кеширует символы \u0000 до \u007f (0–127).
  • Short и Long — кешируют также диапазон [-128, 127].
  • Double и Float — не кешируются.

Настройка диапазона

Для Integer диапазон кеша можно расширить с помощью JVM-параметра:

1
-XX:AutoBoxCacheMax=10000

Теперь все значения до 10000 будут возвращаться из кеша.

Практический вывод

Автобоксинг для маленьких чисел не создает новые объекты — это экономит память. Но сравнивать обертки через == нельзя, только через equals(). Для производительности лучше использовать примитивы, чтобы не тратить ресурсы на боксинг и распаковку.

This post is licensed under CC BY 4.0 by the author.