How We Cut 1GB Memory Usage in Java with String.intern() and Custom Deserializers
Facing massive FullGC alarms due to an oversized in‑memory cache, we leveraged Java’s String.intern() via a custom fastjson deserializer and a rewritten HashMap to deduplicate low‑entropy strings, slashing heap usage by over 1 GB while preserving functionality.
Background
Our project uses a full‑memory cache to achieve low response time, but when the configuration data grew from a few hundred entries to over one hundred thousand, the heap size exploded and FullGC alarms appeared in the pre‑release environment.
Problem Analysis
The large JSON payload contains many low‑entropy strings that fastjson deserializes by creating new String objects for each value, causing massive heap consumption.
Solution Overview
We decided to exploit the constant‑pool mechanism by explicitly calling String.intern() for the specific values that have limited variations. Since fastjson does not intern values automatically, we created a custom deserializer and a custom map implementation to perform interning without changing the business logic.
Custom Deserializer
public class StringPoolDeserializer implements ObjectDeserializer {
@SuppressWarnings("unchecked")
@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object o) {
if (!type.equals(String.class)) {
throw new JSONException("StringPoolDeserializer can only deserialize String");
}
return (T) ((String) parser.parse(o)).intern();
}
@Override
public int getFastMatchToken() {
return 0;
}
}Custom Map for Map<String, String> Values
Because fastjson’s MapDeserializer uses final methods that cannot be overridden, we replaced the original HashMap with a subclass that overrides put to intern both keys and values.
public class StringPoolMap extends HashMap<String, String> {
@Override
public String put(String key, String value) {
if (key != null) {
key = key.intern();
}
if (value != null) {
value = value.intern();
}
return super.put(key, value);
}
}Results
After applying the deserializer and the custom map, heap usage dropped by 800 MB with almost no increase in metaspace. Further tuning of the map handling reduced total memory consumption from over 1.6 GB to 619 MB, saving roughly 1 GB.
Conclusion
The root cause was low‑entropy data not being compressed; interning strings provided a quick, non‑intrusive fix. Future iterations will redesign the data structures to address the problem at its source.
How String.intern() Works
String.intern()is a native method that delegates to the JVM’s string table. The native implementation ultimately calls JVM_InternString, which looks up the string in shared and local tables, creates a new entry if absent, and returns the canonical reference.
#include "jvm.h"
#include "java_lang_String.h"
JNIEXPORT jobject JNICALL Java_java_lang_String_intern(JNIEnv *env, jobject this) {
return JVM_InternString(env, this);
} oop StringTable::intern(Handle string_or_null_h, const jchar* name, int len, TRAPS) {
unsigned int hash = java_lang_String::hash_code(name, len);
// lookup in shared and local tables
oop found_string = lookup_shared(name, len, hash);
if (found_string != nullptr) {
return found_string;
}
if (_alt_hash) {
hash = hash_string(name, len, true);
}
found_string = do_lookup(name, len, hash);
if (found_string != nullptr) {
return found_string;
}
// not found, create and insert
return do_intern(string_or_null_h, name, len, hash, THREAD);
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alibaba Cloud Developer
Alibaba's official tech channel, featuring all of its technology innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
