Как узнать, сколько памяти занимает String
В Java нет оператора sizeof, как в C/C++, поэтому размер строки в памяти рассчитывается исходя из внутренней структуры объектов виртуальной машины JVM.
🟢 Junior Level
В Java нет оператора sizeof, как в C/C++, поэтому размер строки в памяти рассчитывается исходя из внутренней структуры объектов виртуальной машины JVM.
Строка String в памяти — это не один объект, а два связанных объекта в куче (Java Heap):
- Сам объект-обертка
java.lang.String(содержит заголовок, ссылку на массив, флаг кодировки и хеш-код). - Независимый массив байтов
byte[] value(содержит заголовок массива, длину и физические данные символов).
Суть в 30 секундах
На стандартной 64-битной JVM с включенным сжатием ссылок (Compressed OOPs, объем кучи до 32 ГБ): \(\text{Общий размер} = \text{Объект String (24 байта)} + \text{Массив byte[] (16 байт заголовок + данные + выравнивание)}\)
- Для латинских символов (ASCII / Latin-1) в Java 9+: 1 байт на символ.
- Для кириллицы и эмодзи (UTF-16): 2 байта на символ.
// Простой расчет для строки "Hello" (5 символов Latin-1):
// 1. Объект String: 24 байта
// 2. Массив byte[5]: 16 байт (заголовок) + 5 байт (данные) = 21 байт -> округление до 24 байт
// ИТОГО: 24 + 24 = 48 байт в памяти кучи!
Для точного программного измерения используется библиотека JOL (Java Object Layout) от команды OpenJDK:
import org.openjdk.jol.info.GraphLayout;
String text = "Hello";
// Возвращает точный размер объекта и всех связанных данных в байтах:
long totalBytes = GraphLayout.parseInstance(text).totalSize(); // 48 байт
🟡 Middle Level
Разделение понятий: Shallow Size против Retained Size
- Shallow Size (поверхностный размер): Количество памяти, занимаемое только самим объектом
java.lang.Stringбез учета объектов, на которые он ссылается. В 64-битной JVM с Compressed OOPs shallow size всегда равен строго 24 байтам. - Retained Size (удерживаемый размер / Deep Size): Память самого объекта плюс память всех объектов, которые будут удалены сборщиком мусора, если этот объект будет уничтожен (то есть
String(24 байта) + массивbyte[] value).
Точная раскладка полей в HotSpot (64-bit, Compressed OOPs)
Объект java.lang.String (24 байта):
├── Mark Word: 8 байт (состояние блокировок, возраст GC, хеш)
├── Compressed Klass Pointer: 4 байта (указатель на класс java.lang.String)
├── Ссылка на byte[] value: 4 байта (Compressed OOP указатель)
├── byte coder: 1 байт (0 = LATIN1, 1 = UTF16)
├── int hash: 4 байта (кэшированный хеш-код)
├── byte hashIsZero (JDK 13+):1 байт (флаг нулевого хеша)
└── Padding (выравнивание): 2 байта (дополнение до кратности 8 байт)
ИТОГО SHALLOW SIZE: 24 байта
Массив byte[] value:
├── Mark Word: 8 байт
├── Compressed Klass Pointer: 4 байта
├── int length: 4 байта (длина массива в байтах)
├── Данные: N байт (1 байт/символ для Latin-1, 2 байта/символ для UTF-16)
└── Padding (выравнивание): до кратности 8 байт
ИТОГО РАЗМЕР МАССИВА: (16 + N + 7) & ~7 байт
Таблица реальных размеров строк в Java 17/21
| Строка | Символов | Кодировка | Размер String | Размер byte[] | Итоговый Retained Size |
| :— | :— | :— | :— | :— | :— |
| "" (пустая строка) | 0 | Latin-1 | 24 байта | 16 байт (данных 0) | 40 байт |
| "A" | 1 | Latin-1 | 24 байта | $16 + 1 \to$ 24 байта | 48 байт |
| "Hello" | 5 | Latin-1 | 24 байта | $16 + 5 \to$ 24 байта | 48 байт |
| "Привет" | 6 | UTF-16 | 24 байта | $16 + 12 \to$ 32 байта | 56 байт |
| 100 символов Latin-1 | 100 | Latin-1 | 24 байта | $16 + 100 \to$ 120 байт | 144 байта |
| 100 символов UTF-16 | 100 | UTF-16 | 24 байта | $16 + 200 \to$ 224 байта | 248 байт |
🔴 Senior Level
Проблема отключения Compressed OOPs при Heap > 32 GB
Флаг -XX:+UseCompressedOops включен по умолчанию, пока максимальный размер кучи -Xmx не превышает 32 ГБ:
- В этом режиме JVM кодирует 64-битные адреса в 32-битные указатели со сдвигом на 3 бита (масштабирование до $2^{32} \times 8 = 32 \text{ ГБ}$).
- Что происходит при
-Xmx33g? Сжатие ссылок отключается. Все ссылки на объекты и Klass Pointers становятся 64-битными (8 байт вместо 4).- Shallow size объекта
Stringувеличивается с 24 до 32 байт. - Заголовок массива
byte[]увеличивается с 16 до 24 байт. - В приложении с 100 миллионами строк накладные расходы только на служебные заголовки возрастают на 1.6 гигабайта!
- Эффект «32-гигабайтного обрыва»: JVM с кучей 33 ГБ часто вмещает меньше реальных бизнес-данных, чем JVM с кучей 31 ГБ.
- Shallow size объекта
Измерение в рантайме: Instrumentation API против JOL
Существует встроенный механизм Java Agent — java.lang.instrument.Instrumentation:
public class ObjectSizeFetcher {
private static Instrumentation instrumentation;
public static void premain(String args, Instrumentation inst) {
instrumentation = inst;
}
public static long getShallowSize(Object o) {
return instrumentation.getObjectSize(o);
}
}
Критический нюанс: instrumentation.getObjectSize(str) возвращает только Shallow Size (24 байта) независимо от того, содержит строка 5 символов или 5 миллионов символов!
Для глубокого измерения графа объектов без JOL разработчики строят рекурсивный обход полей через Reflection с отслеживанием посещенных ссылок (IdentityHashMap), чтобы исключить циклические ссылки.
Будущее: Project Lilliput и Compact Object Headers (Java 21+)
В рамках OpenJDK Project Lilliput ведутся работы по уменьшению заголовков всех Java-объектов:
- В Java 21 доступен экспериментальный флаг
-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders. - Он сжимает заголовок объекта с 12/16 байт до одного 64-битного слова (8 байт), объединяя Mark Word и сжатый ID класса.
- Для строк это уменьшает размер инстанса
Stringс 24 до 16 байт, а заголовок массиваbyte[]— с 16 до 8 байт. Экономия для микрострок составляет до 30–40% памяти.
4 Tricky Questions
1. Почему instrumentation.getObjectSize("Очень длинный текст на 10 000 символов") возвращает всего 24 байта?
Ответ:
Метод Instrumentation.getObjectSize(Object obj) вычисляет исключительно поверхностный размер (Shallow Size) переданного экземпляра:
- Он учитывает только заголовок переданного объекта и поля, непосредственно объявленные в классе (включая примитивы и сами 4-байтовые ссылки на другие объекты).
- Он не переходит по ссылкам на связанные объекты кучи.
Поле
valueвString— это ссылка. Сам объектStringна 64-битной JVM с Compressed OOPs занимает ровно 24 байта независимо от длины текста. Массив символовbyte[]— это отдельный объект в куче, память которого методgetObjectSize()игнорирует.
2. Сколько памяти в куче потребляет строковый литерал "" (пустая строка)?
Ответ:
Пустая строка "" потребляет ровно 40 байт (при 64-bit JVM и Compressed OOPs):
- Инстанс
java.lang.String: 12 байт (заголовок) + 4 байта (ссылка на массив) + 1 байт (coder) + 4 байта (hash) + 1 байт (hashIsZero) + 2 байта (padding) = 24 байта. - Массив
byte[0]: 12 байт (заголовок) + 4 байта (полеlength = 0) + 0 байт данных = 16 байт. Суммарно: $24 + 16 = \mathbf{40 \text{ байт}}$.
3. Почему при увеличении памяти сервера с -Xmx31g до -Xmx33g приложение может упасть с OutOfMemoryError быстрее?
Ответ:
Потому что при переходе границы в ~32 ГБ JVM отключает сжатие ссылок (Compressed OOPs, -XX:+UseCompressedOops становится false):
- Указатели на объекты и указатели на классы увеличиваются с 4 до 8 байт.
- Минимальное выравнивание и размер заголовков всех объектов возрастают. Инстанс
Stringувеличивается с 24 до 32 байт, а заголовок каждого массива — с 16 до 24 байт. - В типичном Java-приложении строки и коллекции составляют до 70% всех объектов. Увеличение служебного оверхеда на 30–50% «съедает» добавленные 2 ГБ памяти и приводит к тому, что эффективная полезная емкость кучи при 33 ГБ становится меньше, чем была при 31 ГБ.
4. Если в приложении создано 100 000 переменных String s = "admin", сколько памяти кучи они суммарно займут?
Ответ:
Они займут ровно 48 байт на всю программу (плюс память под сами переменные на стеках потоков).
Поскольку "admin" — это строковый литерал, JVM помещает его в String Pool. Все 100 000 переменных будут ссылаться на один-единственный физический экземпляр String в памяти кучи (24 байта) и один массив byte[5] (24 байта).
Если же создавать строки через new String("admin"), то будет создано 100 000 изолированных объектов String, что займет $100\,000 \times 24 \text{ байта} \approx 2.4 \text{ МБ}$ кучи.
🎯 Шпаргалка для интервью
30-секундный ответ
«В Java нет оператора
sizeof. Строка состоит из двух объектов в куче: инстансаString(Shallow Size = 24 байта при Compressed OOPs) и массиваbyte[] value(16 байт заголовок + данные + выравнивание до 8 байт). Символ латиницы в Java 9+ занимает 1 байт, кириллицы — 2 байта. Пустая строка занимает 40 байт,"Hello"— 48 байт. Точно измерить размер строки со всеми зависимостями (Retained Size) позволяет библиотека OpenJDK JOL:GraphLayout.parseInstance(str).totalSize()».
Ключевые тезисы для Senior-интервью
- Shallow vs Retained:
Instrumentation.getObjectSize()меряет только shallow (24 байта для любого String); JOL меряет полный граф с массивомbyte[]. - Формула расчета: $\text{Size} = 24 + \text{align8}(16 + \text{len} \times (1 \ll \text{coder}))$.
- Compressed OOPs (32 GB Cliff): При
-Xmx > 32gуказатели становятся 64-битными, размер заголовков и ссылок возрастает, раздувая память на 30–40%. - Project Lilliput: Сжатие заголовков объектов до 8 байт в будущих версиях Java сэкономит до 40% накладных расходов памяти строк.
- String Pool эффект: Миллион одинаковых литералов занимают память всего одного объекта.
Красные флаги (Чего говорить категорически нельзя)
- ❌ Утверждать: «В Java есть оператор
sizeof». - ❌ Считать размер строки как просто «количество букв $\times$ 2 байта», начисто забывая про заголовки объектов
Stringиbyte[](~40 байт оверхеда). - ❌ Полагать, что
Instrumentation.getObjectSize()считает размер строки вместе с массивом символов. - ❌ Забывать про выравнивание памяти (Padding до 8 байт).