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:
@@ -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/`)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.lib/
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
lib/
|
||||
Executable
+15
@@ -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
|
||||
Executable
+17
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Executable
+50
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user