Як дізнатися, скільки пам'яті займає 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. Що таке компактні рядки в Java 9+]]
- [[22. Що таке дедуплікація рядків в G1 GC]]
- [[1. Як працює String Pool]]
- [[13. Що робить метод substring() і як він працював до Java 7]]