How to Find Out How Much Memory a String Occupies
Java lacks a native sizeof operator like C or C++. Consequently, determining the physical memory consumed by a string requires calculating the JVM heap layout of its constituent...
🟢 Junior Level
Java lacks a native sizeof operator like C or C++. Consequently, determining the physical memory consumed by a string requires calculating the JVM heap layout of its constituent components.
In HotSpot JVM, a String is not a single object, but two interconnected objects in the Java Heap:
- The
java.lang.Stringobject instance (wrapper containing object headers, a reference pointer to the backing array, encoding flags, and the cached hashcode). - The independent backing byte array
byte[] value(containing array headers, length metadata, actual character bytes, and 8-byte alignment padding).
In a Nutshell (30 Seconds)
On a standard 64-bit JVM with Compressed OOPs enabled (-XX:+UseCompressedOops, heap size up to 32 GB):
\(\text{Total Memory} = \text{String Object (24 bytes)} + \text{byte[] Array (16 bytes header + data + 8-byte padding)}\)
- For Latin-1 characters (ASCII / Western Europe) in Java 9+: 1 byte per character.
- For Cyrillic, Asian scripts, and emojis (UTF-16): 2 bytes per character.
// Memory calculation for "Hello" (5 characters Latin-1):
// 1. String wrapper object: 24 bytes
// 2. Backing array byte[5]: 16 bytes (header) + 5 bytes (data) = 21 bytes -> padded to 24 bytes
// TOTAL: 24 + 24 = 48 bytes in Java Heap!
For programmatic measurement, the OpenJDK JOL (Java Object Layout) library provides exact heap layout metrics:
import org.openjdk.jol.info.GraphLayout;
String text = "Hello";
long totalBytes = GraphLayout.parseInstance(text).totalSize(); // 48 bytes
🟡 Middle Level
Shallow Size vs. Retained Size
- Shallow Size: The memory consumed solely by the
java.lang.Stringwrapper object itself, excluding any referenced objects. On a 64-bit JVM with Compressed OOPs, the shallow size of aStringis strictly 24 bytes, regardless of string length. - Retained Size (Deep Size): The sum of the shallow size plus all objects exclusively reachable through it (i.e.,
Stringinstance [24 bytes] +byte[] valuearray [16 bytes header + payload + padding]). This is the exact amount of memory freed when the string is garbage-collected.
HotSpot Memory Layout Breakdown (64-bit, Compressed OOPs)
| Component | Field / Segment | Size | Description |
|---|---|---|---|
String Object |
Mark Word | 8 bytes | Locking state, GC age, identity hashcode |
| Compressed Klass Pointer | 4 bytes | Compressed pointer to java.lang.String.class |
|
value reference |
4 bytes | Compressed OOP pointer to byte[] |
|
coder flag |
1 byte | 0 = LATIN1, 1 = UTF16 |
|
hash |
4 bytes | Cached 32-bit hash value | |
hashIsZero (Java 13+) |
1 byte | Flag indicating whether hash is explicitly 0 | |
| Alignment Padding | 2 bytes | Padded to 8-byte boundary | |
| Total Shallow Size | 24 bytes | Fixed for every String instance |
|
byte[] Array |
Mark Word | 8 bytes | GC and locking metadata |
| Compressed Klass Pointer | 4 bytes | Pointer to [B.class (byte array primitive class) |
|
length |
4 bytes | Array element count | |
| Payload Data | $N$ bytes | $N \times 1$ byte for Latin-1, $N \times 2$ bytes for UTF-16 | |
| Alignment Padding | 0 to 7 bytes | Padded to nearest multiple of 8 bytes | |
| Total Array Size | align8(16 + N) |
Variable depending on length and charset |
String Memory Matrix in Java 17 / 21
| String Example | Length | Encoding | String Shallow |
byte[] Array |
Total Retained Size |
|---|---|---|---|---|---|
"" (Empty String) |
0 | Latin-1 | 24 bytes | 16 bytes (0 data) | 40 bytes |
"A" |
1 | Latin-1 | 24 bytes | $16 + 1 \to$ 24 bytes | 48 bytes |
"Hello" |
5 | Latin-1 | 24 bytes | $16 + 5 \to$ 24 bytes | 48 bytes |
"Привет" |
6 | UTF-16 | 24 bytes | $16 + 12 \to$ 32 bytes | 56 bytes |
| 100 characters Latin-1 | 100 | Latin-1 | 24 bytes | $16 + 100 \to$ 120 bytes | 144 bytes |
| 100 characters UTF-16 | 100 | UTF-16 | 24 bytes | $16 + 200 \to$ 224 bytes | 248 bytes |
🔴 Senior Level
The 32 GB Cliff: When Compressed OOPs Are Disabled
The JVM automatically enables Compressed Ordinary Object Pointers (-XX:+UseCompressedOops) as long as -Xmx does not exceed roughly 32 GB:
- In compressed mode, 64-bit memory addresses are encoded into 32-bit pointers with a 3-bit shift ($2^{32} \times 8 = 32 \text{ GB}$).
- What happens when setting
-Xmx33g? Compressed OOPs are disabled. All object pointers and Klass pointers expand from 4 bytes to full 64-bit 8-byte pointers:Stringshallow size increases from 24 to 32 bytes.byte[]array header increases from 16 to 24 bytes.- In an enterprise service holding 100 million strings, header overhead alone swells by 1.6 gigabytes!
- The 32 GB Cliff Effect: A JVM configured with
-Xmx33gfrequently holds less usable application data than one configured with-Xmx31g.
Runtime Profiling: Instrumentation API vs. JOL
The Java platform includes the Java Agent Instrumentation API:
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);
}
}
Critical Gotcha: instrumentation.getObjectSize(str) returns only Shallow Size (24 bytes), whether the string has 5 characters or 5 million characters!
To measure Retained Size without external dependencies like JOL, developers must implement recursive reflection graph traversals using an IdentityHashMap to track visited object references and prevent infinite loops.
Future JVM Evolution: Project Lilliput (Java 21+)
OpenJDK’s Project Lilliput aims to compress object headers across the JVM:
- Under experimental flags (
-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders), object headers are compressed from 12/16 bytes down to a single 64-bit word (8 bytes), combining the Mark Word and a compressed class ID. - This shrinks
Stringinstance shallow size from 24 to 16 bytes, and thebyte[]array header from 16 to 8 bytes, cutting the memory footprint of micro-strings by 30% to 40%.
4 Tricky Questions
1. Why does instrumentation.getObjectSize("Text with 100,000 characters...") return only 24 bytes?
Answer:
Instrumentation.getObjectSize(Object obj) measures strictly the Shallow Size of the supplied object reference:
- It accounts solely for the object’s direct header (Mark Word + Klass Word) and the fields declared directly within that class (primitives and 4-byte reference pointers).
- It does not traverse object graph references.
In
java.lang.String, thevaluefield is merely a 4-byte reference pointing to an independentbyte[]object in the heap. TheStringwrapper is always 24 bytes under Compressed OOPs, while the backing array containing the 100,000 characters resides as a separate heap allocation ignored bygetObjectSize().
2. How much heap memory does an empty string literal "" consume?
Answer:
An empty string "" consumes exactly 40 bytes (on a 64-bit JVM with Compressed OOPs):
- The
java.lang.Stringwrapper: 12 bytes (header) + 4 bytes (valuereference) + 1 byte (coder) + 4 bytes (hash) + 1 byte (hashIsZero) + 2 bytes (padding) = 24 bytes. - The
byte[0]backing array: 12 bytes (header) + 4 bytes (length = 0) + 0 bytes (payload) = 16 bytes. Total: $24 + 16 = \mathbf{40 \text{ bytes}}$.
3. Why can increasing a production server’s heap from -Xmx31g to -Xmx33g cause it to crash with OutOfMemoryError sooner?
Answer:
Because passing the 32 GB threshold disables Compressed OOPs (-XX:+UseCompressedOops turns off):
- Object references and class pointers expand from 32 bits (4 bytes) to 64 bits (8 bytes).
- All object headers, array metadata, and reference fields across every collection, cache, and string expand by 30% to 50%.
- For string- and collection-heavy applications, the increased pointer tax consumes 2 to 4 GB of additional heap space just for object overhead. The effective usable memory capacity at 33 GB is therefore lower than at 31 GB, causing Eden and Old Gen to saturate faster and triggering premature OutOfMemoryErrors.
4. If an application creates 100,000 variables declared as String s = "admin", how much heap memory do they consume in aggregate?
Answer:
They consume exactly 48 bytes across the entire application (plus stack frame pointer storage on thread stacks).
Because "admin" is a string literal, the compiler and JVM canonicalize it into the String Pool. All 100,000 variables reference the exact same physical instance in the heap: one String object (24 bytes) and one shared byte[5] array (24 bytes).
In contrast, if created via new String("admin"), 100,000 independent String instances would be allocated, consuming $100\,000 \times 24 \text{ bytes} \approx 2.4 \text{ MB}$ of heap memory.
🎯 Interview Cheat Sheet
30-Second Summary
“A Java string consists of two heap objects: the
Stringwrapper (Shallow Size = 24 bytes with Compressed OOPs) and its backingbyte[] valuearray (16 bytes header + payload + 8-byte alignment padding). Under Java 9+ Compact Strings, Latin-1 chars take 1 byte and UTF-16 chars take 2 bytes. An empty string consumes 40 bytes, and"Hello"consumes 48 bytes. To measure exact retained heap usage, use OpenJDK JOL (GraphLayout.parseInstance().totalSize()), asInstrumentation.getObjectSize()returns shallow size only.”
Key Takeaways for Senior Interviews
- Shallow vs. Retained: Shallow size is always 24 bytes (with Compressed OOPs); Retained size includes the
byte[]array. - Calculation Formula: $\text{Retained Size} = 24 + \text{align8}(16 + \text{len} \times (1 \ll \text{coder}))$.
- Compressed OOPs Cliff: Exceeding 32 GB heap disables pointer compression, increasing string wrapper size to 32 bytes and array headers to 24 bytes.
- Project Lilliput: Java 21+ compact object headers reduce object headers to 8 bytes, saving up to 40% memory on micro-strings.
- String Pool Deduplication: A million identical literals consume the memory of only a single 48-byte instance in heap.
Red Flags to Avoid
- ❌ Stating that Java has a
sizeofoperator. - ❌ Calculating string size as simply “characters $\times$ 2 bytes” (ignoring the 40+ bytes of object and array header overhead).
- ❌ Assuming
Instrumentation.getObjectSize()returns the total memory of the string including its character buffer. - ❌ Forgetting the 8-byte memory alignment padding applied to all JVM heap allocations.
Related Questions
- What are Compact Strings in Java 9+ — Latin-1 vs UTF-16 representation
- What is String Deduplication in G1 GC — Garbage collector string optimization
- How String Pool Works — Memory reuse mechanics
- Can You Modify String Contents via Reflection — Modifying internal fields