Add compact-strings module: Compact Strings (JEP 254) measured with JOL on JDK 25, with and without -XX:-CompactStrings, and compact object headers on 25 vs 27

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
2026-10-01 04:23:27 +00:00
co-authored by Claude Sonnet 5.5
parent fadfd05670
commit fc05eed4b5
26 changed files with 943 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
import java.util.Random;
import org.openjdk.jol.info.GraphLayout;
/**
* One million random lower-case strings of 30 to 55 characters (the same shape the old version of the post used),
* measured with JOL's GraphLayout, then the same strings with one euro sign appended to each.
*/
public class MillionStrings {
static final int COUNT = 1_000_000;
public static void main(String[] args) throws Exception {
System.out.println("java.version = " + System.getProperty("java.version"));
String[] latin = new String[COUNT];
String[] wide = new String[COUNT];
Random r = new Random(1);
long chars = 0;
for (int i = 0; i < COUNT; i++) {
int len = 30 + r.nextInt(26);
StringBuilder sb = new StringBuilder(len);
for (int j = 0; j < len; j++) sb.append((char) ('a' + r.nextInt(26)));
latin[i] = sb.toString();
wide[i] = latin[i] + "\u20AC"; // one non-Latin-1 character flips the whole string to UTF-16
chars += len;
}
int latinCoder0 = 0, wideCoder1 = 0;
for (int i = 0; i < COUNT; i++) {
if (StringLayout.coder(latin[i]) == 0) latinCoder0++;
if (StringLayout.coder(wide[i]) == 1) wideCoder1++;
}
System.out.printf("strings : %,d (average %.2f chars)%n", COUNT, chars / (double) COUNT);
System.out.printf("coder == 0 (LATIN1) in set A : %,d%n", latinCoder0);
System.out.printf("coder == 1 (UTF16) in set B : %,d%n", wideCoder1);
GraphLayout a = GraphLayout.parseInstance((Object) latin);
GraphLayout b = GraphLayout.parseInstance((Object) wide);
System.out.printf("A: lower-case ASCII only : %,d bytes (%.1f MB, %.1f bytes per string)%n", a.totalSize(), a.totalSize() / 1e6, a.totalSize() / (double) COUNT);
System.out.printf("B: same + one euro sign : %,d bytes (%.1f MB, %.1f bytes per string)%n", b.totalSize(), b.totalSize() / 1e6, b.totalSize() / (double) COUNT);
System.out.printf("B / A : %.3f%n", b.totalSize() / (double) a.totalSize());
}
}
+57
View File
@@ -0,0 +1,57 @@
import java.lang.reflect.Field;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
import org.openjdk.jol.vm.VM;
/**
* What one String really costs: the String object, the byte[] it points to, and the coder byte that says how to read it.
* Measured with JOL (org.openjdk.jol:jol-core). Run by run.sh under several JVM flag sets.
*/
public class StringLayout {
static byte coder(String s) throws Exception {
Field f = String.class.getDeclaredField("coder");
f.setAccessible(true); // needs --add-opens java.base/java.lang=ALL-UNNAMED
return f.getByte(s);
}
static byte[] value(String s) throws Exception {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
return (byte[]) f.get(s);
}
static String repeat(char c, int n) { return String.valueOf(c).repeat(n); }
public static void main(String[] args) throws Exception {
System.out.println("java.version = " + System.getProperty("java.version"));
System.out.println("vm.flags = " + String.join(" ", java.lang.management.ManagementFactory.getRuntimeMXBean().getInputArguments()
.stream().filter(a -> a.startsWith("-XX")).toList()));
System.out.println(VM.current().details().lines().filter(l -> l.startsWith("# Objects are 8 bytes") || l.startsWith("# Field sizes")
|| l.contains("Array element") || l.contains("compressed")).reduce("", (a, b) -> a + b + "\n"));
System.out.println("=== java.lang.String, the class");
System.out.println(ClassLayout.parseClass(String.class).toPrintable());
System.out.println("=== one Latin-1 string and one UTF-16 string, field by field");
System.out.println(ClassLayout.parseInstance("hello").toPrintable());
System.out.println(ClassLayout.parseInstance("h\u20ACllo").toPrintable());
System.out.println("=== the byte[] behind each");
System.out.println(ClassLayout.parseInstance(value("hello")).toPrintable());
System.out.println(ClassLayout.parseInstance(value("h\u20ACllo")).toPrintable());
System.out.println("=== footprint by length (String object + its byte[], via GraphLayout)");
System.out.println("chars content coder byte[].length String byte[] total");
for (int n : new int[] {0, 1, 5, 10, 20, 40, 100, 1000}) {
for (char c : new char[] {'a', '\u00E9', '\u20AC'}) { // a = ASCII, e-acute = Latin-1 supplement, euro = needs UTF-16
String s = new String(repeat(c, n).toCharArray()); // a fresh object, never the interned literal
long total = GraphLayout.parseInstance(s).totalSize();
long str = ClassLayout.parseInstance(s).instanceSize();
long arr = ClassLayout.parseInstance(value(s)).instanceSize();
System.out.printf("%5d %-8s %5d %13d %6d %6d %5d%n", n,
c == 'a' ? "ASCII" : c == '\u00E9' ? "U+00E9" : "U+20AC", coder(s), value(s).length, str, arr, total);
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/**
* When does a string stay compact? Prints the coder of strings produced by common operations.
* coder 0 = LATIN1 (1 byte per char), coder 1 = UTF16 (2 bytes per char).
*/
public class Upgrade {
static void show(String label, String s) throws Exception {
System.out.printf("%-46s length=%d coder=%d byte[].length=%d%n", label, s.length(),
StringLayout.coder(s), StringLayout.value(s).length);
}
public static void main(String[] args) throws Exception {
System.out.println("java.version = " + System.getProperty("java.version"));
show("\"hello\" (literal)", "hello");
show("\"h\\u00E9llo\" (e-acute, Latin-1 supplement)", "h\u00E9llo");
show("\"h\\u20ACllo\" (euro sign)", "h\u20ACllo");
show("\"hello\" + \"\\u20AC\" (concat)", "hello".concat("\u20AC"));
String wide = "\u20ACabcdef";
show("wide = \"\\u20ACabcdef\"", wide);
show("wide.substring(1) (all Latin-1 again)", wide.substring(1));
show("wide.replace('\\u20AC', 'E')", wide.replace('\u20AC', 'E'));
show("wide.toLowerCase()", wide.toLowerCase());
show("new String(\"caf\\u00E9\".getBytes(UTF_8), UTF_8)", new String("caf\u00E9".getBytes(java.nio.charset.StandardCharsets.UTF_8), java.nio.charset.StandardCharsets.UTF_8));
show("new String(utf8 bytes of ASCII, UTF_8)", new String("plain".getBytes(java.nio.charset.StandardCharsets.UTF_8), java.nio.charset.StandardCharsets.UTF_8));
show("new String(char[] {'a','b'})", new String(new char[] {'a', 'b'}));
show("new String(char[] {'a','\\u4E2D'})", new String(new char[] {'a', '\u4E2D'}));
show("\"\\uD83D\\uDE00\" (emoji, a surrogate pair)", "\uD83D\uDE00");
StringBuilder sb = new StringBuilder("ascii only");
show("StringBuilder(\"ascii only\").toString()", sb.toString());
sb.append('\u20AC');
show("... after append('\\u20AC'), toString()", sb.toString());
sb.setLength(0);
sb.append("back to ascii");
show("... after setLength(0) + ASCII, toString()", sb.toString());
System.out.println("length() of the emoji: " + "\uD83D\uDE00".length() + " (UTF-16 code units, not code points)");
}
}