diff --git a/README.md b/README.md index 75d82eb..ff84d02 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T | [`sealed/`](sealed) | Sealed Classes and Interfaces: Modelling Domains with Exhaustive switch | a `Payment` model, `permits` and the three subclass modifiers, the `default` trap, generic and recursive hierarchies, the visitor for comparison, run-time enforcement and modules | | [`lazy-constants/`](lazy-constants) | Lazy Constants in JDK 27 (JEP 531): Replacing Double-Checked Locking | `LazyConstant` against the holder idiom and double-checked locking, the 26 to 27 failure-semantics change, `List`/`Map`/`Set.ofLazy`, the preview-flag traps and a JMH read-path benchmark | | [`http3/`](http3) | HTTP/3 in the Java HTTP Client (JEP 517): Migrating from HttpClient 11 | Java 11 code unchanged on 25 and 26, the `HTTP_3` opt-in on client vs request, the three `H3_DISCOVERY` modes, fallback timings (including the 60 s `HTTP_3_URI_ONLY` failure), proxy downgrade, HTTP/1.1 vs 2 vs 3 under iptables packet loss, with a local Hypercorn + aioquic server | +| [`compact-strings/`](compact-strings) | Compact Strings in Java: Measured with JOL on JDK 25 (plus Compact Object Headers) | the `String` object and its `byte[]` field by field, Latin-1 vs UTF-16 footprint by length, one million strings with and without `-XX:-CompactStrings`, what flips a string to UTF-16 (and what flips it back), compact object headers on JDK 25 (opt-in) vs 27 (default), a JMH comparison of six operations | ## Captured output (`docs/output/`) diff --git a/compact-strings/.gitignore b/compact-strings/.gitignore new file mode 100644 index 0000000..d7035d5 --- /dev/null +++ b/compact-strings/.gitignore @@ -0,0 +1 @@ +.lib/ diff --git a/compact-strings/README.md b/compact-strings/README.md new file mode 100644 index 0000000..0b9a551 --- /dev/null +++ b/compact-strings/README.md @@ -0,0 +1,24 @@ +# compact-strings — Compact Strings (JEP 254) measured with JOL, plus compact object headers + +Companion code for the ankurm.com article **Compact Strings in Java: Measured with JOL on JDK 25 (plus Compact Object Headers)**. The explanation lives in the +article, in collapsible sections; this folder holds the runnable sources and the transcripts they produced. + +```bash +JDK25=/path/to/jdk-25 JDK27=/path/to/jdk-27 ./run.sh # regenerates output/01-21 (about 40 seconds; downloads JOL 0.17 and checks its sha1) +./jmh/fetch.sh && JDK25=/path/to/jdk-25 ./jmh/run-jmh.sh # output/30-jmh.txt (about 6 minutes; machine dependent) +``` + +- `src/StringLayout.java` — JOL `ClassLayout` of `String`, of a Latin-1 and a UTF-16 instance and of the `byte[]` behind each; a footprint table by length. Needs `--add-opens java.base/java.lang=ALL-UNNAMED` to read `coder`. +- `src/MillionStrings.java` — one million random lower-case strings (30 to 55 characters), measured with `GraphLayout`, then the same strings with one `€` appended. +- `src/Upgrade.java` — the `coder` of strings produced by concatenation, `substring`, `replace`, `toLowerCase`, `new String(bytes, UTF_8)`, `StringBuilder.toString()`. +- `jmh/` — a JMH 1.37 benchmark of six read-only `String` operations with and without `-XX:-CompactStrings`. +- `output/` — `01`-`06` layouts, `11`-`16` the million-string runs, each under six flag sets (JDK 25 default, `-XX:-CompactStrings`, `-XX:+UseCompactObjectHeaders`, both; JDK 27 default and `-XX:-UseCompactObjectHeaders`), `20` coder transcript, `21` `PrintFlagsFinal`, `30` JMH. + +| Thing | Version | +|---|---| +| JDK 25 | Temurin 25.0.4.1+1 | +| JDK 27 | Amazon Corretto 27+35 (`corretto.aws/downloads/latest/amazon-corretto-27-x64-linux-jdk.tar.gz`) | +| JOL | 0.17 (`org.openjdk.jol:jol-core`, latest release in Maven Central's `maven-metadata.xml`) | +| JMH | 1.37 | + +Everything except the JMH numbers is byte-stable. JOL 0.17 on JDK 27 prints one `Unable to attach Serviceability Agent` line (it reads a flag that no longer exists); it is kept in the transcripts because it is real. diff --git a/compact-strings/jmh/.gitignore b/compact-strings/jmh/.gitignore new file mode 100644 index 0000000..c3af857 --- /dev/null +++ b/compact-strings/jmh/.gitignore @@ -0,0 +1 @@ +lib/ diff --git a/compact-strings/jmh/fetch.sh b/compact-strings/jmh/fetch.sh new file mode 100755 index 0000000..3601956 --- /dev/null +++ b/compact-strings/jmh/fetch.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Downloads JMH 1.37 and its two runtime dependencies from Maven Central into ./lib, checking each against the published .sha1 +set -euo pipefail +cd "$(dirname "$0")"; mkdir -p lib +M=https://repo1.maven.org/maven2 +for a in org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37 \ + org/openjdk/jmh/jmh-generator-annprocess/1.37/jmh-generator-annprocess-1.37 \ + net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4 \ + org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1; do + f="lib/$(basename "$a").jar" + for try in 1 2 3 4 5; do [ -s "$f" ] && break; curl -sfL -o "$f" "$M/$a.jar" || { rm -f "$f"; sleep 5; }; done + want=""; for try in 1 2 3 4 5; do want="$(curl -sfL "$M/$a.jar.sha1" | cut -d' ' -f1)" && [ -n "$want" ] && break; sleep 5; done + got="$(sha1sum "$f" | cut -d' ' -f1)" + [ "$want" = "$got" ] && echo "ok $f $got" || { echo "SHA1 MISMATCH $f"; exit 1; } +done diff --git a/compact-strings/jmh/run-jmh.sh b/compact-strings/jmh/run-jmh.sh new file mode 100755 index 0000000..6a6dcfb --- /dev/null +++ b/compact-strings/jmh/run-jmh.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Builds and runs StringBench on JDK 25 with the default and with -XX:-CompactStrings; writes ../output/30-jmh.txt (about 6 minutes). +# JDK25=/path ./run-jmh.sh (run ./fetch.sh once first) +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE" +JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}" +CP="$(ls lib/*.jar | tr '\n' ':')"; B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +"$JDK25/bin/javac" -cp "$CP" -processorpath "$CP" -d "$B" src/bench/StringBench.java 2>&1 | grep -v '^Note:' +{ + echo "machine: $(nproc) CPUs, $(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //')" + for fl in "" "-XX:-CompactStrings"; do + echo; echo "\$ java $fl org.openjdk.jmh.Main -f 2 -wi 3 -w 1 -i 5 -r 1 ($("$JDK25/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'), JMH 1.37)" + "$JDK25/bin/java" -cp "$B:$CP" org.openjdk.jmh.Main -f 2 -wi 3 -w 1 -i 5 -r 1 -jvmArgs "$fl" 2>&1 | sed -n '/^Benchmark /,$p' + done +} > "$HERE/../output/30-jmh.txt" +cat "$HERE/../output/30-jmh.txt" diff --git a/compact-strings/jmh/src/bench/StringBench.java b/compact-strings/jmh/src/bench/StringBench.java new file mode 100644 index 0000000..5cc05cb --- /dev/null +++ b/compact-strings/jmh/src/bench/StringBench.java @@ -0,0 +1,34 @@ +package bench; + +import org.openjdk.jmh.annotations.*; +import java.util.concurrent.TimeUnit; + +/** + * Read-only String operations on 256-character strings. Every string is built from a char[] so no two share a byte[]. + * Run twice by run-jmh.sh: with the default and with -XX:-CompactStrings. hashCode is deliberately absent: String caches it, + * so a loop would measure the cache, not the hashing. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class StringBench { + String a, b, c; + int hit; + + @Setup + public void setup() { + char[] x = new char[256]; + for (int i = 0; i < x.length; i++) x[i] = (char) ('a' + (i % 26)); + a = new String(x); + b = new String(x); // equal content, different object + x[255] = 'Z'; + c = new String(x); // differs in the last character + } + + @Benchmark public boolean equalsSameContent() { return a.equals(b); } + @Benchmark public int compareToLastCharDiffers() { return a.compareTo(c); } + @Benchmark public int indexOfLastChar() { return a.indexOf('z', 200); } + @Benchmark public int indexOfAbsentString() { return a.indexOf("zyx"); } + @Benchmark public String concatTwo() { return a.concat(b); } + @Benchmark public String toUpper() { return a.toUpperCase(); } +} diff --git a/compact-strings/output/01-layout-jdk25-default.txt b/compact-strings/output/01-layout-jdk25-default.txt new file mode 100644 index 0000000..ac00c3b --- /dev/null +++ b/compact-strings/output/01-layout-jdk25-default.txt @@ -0,0 +1,91 @@ +$ java StringLayout (JDK 25) +java.version = 25.0.4.1 +vm.flags = -XX:+EnableDynamicAgentLoading +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 (object header: class) N/A + 12 4 int String.hash N/A + 16 1 byte String.coder N/A + 17 1 boolean String.hashIsZero N/A + 18 2 (alignment/padding gap) + 20 4 byte[] String.value N/A +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00176520 + 12 4 int String.hash 0 + 16 1 byte String.coder 0 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 101, 108, 108, 111] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00176520 + 12 4 int String.hash 0 + 16 1 byte String.coder 1 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00173550 + 12 4 (array length) 5 + 16 5 byte [B. N/A + 21 3 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00173550 + 12 4 (array length) 10 + 16 10 byte [B. N/A + 26 6 (object alignment gap) +Instance size: 32 bytes +Space losses: 0 bytes internal + 6 bytes external = 6 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 0 0 24 16 40 + 0 U+00E9 0 0 24 16 40 + 0 U+20AC 0 0 24 16 40 + 1 ASCII 0 1 24 24 48 + 1 U+00E9 0 1 24 24 48 + 1 U+20AC 1 2 24 24 48 + 5 ASCII 0 5 24 24 48 + 5 U+00E9 0 5 24 24 48 + 5 U+20AC 1 10 24 32 56 + 10 ASCII 0 10 24 32 56 + 10 U+00E9 0 10 24 32 56 + 10 U+20AC 1 20 24 40 64 + 20 ASCII 0 20 24 40 64 + 20 U+00E9 0 20 24 40 64 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 0 40 24 56 80 + 40 U+00E9 0 40 24 56 80 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 0 100 24 120 144 + 100 U+00E9 0 100 24 120 144 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 0 1000 24 1016 1040 + 1000 U+00E9 0 1000 24 1016 1040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/02-layout-jdk25-no-compact-strings.txt b/compact-strings/output/02-layout-jdk25-no-compact-strings.txt new file mode 100644 index 0000000..07e457f --- /dev/null +++ b/compact-strings/output/02-layout-jdk25-no-compact-strings.txt @@ -0,0 +1,91 @@ +$ java -XX:-CompactStrings StringLayout (JDK 25) +java.version = 25.0.4.1 +vm.flags = -XX:+EnableDynamicAgentLoading -XX:-CompactStrings +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 (object header: class) N/A + 12 4 int String.hash N/A + 16 1 byte String.coder N/A + 17 1 boolean String.hashIsZero N/A + 18 2 (alignment/padding gap) + 20 4 byte[] String.value N/A +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x4d001e38 + 12 4 int String.hash 0 + 16 1 byte String.coder 1 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 0, 101, 0, 108, 0, 108, 0, 111, 0] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x4d001e38 + 12 4 int String.hash 0 + 16 1 byte String.coder 1 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x4d000a00 + 12 4 (array length) 10 + 16 10 byte [B. N/A + 26 6 (object alignment gap) +Instance size: 32 bytes +Space losses: 0 bytes internal + 6 bytes external = 6 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x4d000a00 + 12 4 (array length) 10 + 16 10 byte [B. N/A + 26 6 (object alignment gap) +Instance size: 32 bytes +Space losses: 0 bytes internal + 6 bytes external = 6 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 1 0 24 16 40 + 0 U+00E9 1 0 24 16 40 + 0 U+20AC 1 0 24 16 40 + 1 ASCII 1 2 24 24 48 + 1 U+00E9 1 2 24 24 48 + 1 U+20AC 1 2 24 24 48 + 5 ASCII 1 10 24 32 56 + 5 U+00E9 1 10 24 32 56 + 5 U+20AC 1 10 24 32 56 + 10 ASCII 1 20 24 40 64 + 10 U+00E9 1 20 24 40 64 + 10 U+20AC 1 20 24 40 64 + 20 ASCII 1 40 24 56 80 + 20 U+00E9 1 40 24 56 80 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 1 80 24 96 120 + 40 U+00E9 1 80 24 96 120 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 1 200 24 216 240 + 100 U+00E9 1 200 24 216 240 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 1 2000 24 2016 2040 + 1000 U+00E9 1 2000 24 2016 2040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/03-layout-jdk25-compact-headers.txt b/compact-strings/output/03-layout-jdk25-compact-headers.txt new file mode 100644 index 0000000..2f81601 --- /dev/null +++ b/compact-strings/output/03-layout-jdk25-compact-headers.txt @@ -0,0 +1,89 @@ +$ java -XX:+UseCompactObjectHeaders StringLayout (JDK 25) +java.version = 25.0.4.1 +vm.flags = -XX:+EnableDynamicAgentLoading -XX:+UseCompactObjectHeaders +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 int String.hash N/A + 12 1 byte String.coder N/A + 13 1 boolean String.hashIsZero N/A + 14 2 (alignment/padding gap) + 16 4 byte[] String.value N/A + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017940000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 0 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 101, 108, 108, 111] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017940000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 1 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x00174c0000000001 (Lilliput) + 8 4 (array length) 5 + 12 5 byte [B. N/A + 17 7 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 7 bytes external = 7 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x00174c0000000001 (Lilliput) + 8 4 (array length) 10 + 12 10 byte [B. N/A + 22 2 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 2 bytes external = 2 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 0 0 24 16 40 + 0 U+00E9 0 0 24 16 40 + 0 U+20AC 0 0 24 16 40 + 1 ASCII 0 1 24 16 40 + 1 U+00E9 0 1 24 16 40 + 1 U+20AC 1 2 24 16 40 + 5 ASCII 0 5 24 24 48 + 5 U+00E9 0 5 24 24 48 + 5 U+20AC 1 10 24 24 48 + 10 ASCII 0 10 24 24 48 + 10 U+00E9 0 10 24 24 48 + 10 U+20AC 1 20 24 32 56 + 20 ASCII 0 20 24 32 56 + 20 U+00E9 0 20 24 32 56 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 0 40 24 56 80 + 40 U+00E9 0 40 24 56 80 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 0 100 24 112 136 + 100 U+00E9 0 100 24 112 136 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 0 1000 24 1016 1040 + 1000 U+00E9 0 1000 24 1016 1040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/04-layout-jdk25-compact-headers-no-compact-strings.txt b/compact-strings/output/04-layout-jdk25-compact-headers-no-compact-strings.txt new file mode 100644 index 0000000..277416f --- /dev/null +++ b/compact-strings/output/04-layout-jdk25-compact-headers-no-compact-strings.txt @@ -0,0 +1,89 @@ +$ java -XX:+UseCompactObjectHeaders -XX:-CompactStrings StringLayout (JDK 25) +java.version = 25.0.4.1 +vm.flags = -XX:+EnableDynamicAgentLoading -XX:+UseCompactObjectHeaders -XX:-CompactStrings +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 int String.hash N/A + 12 1 byte String.coder N/A + 13 1 boolean String.hashIsZero N/A + 14 2 (alignment/padding gap) + 16 4 byte[] String.value N/A + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0008540000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 1 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 0, 101, 0, 108, 0, 108, 0, 111, 0] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0008540000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 1 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0008140000000001 (Lilliput) + 8 4 (array length) 10 + 12 10 byte [B. N/A + 22 2 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 2 bytes external = 2 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0008140000000001 (Lilliput) + 8 4 (array length) 10 + 12 10 byte [B. N/A + 22 2 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 2 bytes external = 2 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 1 0 24 16 40 + 0 U+00E9 1 0 24 16 40 + 0 U+20AC 1 0 24 16 40 + 1 ASCII 1 2 24 16 40 + 1 U+00E9 1 2 24 16 40 + 1 U+20AC 1 2 24 16 40 + 5 ASCII 1 10 24 24 48 + 5 U+00E9 1 10 24 24 48 + 5 U+20AC 1 10 24 24 48 + 10 ASCII 1 20 24 32 56 + 10 U+00E9 1 20 24 32 56 + 10 U+20AC 1 20 24 32 56 + 20 ASCII 1 40 24 56 80 + 20 U+00E9 1 40 24 56 80 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 1 80 24 96 120 + 40 U+00E9 1 80 24 96 120 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 1 200 24 216 240 + 100 U+00E9 1 200 24 216 240 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 1 2000 24 2016 2040 + 1000 U+00E9 1 2000 24 2016 2040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/05-layout-jdk27-default.txt b/compact-strings/output/05-layout-jdk27-default.txt new file mode 100644 index 0000000..4d6ed6c --- /dev/null +++ b/compact-strings/output/05-layout-jdk27-default.txt @@ -0,0 +1,90 @@ +$ java StringLayout (JDK 27) +java.version = 27 +vm.flags = -XX:+EnableDynamicAgentLoading +# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.runtime.VM.isCompressedKlassPointersEnabled() +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 int String.hash N/A + 12 1 byte String.coder N/A + 13 1 boolean String.hashIsZero N/A + 14 2 (alignment/padding gap) + 16 4 byte[] String.value N/A + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017cc0000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 0 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 101, 108, 108, 111] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017cc0000000001 (Lilliput) + 8 4 int String.hash 0 + 12 1 byte String.coder 1 + 13 1 boolean String.hashIsZero false + 14 2 (alignment/padding gap) + 16 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] + 20 4 (object alignment gap) +Instance size: 24 bytes +Space losses: 2 bytes internal + 4 bytes external = 6 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017980000000001 (Lilliput) + 8 4 (array length) 5 + 12 5 byte [B. N/A + 17 7 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 7 bytes external = 7 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0017980000000001 (Lilliput) + 8 4 (array length) 10 + 12 10 byte [B. N/A + 22 2 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 2 bytes external = 2 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 0 0 24 16 40 + 0 U+00E9 0 0 24 16 40 + 0 U+20AC 0 0 24 16 40 + 1 ASCII 0 1 24 16 40 + 1 U+00E9 0 1 24 16 40 + 1 U+20AC 1 2 24 16 40 + 5 ASCII 0 5 24 24 48 + 5 U+00E9 0 5 24 24 48 + 5 U+20AC 1 10 24 24 48 + 10 ASCII 0 10 24 24 48 + 10 U+00E9 0 10 24 24 48 + 10 U+20AC 1 20 24 32 56 + 20 ASCII 0 20 24 32 56 + 20 U+00E9 0 20 24 32 56 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 0 40 24 56 80 + 40 U+00E9 0 40 24 56 80 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 0 100 24 112 136 + 100 U+00E9 0 100 24 112 136 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 0 1000 24 1016 1040 + 1000 U+00E9 0 1000 24 1016 1040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/06-layout-jdk27-headers-off.txt b/compact-strings/output/06-layout-jdk27-headers-off.txt new file mode 100644 index 0000000..5aeeed2 --- /dev/null +++ b/compact-strings/output/06-layout-jdk27-headers-off.txt @@ -0,0 +1,92 @@ +$ java -XX:-UseCompactObjectHeaders StringLayout (JDK 27) +java.version = 27 +vm.flags = -XX:+EnableDynamicAgentLoading -XX:-UseCompactObjectHeaders +# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.runtime.VM.isCompressedKlassPointersEnabled() +# Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 +# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 + +=== java.lang.String, the class +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) N/A + 8 4 (object header: class) N/A + 12 4 int String.hash N/A + 16 1 byte String.coder N/A + 17 1 boolean String.hashIsZero N/A + 18 2 (alignment/padding gap) + 20 4 byte[] String.value N/A +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== one Latin-1 string and one UTF-16 string, field by field +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x0017b7d8 + 12 4 int String.hash 0 + 16 1 byte String.coder 0 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 101, 108, 108, 111] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +java.lang.String object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x0017b7d8 + 12 4 int String.hash 0 + 16 1 byte String.coder 1 + 17 1 boolean String.hashIsZero false + 18 2 (alignment/padding gap) + 20 4 byte[] String.value [104, 0, -84, 32, 108, 0, 108, 0, 111, 0] +Instance size: 24 bytes +Space losses: 2 bytes internal + 0 bytes external = 2 bytes total + +=== the byte[] behind each +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1) + 8 4 (object header: class) 0x00178240 + 12 4 (array length) 5 + 16 5 byte [B. N/A + 21 3 (object alignment gap) +Instance size: 24 bytes +Space losses: 0 bytes internal + 3 bytes external = 3 bytes total + +[B object internals: +OFF SZ TYPE DESCRIPTION VALUE + 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0) + 8 4 (object header: class) 0x00178240 + 12 4 (array length) 10 + 16 10 byte [B. N/A + 26 6 (object alignment gap) +Instance size: 32 bytes +Space losses: 0 bytes internal + 6 bytes external = 6 bytes total + +=== footprint by length (String object + its byte[], via GraphLayout) +chars content coder byte[].length String byte[] total + 0 ASCII 0 0 24 16 40 + 0 U+00E9 0 0 24 16 40 + 0 U+20AC 0 0 24 16 40 + 1 ASCII 0 1 24 24 48 + 1 U+00E9 0 1 24 24 48 + 1 U+20AC 1 2 24 24 48 + 5 ASCII 0 5 24 24 48 + 5 U+00E9 0 5 24 24 48 + 5 U+20AC 1 10 24 32 56 + 10 ASCII 0 10 24 32 56 + 10 U+00E9 0 10 24 32 56 + 10 U+20AC 1 20 24 40 64 + 20 ASCII 0 20 24 40 64 + 20 U+00E9 0 20 24 40 64 + 20 U+20AC 1 40 24 56 80 + 40 ASCII 0 40 24 56 80 + 40 U+00E9 0 40 24 56 80 + 40 U+20AC 1 80 24 96 120 + 100 ASCII 0 100 24 120 144 + 100 U+00E9 0 100 24 120 144 + 100 U+20AC 1 200 24 216 240 + 1000 ASCII 0 1000 24 1016 1040 + 1000 U+00E9 0 1000 24 1016 1040 + 1000 U+20AC 1 2000 24 2016 2040 diff --git a/compact-strings/output/11-million-jdk25-default.txt b/compact-strings/output/11-million-jdk25-default.txt new file mode 100644 index 0000000..8dff2b6 --- /dev/null +++ b/compact-strings/output/11-million-jdk25-default.txt @@ -0,0 +1,8 @@ +$ java -Xmx3g MillionStrings (JDK 25) +java.version = 25.0.4.1 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 1,000,000 +coder == 1 (UTF16) in set B : 1,000,000 +A: lower-case ASCII only : 89,844,680 bytes (89.8 MB, 89.8 bytes per string) +B: same + one euro sign : 133,839,824 bytes (133.8 MB, 133.8 bytes per string) +B / A : 1.490 diff --git a/compact-strings/output/12-million-jdk25-no-compact-strings.txt b/compact-strings/output/12-million-jdk25-no-compact-strings.txt new file mode 100644 index 0000000..79ad32e --- /dev/null +++ b/compact-strings/output/12-million-jdk25-no-compact-strings.txt @@ -0,0 +1,8 @@ +$ java -Xmx3g -XX:-CompactStrings MillionStrings (JDK 25) +java.version = 25.0.4.1 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 0 +coder == 1 (UTF16) in set B : 1,000,000 +A: lower-case ASCII only : 131,995,184 bytes (132.0 MB, 132.0 bytes per string) +B: same + one euro sign : 133,839,824 bytes (133.8 MB, 133.8 bytes per string) +B / A : 1.014 diff --git a/compact-strings/output/13-million-jdk25-compact-headers.txt b/compact-strings/output/13-million-jdk25-compact-headers.txt new file mode 100644 index 0000000..bebacf6 --- /dev/null +++ b/compact-strings/output/13-million-jdk25-compact-headers.txt @@ -0,0 +1,8 @@ +$ java -Xmx3g -XX:+UseCompactObjectHeaders MillionStrings (JDK 25) +java.version = 25.0.4.1 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 1,000,000 +coder == 1 (UTF16) in set B : 1,000,000 +A: lower-case ASCII only : 86,150,520 bytes (86.2 MB, 86.2 bytes per string) +B: same + one euro sign : 130,145,168 bytes (130.1 MB, 130.1 bytes per string) +B / A : 1.511 diff --git a/compact-strings/output/14-million-jdk25-compact-headers-no-compact-strings.txt b/compact-strings/output/14-million-jdk25-compact-headers-no-compact-strings.txt new file mode 100644 index 0000000..8b84131 --- /dev/null +++ b/compact-strings/output/14-million-jdk25-compact-headers-no-compact-strings.txt @@ -0,0 +1,8 @@ +$ java -Xmx3g -XX:+UseCompactObjectHeaders -XX:-CompactStrings MillionStrings (JDK 25) +java.version = 25.0.4.1 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 0 +coder == 1 (UTF16) in set B : 1,000,000 +A: lower-case ASCII only : 127,997,304 bytes (128.0 MB, 128.0 bytes per string) +B: same + one euro sign : 130,145,168 bytes (130.1 MB, 130.1 bytes per string) +B / A : 1.017 diff --git a/compact-strings/output/15-million-jdk27-default.txt b/compact-strings/output/15-million-jdk27-default.txt new file mode 100644 index 0000000..7846c3b --- /dev/null +++ b/compact-strings/output/15-million-jdk27-default.txt @@ -0,0 +1,9 @@ +$ java -Xmx3g MillionStrings (JDK 27) +java.version = 27 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 1,000,000 +coder == 1 (UTF16) in set B : 1,000,000 +# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.runtime.VM.isCompressedKlassPointersEnabled() +A: lower-case ASCII only : 86,150,520 bytes (86.2 MB, 86.2 bytes per string) +B: same + one euro sign : 130,145,168 bytes (130.1 MB, 130.1 bytes per string) +B / A : 1.511 diff --git a/compact-strings/output/16-million-jdk27-headers-off.txt b/compact-strings/output/16-million-jdk27-headers-off.txt new file mode 100644 index 0000000..b2fe9b6 --- /dev/null +++ b/compact-strings/output/16-million-jdk27-headers-off.txt @@ -0,0 +1,9 @@ +$ java -Xmx3g -XX:-UseCompactObjectHeaders MillionStrings (JDK 27) +java.version = 27 +strings : 1,000,000 (average 42.50 chars) +coder == 0 (LATIN1) in set A : 1,000,000 +coder == 1 (UTF16) in set B : 1,000,000 +# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.runtime.VM.isCompressedKlassPointersEnabled() +A: lower-case ASCII only : 89,844,680 bytes (89.8 MB, 89.8 bytes per string) +B: same + one euro sign : 133,839,824 bytes (133.8 MB, 133.8 bytes per string) +B / A : 1.490 diff --git a/compact-strings/output/20-upgrade.txt b/compact-strings/output/20-upgrade.txt new file mode 100644 index 0000000..d0b62f2 --- /dev/null +++ b/compact-strings/output/20-upgrade.txt @@ -0,0 +1,39 @@ +$ java Upgrade (JDK 25) +java.version = 25.0.4.1 +"hello" (literal) length=5 coder=0 byte[].length=5 +"h\u00E9llo" (e-acute, Latin-1 supplement) length=5 coder=0 byte[].length=5 +"h\u20ACllo" (euro sign) length=5 coder=1 byte[].length=10 +"hello" + "\u20AC" (concat) length=6 coder=1 byte[].length=12 +wide = "\u20ACabcdef" length=7 coder=1 byte[].length=14 +wide.substring(1) (all Latin-1 again) length=6 coder=0 byte[].length=6 +wide.replace('\u20AC', 'E') length=7 coder=0 byte[].length=7 +wide.toLowerCase() length=7 coder=1 byte[].length=14 +new String("caf\u00E9".getBytes(UTF_8), UTF_8) length=4 coder=0 byte[].length=4 +new String(utf8 bytes of ASCII, UTF_8) length=5 coder=0 byte[].length=5 +new String(char[] {'a','b'}) length=2 coder=0 byte[].length=2 +new String(char[] {'a','\u4E2D'}) length=2 coder=1 byte[].length=4 +"\uD83D\uDE00" (emoji, a surrogate pair) length=2 coder=1 byte[].length=4 +StringBuilder("ascii only").toString() length=10 coder=0 byte[].length=10 +... after append('\u20AC'), toString() length=11 coder=1 byte[].length=22 +... after setLength(0) + ASCII, toString() length=13 coder=0 byte[].length=13 +length() of the emoji: 2 (UTF-16 code units, not code points) + +$ java -XX:-CompactStrings Upgrade (JDK 25) +java.version = 25.0.4.1 +"hello" (literal) length=5 coder=1 byte[].length=10 +"h\u00E9llo" (e-acute, Latin-1 supplement) length=5 coder=1 byte[].length=10 +"h\u20ACllo" (euro sign) length=5 coder=1 byte[].length=10 +"hello" + "\u20AC" (concat) length=6 coder=1 byte[].length=12 +wide = "\u20ACabcdef" length=7 coder=1 byte[].length=14 +wide.substring(1) (all Latin-1 again) length=6 coder=1 byte[].length=12 +wide.replace('\u20AC', 'E') length=7 coder=1 byte[].length=14 +wide.toLowerCase() length=7 coder=1 byte[].length=14 +new String("caf\u00E9".getBytes(UTF_8), UTF_8) length=4 coder=1 byte[].length=8 +new String(utf8 bytes of ASCII, UTF_8) length=5 coder=1 byte[].length=10 +new String(char[] {'a','b'}) length=2 coder=1 byte[].length=4 +new String(char[] {'a','\u4E2D'}) length=2 coder=1 byte[].length=4 +"\uD83D\uDE00" (emoji, a surrogate pair) length=2 coder=1 byte[].length=4 +StringBuilder("ascii only").toString() length=10 coder=1 byte[].length=20 +... after append('\u20AC'), toString() length=11 coder=1 byte[].length=22 +... after setLength(0) + ASCII, toString() length=13 coder=1 byte[].length=26 +length() of the emoji: 2 (UTF-16 code units, not code points) diff --git a/compact-strings/output/21-flags.txt b/compact-strings/output/21-flags.txt new file mode 100644 index 0000000..e7a82df --- /dev/null +++ b/compact-strings/output/21-flags.txt @@ -0,0 +1,18 @@ +$ java -XX:+PrintFlagsFinal -version | grep -E 'CompactStrings|CompactObjectHeaders' (JDK 25) + bool CompactStrings = true {pd product} {default} + bool UseCompactObjectHeaders = false {product lp64_product} {default} + +$ java -XX:-CompactStrings -version (JDK 25) +openjdk version "25.0.4.1" 2026-08-18 LTS +OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS) +OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode) + +$ java -XX:+PrintFlagsFinal -version | grep -E 'CompactStrings|CompactObjectHeaders' (JDK 27) + bool CompactStrings = true {pd product} {default} + bool UseCompactObjectHeaders = true {product lp64_product} {default} + +$ java -XX:-CompactStrings -version (JDK 27) +openjdk version "27" 2026-09-15 +OpenJDK Runtime Environment Corretto-27.0.0.35.1 (build 27+35-FR) +OpenJDK 64-Bit Server VM Corretto-27.0.0.35.1 (build 27+35-FR, mixed mode) + diff --git a/compact-strings/output/30-jmh.txt b/compact-strings/output/30-jmh.txt new file mode 100644 index 0000000..1a0fde4 --- /dev/null +++ b/compact-strings/output/30-jmh.txt @@ -0,0 +1,19 @@ +machine: 2 CPUs, Intel(R) Xeon(R) Processor @ 2.10GHz + +$ java org.openjdk.jmh.Main -f 2 -wi 3 -w 1 -i 5 -r 1 (25.0.4.1+1-LTS, JMH 1.37) +Benchmark Mode Cnt Score Error Units +StringBench.compareToLastCharDiffers avgt 10 13.515 ± 0.096 ns/op +StringBench.concatTwo avgt 10 75.682 ± 1.184 ns/op +StringBench.equalsSameContent avgt 10 8.563 ± 0.179 ns/op +StringBench.indexOfAbsentString avgt 10 36.571 ± 0.238 ns/op +StringBench.indexOfLastChar avgt 10 2.872 ± 0.092 ns/op +StringBench.toUpper avgt 10 276.137 ± 11.187 ns/op + +$ java -XX:-CompactStrings org.openjdk.jmh.Main -f 2 -wi 3 -w 1 -i 5 -r 1 (25.0.4.1+1-LTS, JMH 1.37) +Benchmark Mode Cnt Score Error Units +StringBench.compareToLastCharDiffers avgt 10 16.630 ± 1.244 ns/op +StringBench.concatTwo avgt 10 142.990 ± 2.788 ns/op +StringBench.equalsSameContent avgt 10 14.370 ± 0.857 ns/op +StringBench.indexOfAbsentString avgt 10 71.265 ± 1.693 ns/op +StringBench.indexOfLastChar avgt 10 3.677 ± 0.308 ns/op +StringBench.toUpper avgt 10 558.868 ± 21.992 ns/op diff --git a/compact-strings/run.sh b/compact-strings/run.sh new file mode 100755 index 0000000..4546634 --- /dev/null +++ b/compact-strings/run.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Compact Strings measured with JOL: String layout, a million strings, when a string stays compact, flag status. +# Regenerates every file in output/ except 07-jmh.txt (see jmh/). +# JDK25=/path JDK27=/path ./run.sh +# JDK 27 is Amazon Corretto 27+35 (https://corretto.aws/downloads/latest/amazon-corretto-27-x64-linux-jdk.tar.gz); JDK 25 is Temurin 25.0.4.1+1. +set -uo pipefail +unset JAVA_TOOL_OPTIONS +HERE="$(cd "$(dirname "$0")" && pwd)"; cd "$HERE" +JDK25="${JDK25:-/opt/jdks/jdk-25.0.4.1+1}"; JDK27="${JDK27:-/opt/jdks/jdk27}" +OUT="$HERE/output"; mkdir -p "$OUT" .lib +JOL=.lib/jol-core-0.17.jar +if [ ! -s "$JOL" ]; then + for try in 1 2 3 4 5; do curl -sfL -o "$JOL" https://repo1.maven.org/maven2/org/openjdk/jol/jol-core/0.17/jol-core-0.17.jar && break; sleep 5; done +fi +echo "4c98e9e61b3f189241057cb21b4331d1093e5b85 $JOL" | sha1sum -c - >/dev/null || { echo "JOL sha1 mismatch"; exit 1; } # Maven Central's own .sha1 +B="$(mktemp -d)"; trap 'rm -rf "$B"' EXIT +"$JDK25/bin/javac" --release 25 -cp "$JOL" -d "$B" src/*.java || exit 1 + +# JOL attaches to itself for Instrumentation and uses sun.misc.Unsafe (warned about on 24+); String's fields need --add-opens. +COMMON=(-Djdk.attach.allowAttachSelf=true -XX:+EnableDynamicAgentLoading --sun-misc-unsafe-memory-access=allow + --add-opens java.base/java.lang=ALL-UNNAMED -cp "$B:$JOL") +run() { local jdk="$1" cls="$2"; shift 2; "$jdk/bin/java" "${COMMON[@]}" "$@" "$cls" 2>&1 | grep -v 'Picked up'; } + +# 01 - the layout of one String and a table by length, under six flag sets +i=0 +for cfg in "25|default|jdk25-default" "25|-XX:-CompactStrings|jdk25-no-compact-strings" "25|-XX:+UseCompactObjectHeaders|jdk25-compact-headers" "25|-XX:+UseCompactObjectHeaders -XX:-CompactStrings|jdk25-compact-headers-no-compact-strings" "27|default|jdk27-default" "27|-XX:-UseCompactObjectHeaders|jdk27-headers-off"; do + v="${cfg%%|*}"; rest="${cfg#*|}"; flags="${rest%%|*}"; tag="${rest#*|}"; jdk="$JDK25"; [ "$v" = 27 ] && jdk="$JDK27" + [ "$flags" = default ] && fl=() || read -ra fl <<< "$flags" + i=$((i+1)); f="$OUT/$(printf %02d $i)-layout-$tag.txt" + { echo "\$ java ${fl[*]:-} StringLayout (JDK $v)"; run "$jdk" StringLayout "${fl[@]}"; } > "$f" +done + +# 02 - a million strings, same six flag sets +i=10 +for cfg in "25|default|jdk25-default" "25|-XX:-CompactStrings|jdk25-no-compact-strings" "25|-XX:+UseCompactObjectHeaders|jdk25-compact-headers" "25|-XX:+UseCompactObjectHeaders -XX:-CompactStrings|jdk25-compact-headers-no-compact-strings" "27|default|jdk27-default" "27|-XX:-UseCompactObjectHeaders|jdk27-headers-off"; do + v="${cfg%%|*}"; rest="${cfg#*|}"; flags="${rest%%|*}"; tag="${rest#*|}"; jdk="$JDK25"; [ "$v" = 27 ] && jdk="$JDK27" + [ "$flags" = default ] && fl=() || read -ra fl <<< "$flags" + i=$((i+1)); f="$OUT/$(printf %02d $i)-million-$tag.txt" + { echo "\$ java -Xmx3g ${fl[*]:-} MillionStrings (JDK $v)"; run "$jdk" MillionStrings -Xmx3g "${fl[@]}"; } > "$f" +done + +# 03 - when does a string stay compact (JDK 25, default; and the same program with -XX:-CompactStrings) +{ echo "\$ java Upgrade (JDK 25)"; run "$JDK25" Upgrade; echo; echo "\$ java -XX:-CompactStrings Upgrade (JDK 25)"; run "$JDK25" Upgrade -XX:-CompactStrings; } > "$OUT/20-upgrade.txt" + +# 04 - is the flag diagnostic? (it is not: a product flag) +{ for v in 25 27; do jdk="$JDK25"; [ "$v" = 27 ] && jdk="$JDK27" + echo "\$ java -XX:+PrintFlagsFinal -version | grep -E 'CompactStrings|CompactObjectHeaders' (JDK $v)" + "$jdk/bin/java" -XX:+PrintFlagsFinal -version 2>&1 | grep -E 'CompactStrings|CompactObjectHeaders'; echo + echo "\$ java -XX:-CompactStrings -version (JDK $v)"; "$jdk/bin/java" -XX:-CompactStrings -version 2>&1 | grep -v 'Picked up'; echo; done; } > "$OUT/21-flags.txt" +echo done diff --git a/compact-strings/src/MillionStrings.java b/compact-strings/src/MillionStrings.java new file mode 100644 index 0000000..43e53b4 --- /dev/null +++ b/compact-strings/src/MillionStrings.java @@ -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()); + } +} diff --git a/compact-strings/src/StringLayout.java b/compact-strings/src/StringLayout.java new file mode 100644 index 0000000..5bd9c33 --- /dev/null +++ b/compact-strings/src/StringLayout.java @@ -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); + } + } + } +} diff --git a/compact-strings/src/Upgrade.java b/compact-strings/src/Upgrade.java new file mode 100644 index 0000000..ba0496b --- /dev/null +++ b/compact-strings/src/Upgrade.java @@ -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)"); + } +}