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