Как узнать, сколько памяти занимает String?
Размер строки в памяти зависит от версии Java и от содержимого строки.
🟢 Junior Level
Размер строки в памяти зависит от версии Java и от содержимого строки.
Простой расчёт для Java 9+:
Общий размер = объект String (~24 байта) + массив byte[] (~16 байт заголовок) + символы
Пример:
String s = "Hello"; // 5 символов Latin-1
// String object: ~24 bytes
// byte[5] array: ~21 byte (16 заголовок + 5 данных, выровнено до 24)
// Итого: ~48 bytes
Как точно измерить: Используйте библиотеку JOL (Java Object Layout):
import org.openjdk.jol.info.GraphLayout;
String s = "Hello";
System.out.println(GraphLayout.parseInstance(s).totalSize());
// Выведет точный размер в байтах, включая сам объект и все связанные данные
Простая аналогия: String — это коробка (объект String), внутри которой лежит другая коробка (массив байтов). Чтобы узнать общий размер, нужно сложить обе коробки.
🟡 Middle Level
Структура объекта String (Java 9+, 64-bit JVM с CompressedOops)
String Object (24 bytes):
├── Mark Word (object header): 12 bytes
├── Class Pointer (compressed): 4 bytes
├── byte[] value (reference): 4 bytes
├── byte coder: 1 byte
├── int hash: 4 bytes
├── Padding: 3 bytes (до 8-byte alignment)
└── TOTAL: 24 bytes (округлено до кратного 8)
byte[] Array:
├── Array Header (Mark + Class): 12 bytes
├── Array length: 4 bytes
├── Data: N bytes (1 byte/char для Latin-1, 2 для UTF-16)
├── Padding: до 8-byte boundary
└── TOTAL: 16 + N (округлено до 8) bytes
Примеры расчёта
| Строка | Java 8 (char[]) |
Java 9+ (Latin-1) | Java 9+ (UTF-16) |
|---|---|---|---|
"" |
40 bytes | 48 bytes | N/A |
"Hello" |
48 bytes | 48 bytes | N/A |
"Привет" |
52 bytes | N/A | 52 bytes |
| 100 chars (Latin-1) | 232 bytes | 140 bytes | N/A |
| 100 chars (mixed) | 232 bytes | N/A | 240 bytes |
Как измерить на практике
JOL (Java Object Layout):
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.17</version>
</dependency>
// Полный размер (String + массив)
long total = GraphLayout.parseInstance(s).totalSize();
// Детальная раскладка
System.out.println(GraphLayout.parseInstance(s).toPrintable());
Таблица типичных ошибок
| Ошибка | Последствия | Решение |
|---|---|---|
Использовать sizeof как в C++ |
В Java нет sizeof |
Используйте JOL или Instrumentation.getObjectSize() |
| Считать только символы, забывая про заголовки | Занижение на ~28–40 bytes | Всегда учитывайте overhead: String object + array header |
| Не учитывать CompressedOops | Неправильные расчёты для Heap > 32GB | Без CompressedOops каждый указатель = 8 bytes вместо 4 |
Сравнение: Java 8 vs Java 9+
| Аспект | Java 8 | Java 9+ (Latin-1) | Java 9+ (UTF-16) |
|---|---|---|---|
| Внутренний массив | char[] (2 bytes/char) |
byte[] (1 byte/char) |
byte[] (2 bytes/char) |
| Поле coder | Нет | 1 byte | 1 byte |
"Hello" размер |
48 bytes | 48 bytes | N/A |
"Привет" размер |
52 bytes | N/A | 52 bytes |
Когда НЕ нужно точно измерять размер String
- Короткоживущие строки — Young GC соберёт их бесплатно
- Небольшое количество строк — overhead незаметен на фоне Heap
- Прототипы и PoC — оптимизируйте только при доказанной проблеме
🔴 Senior Level
Internal Implementation — точный расчёт
64-bit JVM с UseCompressedOops (по умолчанию для Heap < 32GB):
// String object (Java 9+)
// Mark Word: 12 bytes (8 mark word + 4 klass pointer compressed)
// value ref: 4 bytes
// coder: 1 byte
// hash: 4 bytes
// hashIsZero: 1 byte (в некоторых JDK-билдах)
// Padding: до 24 bytes (кратное 8)
// = 24 bytes total
// byte[] array
// Mark Word: 12 bytes
// length: 4 bytes
// data: N bytes
// Padding: до 8-byte boundary
// = 16 + N (rounded up to 8)
Без CompressedOops (-XX:-UseCompressedOops, Heap > 32GB):
- Каждый указатель = 8 bytes вместо 4
- String object: ~40 bytes (vs 24 с compressed)
- byte[] array: ~24 + N bytes
Edge Cases (минимум 3)
1. String Pool — один объект, много ссылок:
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hello";
// Все три ссылки указывают на ОДИН объект в String Pool
// Общая память: 44 bytes (а не 3 × 44 = 132)
2. Substring (Java 7+) — копирует массив:
String huge = "A".repeat(1_000_000); // ~1MB
String sub = huge.substring(0, 5); // "AAAAA"
// sub — отдельный byte[5], а не ссылка на часть huge
// До Java 7: sub шарил массив huge (memory leak при huge.substring(0,5))
// Java 7+: копирует — безопасно, но sub = ~44 bytes
3. Interned strings — дополнительный overhead:
String s = new String("Hello").intern();
// String объект: ~44 bytes
// + entry в StringTable: ~24-40 bytes (зависит от JVM версии, native hashtable entry)
// Итого: ~76 bytes на уникальную интернированную строку
4. CompressedOops отключается при Heap > 32GB:
# При -Xmx64g: CompressedOops может отключиться
# String object: 40 bytes вместо 24
# При 10M строк: +160MB overhead!
5. Substring от UTF-16 строки — наследует UTF-16:
String mixed = "Hello Мир"; // UTF-16 (из-за кириллицы)
String sub = mixed.substring(0, 5); // "Hello" — всё ещё UTF-16!
// sub занимает ~56 bytes (UTF-16: 24 String + 32 byte[10]) вместо ~48 bytes
// (Latin-1: 24 String + 24 byte[5]). Разница ~8 bytes.
// Потеря: ~5 bytes на подстроку
Производительность — реальные измерения
// JOL benchmark
String empty = "";
String latin5 = "Hello";
String cyrillic5 = "Привет";
String latin100 = "A".repeat(100);
GraphLayout.parseInstance(empty).totalSize(); // 40 bytes
GraphLayout.parseInstance(latin5).totalSize(); // 44 bytes
GraphLayout.parseInstance(cyrillic5).totalSize(); // 52 bytes
GraphLayout.parseInstance(latin100).totalSize(); // 140 bytes
| Сценарий | Размер на строку | 1M строк | 10M строк |
|---|---|---|---|
| Empty string | 40 bytes | 40MB | 400MB |
| Latin-1, 5 chars | 44 bytes | 44MB | 440MB |
| UTF-16, 5 chars | 52 bytes | 52MB | 520MB |
| Latin-1, 100 chars | 140 bytes | 140MB | 1.4GB |
| UTF-16, 100 chars | 240 bytes | 240MB | 2.4GB |
Память и GC implications
Heap savings:
- Compact Strings (Java 9+): ~40–50% экономии для ASCII/Latin-1 строк
- При 70% Latin-1 строк в приложении: общее снижение Heap на 20–30%
- Меньше Heap → реже Full GC → ниже latency
GC cycles:
- Young GC: сканирует Eden/Survivor — меньше аллокаций строк → быстрее scan
- Old Gen: меньше объектов → меньше work для marking/compaction
- G1 GC: меньший region size для string-heavy приложений → эффективнее evacuation
Thread Safety
String — immutable, thread-safe. Размер не меняется после создания. coder — final, value — @Stable. Никаких race conditions при чтении размера из нескольких потоков.
Production War Story
Сценарий: Кэш 1M строк в памяти (user profiles, JSON API сервис):
- Java 8: 1M × ~50 bytes (avg) = ~50MB
- Java 9+ Compact: 1M × ~35 bytes (avg, 70% Latin-1) = ~35MB
- Экономия: 15MB → меньше GC pressure, Full GC реже на 25%
Сценарий 2: Highload сервис с -Xmx2g:
- Без CompressedOops: String overhead = ~40 bytes/object
- С CompressedOops: String overhead = ~24 bytes/object
- При 10M объектов: 160MB экономии только на заголовках
- Это разница между стабильной работой и OOM при пиковой нагрузке
Сценарий 3: Log aggregator — хранение 10M log lines в памяти:
- Каждая строка: ~200 bytes (avg, mixed Latin-1/UTF-16)
- Итого: ~2GB только на строки
- Включение
-XX:+UseStringDeduplication: экономия 400MB (дублирующиеся log levels, host names)
Monitoring
# Проверить CompressedOops
java -XX:+PrintFlagsFinal -version 2>&1 | grep UseCompressedOops
# bool UseCompressedOops = true {lp64_product}
# Heap histogram — сколько строк в памяти
jmap -histo:live <pid> | head -30
# num #instances #bytes class name
# 1: 1234567 49382680 java.lang.String
# JOL в runtime
java -javaagent:jol-cli.jar=includes=java.lang.String -jar app.jar
# MAT (Memory Analyzer Tool)
# Heap dump → Dominator Tree → java.lang.String → Shallow/Retained Heap
# JFR — аллокации
java -XX:StartFlightRecording=settings=profile,filename=recording.jfr ...
# В JFR: Memory → Object Allocation — filter by java.lang.String
// Runtime-измерение через Instrumentation
// (требует -javaagent или Attach API)
long size = instrumentation.getObjectSize(stringInstance);
// JOL — полный footprint
System.out.println(GraphLayout.parseInstance(s).toFootprint());
Best Practices для Highload
- Используйте JOL для точного измерения, не считайте вручную
- CompressedOops включён по умолчанию — не отключайте без веской причины
- Compact Strings (Java 9+) дают бесплатную экономию 40–50% для Latin-1
- String Pool: дублирующиеся строки = один объект (экономия при высокой дедупликации)
- Для ultra-low-latency: избегайте String, используйте
byte[]илиByteBuf(Netty) - При Heap > 32GB: CompressedOops отключается → +30–50% overhead на объекты → планируйте capacity
- Для string-heavy приложений: рассмотрите
-XX:+UseStringDeduplication(G1 GC) - Профилируйте перед оптимизацией: иногда String — лишь 5% Heap, и оптимизация не даст профита
🎯 Шпаргалка для интервью
Обязательно знать:
- Размер String = объект (~24 bytes) + byte[] массив (~16 + N bytes), где N зависит от coder
- Latin-1: 1 байт/символ, UTF-16: 2 байта/символ (Java 9+)
"Hello"≈ 48 bytes (24 String + 24 byte[5]),"Привет"≈ 52 bytes (UTF-16)- JOL (Java Object Layout) — библиотека для точного измерения:
GraphLayout.parseInstance(s).totalSize() - CompressedOops (по умолчанию для Heap < 32GB) уменьшает указатели с 8 до 4 байт
- Без CompressedOops (Heap > 32GB): String object ≈ 40 bytes вместо 24
Частые уточняющие вопросы:
- Как точно узнать размер String? — JOL:
GraphLayout.parseInstance(s).totalSize(). ИлиInstrumentation.getObjectSize(). - Почему
""(пустая строка) занимает 48 байт? — 24 bytes (String object) + 24 bytes (пустой byte[] array с заголовком). - Влияет ли String Pool на размер? — Да: дублирующиеся литералы = один объект. 100 ссылок на
"Hello"= 48 bytes total, а не 4800. - Что будет при Heap > 32GB? — CompressedOops отключается, каждый указатель = 8 bytes. При 10M строк: +160MB overhead.
Красные флаги (НЕ говорить):
- ❌ “В Java есть
sizeofкак в C++” — нет, используйте JOL илиInstrumentation - ❌ “Размер String = только длина символов” — забудьте про заголовки объектов (~40 bytes overhead)
- ❌ “CompressedOops всегда включён” — отключается при Heap > 32GB
- ❌ “substring() до сих пор шарит массив” — с Java 7u6 копирует, memory leak исправлен
Связанные темы:
- [[19. Что такое compact strings в Java 9+]]
- [[22. Что такое String deduplication в G1 GC]]
- [[1. Как работает String Pool]]
- [[13. Что делает метод substring() и как он работал до Java 7]]