Can You Change String Value via Reflection
However, mutating strings via reflection causes catastrophic application-wide side effects:
🟢 Junior Level
30-Second Summary
- In Modern Java (Java 17+, LTS 21): NO, blocked by default. Starting with Java 16 (JEP 396) and Java 17 (JEP 403), the JVM enforces Strong Encapsulation of JDK Internals. Calling
field.setAccessible(true)onString.valueimmediately throws a fatalInaccessibleObjectException. - In Legacy Java (Java 8 and earlier): Technically yes. Using the Reflection API, developers could access the private
valuefield, suppress access checks viasetAccessible(true), and overwrite the backing array elements in place.
However, mutating strings via reflection causes catastrophic application-wide side effects:
- Global String Pool Corruption: Mutating
"Hello"changes the literal globally across all classes and frameworks in the entire JVM. HashMapandHashSetDestruction: The cachedhashfield is not updated; map keys become permanently unlocatable, triggering memory leaks.- Security Invariant Destruction: File paths, user logins, and authentication tokens are silently falsified.
Historical Anti-Pattern: Java 8 Reflection Hack
public class StringReflectionHackJava8 {
public static void main(String[] args) throws Exception {
String greeting = "Hello";
// Access private field 'value' (char[] in Java 8)
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] chars = (char[]) valueField.get(greeting);
chars[0] = 'J'; // Replaces 'H' with 'J'
System.out.println(greeting); // Prints "Jello"!
System.out.println("Hello"); // 💥 ALSO prints "Jello" due to String Pool sharing!
}
}
🟡 Middle Level
Evolution of JVM Defenses: Strong Encapsulation (Java 9 – 17+)
With the Java Platform Module System (Project Jigsaw) in Java 9, Oracle systematically restricted reflective access to internal JDK fields:
- Java 9–15: Emitted runtime warning messages (“An illegal reflective access operation has occurred”).
- Java 16 (JEP 396): Encapsulation enabled by default.
- Java 17 (JEP 403): The escape hatch
--illegal-access=permitwas permanently removed.
Attempting reflection on String.value in Java 17 / 21:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true); // 💥 THIEVES ARE STOPPED HERE!
The JVM aborts the operation immediately:
java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module
The Only JVM Escape Hatch: --add-opens
To bypass encapsulation in Java 17+, you must pass explicit JVM startup flags:
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
[!CAUTION] Utilizing
--add-opens java.base/java.langin production environments introduces severe security vulnerabilities and violates enterprise compliance and security standards.
Internal Representation: Java 8 vs. Java 9+ (Compact Strings)
- Java 8:
private final char[] value; // UTF-16, 2 bytes per char - Java 9+ (JEP 254 Compact Strings):
@Stable private final byte[] value; // 1 byte per char for LATIN1, 2 bytes for UTF16 private final byte coder; // 0 = LATIN1, 1 = UTF16In modern Java, mutating backing data requires manipulating raw
byte[]arrays while accounting for the active encodingcoder.
🔴 Senior Level
JIT C2 Compiler: Constant Folding and Memory Desynchronization
Even if an attacker circumvents module encapsulation via JVM flags or low-level off-heap writes, HotSpot C2 compiler optimizations introduce Undefined Behavior:
- Constant Folding:
- HotSpot C2 treats string constants as absolute compile-time invariants.
- String literal references are directly inlined into machine instructions or CPU registers during compilation.
- Memory Desynchronization:
- The heap
byte[]array may be modified in physical RAM to"HACKED". - However, compiled native JIT methods continue reading the original constant
"ORIGINAL"directly from CPU registers or the code constant segment. - As a result, different threads and execution paths observe contradictory, desynchronized values for the exact same object reference.
- The heap
The sun.misc.Unsafe Attack and Its Sunset (JEP 471)
Historically, reflection blocks could be bypassed via direct memory offsets in sun.misc.Unsafe:
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
long offset = unsafe.objectFieldOffset(String.class.getDeclaredField("value"));
byte[] value = (byte[]) unsafe.getObject(str, offset);
unsafe.putByte(value, 16L, (byte) 'X'); // Direct physical RAM write
Modern JVM Trajectory:
- In modern OpenJDK releases,
objectFieldOffseton fields injava.basethrows an exception. - In Java 23/24 (JEP 471), memory access methods in
sun.misc.Unsafeare deprecated for removal, directing developers toward the safe Foreign Function & Memory API (JEP 454).
4 Tricky Questions
1. If string literal "secret" is modified via reflection under Java 8, what will subsequent System.out.println("secret") statements output?
Answer:
Subsequent System.out.println("secret") calls will print the mutated text (e.g., "pecret")!
The literal "secret" is loaded into the String Constant Pool once during class loading. All code locations referencing "secret" point to that exact single instance in heap memory. Mutating its internal character array modifies the string globally throughout the entire JVM.
2. Why does map.get(key) fail to find entries in a HashMap after reflection modifies the string key’s value, even if searched with the exact same key reference?
Answer:
String caches its hash code in private int hash.
- When the key is first inserted,
hashCode()is calculated and stored inhash, determining the bucket index. - Mutating the backing
valuearray via reflection does not recalculate or clear thehashfield. - When searching with a new string containing the modified text, its newly computed hash code routes to a different bucket, returning
null. - When searching using the mutated instance itself or original text,
equals()compares actual array bytes, which fail equality checks against keys in the bucket. The entry becomes unretrievable, creating a permanent memory leak.
3. Can InaccessibleObjectException be bypassed in Java 17 using a custom SecurityManager?
Answer: No, it cannot!
- Strong encapsulation of modules (JEP 403) is enforced at the JVM runtime layer in
AccessibleObject.checkCanSetAccessible()prior to anySecurityManagerevaluation. - Starting with Java 17 (JEP 411),
SecurityManageris deprecated for removal and disabled by default. The only mechanism to permit access is the JVM command-line flag--add-opens.
4. How does the HotSpot C2 compiler behave when a method returns "status: " + "OK" if the literal "OK" was modified in memory via Unsafe?
Answer:
The method will continue to return the original text "status: OK"!
"status: " and "OK" are compile-time constants. The javac compiler performs constant folding during compilation and emits a single, pre-merged "status: OK" entry into the class Constant Pool. It possesses its own independent byte[] array in the String Pool, completely unaffected by mutations to the separate "OK" instance.
🎯 Interview Cheat Sheet
Summary: Can You Change String Value via Reflection?
- Java 17+: NO, blocked by default with
InaccessibleObjectException. - How to Bypass: Requires JVM startup flag
--add-opens java.base/java.lang=ALL-UNNAMED. - Java 8: Technically possible via
setAccessible(true)onvaluefield.
Consequences of String Mutation
| Impact Area | Architectural Consequence |
|---|---|
| String Pool | Alters all identical string literals globally across the JVM |
HashMap / HashSet |
Cached hash desynchronizes from characters; entries become unlocatable |
| Security | TOCTOU security validation breaches and unauthorized access |
| HotSpot C2 JIT | Desynchronization between CPU register constant-folded values and Heap memory |
Red Flags to Avoid
- ❌ “In modern Java 17/21, you can freely modify String fields using reflection.” (Demonstrates ignorance of JEP 396/403 Strong Encapsulation).
- ❌ “Mutating a String via reflection only impacts that local variable.” (Literals are shared via the String Pool; changes impact the entire JVM).
- ❌ “HashMap will continue to work normally because the reference is identical.” (Hash codes and
equals()byte comparisons will diverge, losing map entries).
Related Topics
- Why is String Class Immutable — String immutability design
- How Does String Pool Work and How is It Related to Immutability — String pool architecture
- What are the Consequences of String Immutability — Architectural benefits and trade-offs
- What Happens If You Modify a Mutable Key in HashMap — Hash bucket corruption