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
+1
View File
@@ -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/`)
+1
View File
@@ -0,0 +1 @@
.lib/
+24
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
lib/
+15
View File
@@ -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
+17
View File
@@ -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"
@@ -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(); }
}
@@ -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.<elements> 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.<elements> 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
@@ -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.<elements> 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.<elements> 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
@@ -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.<elements> 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.<elements> 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
@@ -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.<elements> 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.<elements> 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
@@ -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.<elements> 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.<elements> 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
@@ -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.<elements> 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.<elements> 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+39
View File
@@ -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)
+18
View File
@@ -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)
+19
View File
@@ -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
+50
View File
@@ -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
+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)");
}
}