Blog
String Theory: Optimizing Memory In High-Load Systems
Strings look innocent because they are everywhere. That is exactly why they become expensive.
In high-load Java systems, one careless string operation inside hot path can create ocean of short-lived garbage. Garbage collector will clean it, yes. Then you will clean incident channel.
How Java Stores Strings
Modern Java stores String as immutable object backed by byte array plus encoding marker, thanks to compact strings introduced after Java 8. Latin-1 text can use one byte per character. Other text uses UTF-16-like representation.
Immutability is useful. It enables safe sharing, hash caching, and string pool behavior. It also means every “change” creates another object.
The string pool stores interned string instances, mostly literals and explicit intern() results. Do not treat it as free compression machine. Interning unbounded user data is memory leak with nice API.
The Loop That Burns Heap
Classic bad code:
String csv = "";
for (Transaction tx : transactions) {
csv += tx.id() + "," + tx.amount() + "
";
}
Each concatenation creates new intermediate content. Compiler can optimize simple expression, but loop accumulation still reallocates growing string again and again.
Use builder with expected size when possible.
StringBuilder csv = new StringBuilder(transactions.size() * 48);
for (Transaction tx : transactions) {
csv.append(tx.id())
.append(',')
.append(tx.amount())
.append('
');
}
return csv.toString();
This is not micro-optimization when loop runs millions times per hour.
Avoid Accidental Formatting Cost
String.format is readable but heavy. It parses format string, handles locale logic, and allocates. Fine for logs and admin screens. Suspicious in transaction hot path.
String key = userId + ':' + accountId;
This is usually better than:
String key = String.format("%s:%s", userId, accountId);
Measure, but know what you measure.
Substring Is Not Old Trick Anymore
Old Java versions had substring sharing backing array, which caused memory retention surprises. Modern Java copies relevant bytes. This avoids leak but means slicing massive stream into many strings allocates.
For parsers, consider streaming APIs, byte buffers, or domain-specific scanner when profiling proves string allocation dominates.
Logging Can Destroy Throughput
This is common crime:
log.debug("payload=" + expensiveSerialize(payload));
Even when debug disabled, concatenation and serialization may happen before logger sees level. Use parameterized logging and guard expensive work.
if (log.isDebugEnabled()) {
log.debug("payload={}", expensiveSerialize(payload));
}
What To Measure
Look at allocation rate, not only CPU. Java Flight Recorder, async-profiler allocation mode, and GC logs will show if strings dominate.
If service spends time allocating temporary request IDs, JSON fragments, cache keys, and log messages, latency will jitter under pressure.
Final Rule
Do not optimize every string. That is amateur performance theater.
Find hot paths. Remove pointless allocation there. Keep normal code readable elsewhere. Production performance is not about cleverness. It is about spending attention where traffic actually goes.