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()
. Для производительности лучше использовать примитивы, чтобы не тратить ресурсы на боксинг и распаковку.