Add jdk27-memory: compact object headers and G1-everywhere on a Spring Boot service
Spring Boot 4.1.1 catalog service measured on JDK 25, 26 and 27: live heap 320 MB to 273 MB with compact headers, per-class bytes per instance, container-sizing sweeps with real cgroup limits, 1-CPU Serial vs G1 with Native Memory Tracking, removed flags (MaxRAM, UseCompressedClassPointers), and a kernel OOM-kill check. Outputs in jdk27-memory/output. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
@@ -71,6 +71,7 @@ the part most "ZGC is faster" posts leave out.
|
||||
| [`internals/PageSizeDemo.java`](internals/src/com/ankurm/zgc/internals/PageSizeDemo.java) | ZGC page classes vs G1 humongous, read from the live VM | none |
|
||||
| [`analysis/GcLogParser.java`](analysis/src/com/ankurm/zgc/analysis/GcLogParser.java) | Unified-log parser that keeps pauses and concurrent phases apart | none |
|
||||
| [`jmh/`](jmh/src/main/java/com/ankurm/zgc/jmh) | Throughput and barrier microbenchmarks | JMH 1.37 |
|
||||
| [`jdk27-memory/`](jdk27-memory) | Spring Boot 4.1 service measured on JDK 25, 26 and 27: compact object headers (JEP 534) and G1 everywhere (JEP 523), heap footprint, container sizing with real cgroup limits. Its README and `output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/) | Spring Boot 4.1.1, three JDKs |
|
||||
|
||||
### JMH benchmarks
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -0,0 +1,80 @@
|
||||
# jdk27-memory: compact object headers and G1-everywhere, measured on a Spring Boot service
|
||||
|
||||
Companion module for **[JDK 27 Memory Changes: Compact Object Headers by Default and G1 Everywhere (JEP 534 + 523)](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/)**
|
||||
on [ankurm.com](https://ankurm.com). Everything the post quotes is a file in [`output/`](output), regenerated by [`run.sh`](run.sh).
|
||||
The explanation lives in the post; this repository holds the code and the raw transcripts.
|
||||
|
||||
The module sits inside the `zgc-jdk25-benchmarks` repository because it answers the follow-up question to the
|
||||
[G1 vs Generational ZGC](https://ankurm.com/generational-zgc-jdk-25-vs-g1/) work: what did the default collector
|
||||
and the object header do to the same kind of heap on JDK 27.
|
||||
|
||||
## What is here
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| [`src/main/java/com/ankurm/memory/`](src/main/java/com/ankurm/memory) | A Spring Boot 4.1.1 service holding a 2,000,000-product catalog in a `Map<Long, Product>`, with `/load`, `/memory`, `/lookups`, `/products/{id}` and `/ping` |
|
||||
| [`src/offsets/Offsets.java`](src/offsets/Offsets.java) | Prints where the first field of an object starts (JDK 25/26: byte 12; JDK 27: byte 8) |
|
||||
| [`scripts/footprint.sh`](scripts/footprint.sh) | The same jar on JDK 25, 26 and 27, compact headers on and off, N runs each |
|
||||
| [`scripts/classes.sh`](scripts/classes.sh) | Bytes per instance of the classes that dominate the heap, from `jcmd GC.class_histogram` |
|
||||
| [`scripts/flags.sh`](scripts/flags.sh) | What each JDK chooses by default (collector, header mode, heap) and which flags still work |
|
||||
| [`scripts/container-sizing.sh`](scripts/container-sizing.sh) | The smallest container that can hold the catalog, before and after, using real cgroup limits |
|
||||
| [`scripts/one-cpu.sh`](scripts/one-cpu.sh) | A 1 GB, 1-CPU container: Serial (JDK 26) versus G1 (JDK 27), with Native Memory Tracking |
|
||||
| [`scripts/oom-check.sh`](scripts/oom-check.sh) | Runs the edge case of the sweep four times and reads the cgroup's counters and the kernel log, to show that "killed" is the kernel's memory controller |
|
||||
| [`scripts/sample.sh`](scripts/sample.sh) | A transcript of the service's own endpoints |
|
||||
| [`scripts/lib.sh`](scripts/lib.sh), [`scripts/env.sh`](scripts/env.sh) | Shared start/load/measure logic and JDK locations |
|
||||
|
||||
## Output
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| [`output/00-sample-endpoints.txt`](output/00-sample-endpoints.txt) | The endpoints on JDK 27: idle, loaded, one lookup, one 404 |
|
||||
| [`output/01-jvm-defaults.txt`](output/01-jvm-defaults.txt) | Default collector and header mode per JDK, in containers of 1 CPU and 2 CPUs, and the flags that no longer work |
|
||||
| [`output/02-footprint.txt`](output/02-footprint.txt) | Every run of the six-configuration matrix, medians, and the runs grouped by whether a collection happened during the load |
|
||||
| [`output/03-classes.txt`](output/03-classes.txt) | Per-class bytes per instance, JDK 26 vs JDK 27 vs JDK 27 opted out |
|
||||
| [`output/04-offsets.txt`](output/04-offsets.txt) | Field offsets on JDK 25, 26, 27 and 27 opted out |
|
||||
| [`output/05-container-sizing.txt`](output/05-container-sizing.txt) | Pass/fail per memory limit, with the failure kind (Java `OutOfMemoryError`, kernel kill, or no progress) |
|
||||
| [`output/06-one-cpu.txt`](output/06-one-cpu.txt) | Serial vs G1 in a 1 GB, 1-CPU container: footprint, GC time, and the collector's native memory |
|
||||
| [`output/07-oom-check.txt`](output/07-oom-check.txt) | Four runs at the 400 MB edge: HTTP status, exit code, `memory.failcnt`, `oom_kill`, and the kernel log line for each killed process |
|
||||
|
||||
## Headline result
|
||||
|
||||
Two million products, `-Xms1g -Xmx1g`, G1, five runs per configuration
|
||||
([`output/02-footprint.txt`](output/02-footprint.txt)):
|
||||
|
||||
| Configuration | Live heap after loading | Idle live heap |
|
||||
|---|---|---|
|
||||
| JDK 25 default | 320 MB | 12 MB |
|
||||
| JDK 26 default | 320 MB | 13 MB |
|
||||
| JDK 27 `-XX:-UseCompactObjectHeaders` | 320 MB | 13 MB |
|
||||
| JDK 25 / 26 `-XX:+UseCompactObjectHeaders` | 273 MB | 11 MB |
|
||||
| **JDK 27 default** | **273 MB** | **12 MB** |
|
||||
|
||||
The saving is the header, not the JDK version: JDK 25 and 26 reach the same 273 MB with the flag.
|
||||
[`output/03-classes.txt`](output/03-classes.txt) shows where it comes from: `Product` 40 to 32 bytes,
|
||||
`HashMap$Node` 32 to 24, `Long` 24 to 16, while `String` (24) and the `byte[]` behind it (about 32.6) do not move.
|
||||
|
||||
Resident memory and lookup time did **not** follow the heap cleanly, and the reason is in the data rather than
|
||||
in the headers: they depend on whether a young collection happened during the load. The last block of
|
||||
`02-footprint.txt` groups the runs to show it.
|
||||
|
||||
## Running it
|
||||
|
||||
Needs three JDKs (25, 26, 27), Maven and `curl`. The container runs additionally need root and a writable
|
||||
cgroup v1 hierarchy: the sandbox this was written in has no Docker daemon, so `scripts/lib.sh` creates cgroups
|
||||
with a memory limit and CPU quota by hand, which is what Docker does and what the JVM reads. On a machine with
|
||||
Docker you can reproduce the same effect with `docker run --memory=... --cpus=...`.
|
||||
|
||||
```console
|
||||
cd jdk27-memory
|
||||
JDK25=/path/to/jdk-25 JDK26=/path/to/jdk-26 JDK27=/path/to/jdk-27 ./run.sh # about 45 minutes
|
||||
FAST=1 ./run.sh # one run per configuration
|
||||
```
|
||||
|
||||
Each script also runs on its own, for example `bash scripts/flags.sh` or `bash scripts/footprint.sh 2000000 5`.
|
||||
|
||||
## Caveats
|
||||
|
||||
* One 2-CPU virtual machine. Timings are noisy; the live-heap figures are not (they varied by at most 1 MB across runs).
|
||||
* Compiled for Java 25 and run on 25, 26 and 27, so the jar is identical across runs.
|
||||
* `openjdk.org/jeps` returned HTTP 403 to the tooling used, so JEP claims come from running the JDKs, not from the JEP text.
|
||||
* No third-party agent, profiler or serialisation library was tested against compact headers.
|
||||
@@ -0,0 +1,41 @@
|
||||
$ curl -s -w ' [HTTP %{http_code}]\n' localhost:18081/memory
|
||||
java.version=27
|
||||
UseCompactObjectHeaders=true
|
||||
collectors=[G1 Young Generation, G1 Concurrent GC, G1 Old Generation]
|
||||
availableProcessors=2
|
||||
maxHeapMB=1024
|
||||
products=0
|
||||
heapLiveAfterGcMB=12
|
||||
heapCommittedMB=1024
|
||||
rssMB=238
|
||||
gcCountTotal=7
|
||||
gcMillisTotal=121
|
||||
[HTTP 200]
|
||||
|
||||
$ curl -s -w ' [HTTP %{http_code}]\n' localhost:18081/load?n=2000000
|
||||
loaded=2000000
|
||||
loadMillis=237
|
||||
gcCountDuringLoad=0
|
||||
gcMillisDuringLoad=0
|
||||
[HTTP 200]
|
||||
|
||||
$ curl -s -w ' [HTTP %{http_code}]\n' localhost:18081/memory
|
||||
java.version=27
|
||||
UseCompactObjectHeaders=true
|
||||
collectors=[G1 Young Generation, G1 Concurrent GC, G1 Old Generation]
|
||||
availableProcessors=2
|
||||
maxHeapMB=1024
|
||||
products=2000000
|
||||
heapLiveAfterGcMB=272
|
||||
heapCommittedMB=1024
|
||||
rssMB=417
|
||||
gcCountTotal=9
|
||||
gcMillisTotal=626
|
||||
[HTTP 200]
|
||||
|
||||
$ curl -s -w ' [HTTP %{http_code}]\n' localhost:18081/products/42
|
||||
{"id":42,"priceCents":142,"stock":42,"active":true,"sku":"SKU-100000042"} [HTTP 200]
|
||||
|
||||
$ curl -s -w ' [HTTP %{http_code}]\n' localhost:18081/products/2000000
|
||||
{"timestamp":"2026-09-24T14:57:54.671Z","status":404,"error":"Not Found","path":"/products/2000000"} [HTTP 404]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
== JDK 25: OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||
default (this machine: 2 CPUs) -> compact=false collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:+UseCompactObjectHeaders -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:-UseCompactObjectHeaders -> compact=false collector=UseG1GC MaxHeapSize=1498MB
|
||||
|
||||
== JDK 26: OpenJDK Runtime Environment Temurin-26.0.2.1+1 (build 26.0.2.1+1)
|
||||
default (this machine: 2 CPUs) -> compact=false collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:+UseCompactObjectHeaders -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:-UseCompactObjectHeaders -> compact=false collector=UseG1GC MaxHeapSize=1498MB
|
||||
|
||||
== JDK 27: OpenJDK Runtime Environment Temurin-27+35 (build 27+35)
|
||||
default (this machine: 2 CPUs) -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:+UseCompactObjectHeaders -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
-XX:-UseCompactObjectHeaders -> compact=false collector=UseG1GC MaxHeapSize=1498MB
|
||||
|
||||
== The same JDKs in a container: 1 GB memory limit, 1 CPU (a cgroup, set the way Docker sets it)
|
||||
JDK 25, 1 GB, 1 CPU -> compact=false collector=UseSerialGC MaxHeapSize=256MB
|
||||
JDK 25, 1 GB, 1 CPU, MaxRAMPercentage=75 -> compact=false collector=UseSerialGC MaxHeapSize=768MB
|
||||
JDK 26, 1 GB, 1 CPU -> compact=false collector=UseSerialGC MaxHeapSize=256MB
|
||||
JDK 26, 1 GB, 1 CPU, MaxRAMPercentage=75 -> compact=false collector=UseSerialGC MaxHeapSize=768MB
|
||||
JDK 27, 1 GB, 1 CPU -> compact=true collector=UseG1GC MaxHeapSize=256MB
|
||||
JDK 27, 1 GB, 1 CPU, MaxRAMPercentage=75 -> compact=true collector=UseG1GC MaxHeapSize=768MB
|
||||
|
||||
== 1 GB, 2 CPUs
|
||||
JDK 26, 1 GB, 2 CPUs -> compact=false collector=UseSerialGC MaxHeapSize=256MB
|
||||
JDK 27, 1 GB, 2 CPUs -> compact=true collector=UseG1GC MaxHeapSize=256MB
|
||||
|
||||
== -XX:MaxRAM: the flag many people used to fake a container limit
|
||||
JDK 26 -XX:MaxRAM=1g -> compact=false collector=UseG1GC MaxHeapSize=256MB
|
||||
JVM says: OpenJDK 64-Bit Server VM warning: Option MaxRAM was deprecated in version 26.0 and will likely be removed in a future release.
|
||||
JDK 27 -XX:MaxRAM=1g -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
JVM says: OpenJDK 64-Bit Server VM warning: Ignoring option MaxRAM; support was removed in 27.0
|
||||
|
||||
== Compact headers with each collector on JDK 27
|
||||
JDK 27 -XX:+UseG1GC -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
JDK 27 -XX:+UseParallelGC -> compact=true collector=UseParallelGC MaxHeapSize=1498MB
|
||||
JDK 27 -XX:+UseSerialGC -> compact=true collector=UseSerialGC MaxHeapSize=1498MB
|
||||
JDK 27 -XX:+UseZGC -> compact=true collector=UseZGC MaxHeapSize=1498MB
|
||||
|
||||
== Compressed pointers on JDK 27
|
||||
JDK 27 -XX:-UseCompressedClassPointers -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
JVM says: OpenJDK 64-Bit Server VM warning: Ignoring option UseCompressedClassPointers; support was removed in 27.0
|
||||
JDK 27 -XX:-UseCompressedOops -> compact=true collector=UseG1GC MaxHeapSize=1498MB
|
||||
JDK 27 -Xmx32g (a large explicit heap) -> compact=true collector=UseG1GC MaxHeapSize=32768MB
|
||||
@@ -0,0 +1,52 @@
|
||||
# 2026-09-24 2000000 products, -Xms1g -Xmx1g, G1, 5 runs per configuration; MED lines are medians
|
||||
# JDK25=OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
|
||||
# JDK26=OpenJDK Runtime Environment Temurin-26.0.2.1+1 (build 26.0.2.1+1)
|
||||
# JDK27=OpenJDK Runtime Environment Temurin-27+35 (build 27+35)
|
||||
run jdk25-default/1 idle=13MB live=321MB rss=462MB gcDuringLoad=0 (0ms) lookup=246ns compact=false
|
||||
run jdk25-default/2 idle=12MB live=320MB rss=461MB gcDuringLoad=0 (0ms) lookup=310ns compact=false
|
||||
run jdk25-default/3 idle=13MB live=320MB rss=696MB gcDuringLoad=1 (213ms) lookup=187ns compact=false
|
||||
run jdk25-default/4 idle=12MB live=321MB rss=462MB gcDuringLoad=0 (0ms) lookup=244ns compact=false
|
||||
run jdk25-default/5 idle=12MB live=320MB rss=459MB gcDuringLoad=0 (0ms) lookup=282ns compact=false
|
||||
MED jdk25-default idle=12MB live=320MB rss=462MB gcDuringLoad=0 gcMs=0 lookupNs=246
|
||||
run jdk25-compact/1 idle=12MB live=273MB rss=416MB gcDuringLoad=0 (0ms) lookup=256ns compact=true
|
||||
run jdk25-compact/2 idle=11MB live=273MB rss=415MB gcDuringLoad=0 (0ms) lookup=245ns compact=true
|
||||
run jdk25-compact/3 idle=11MB live=273MB rss=415MB gcDuringLoad=0 (0ms) lookup=265ns compact=true
|
||||
run jdk25-compact/4 idle=11MB live=273MB rss=667MB gcDuringLoad=1 (236ms) lookup=158ns compact=true
|
||||
run jdk25-compact/5 idle=11MB live=272MB rss=413MB gcDuringLoad=0 (0ms) lookup=256ns compact=true
|
||||
MED jdk25-compact idle=11MB live=273MB rss=415MB gcDuringLoad=0 gcMs=0 lookupNs=256
|
||||
run jdk26-default/1 idle=13MB live=320MB rss=665MB gcDuringLoad=1 (203ms) lookup=202ns compact=false
|
||||
run jdk26-default/2 idle=13MB live=320MB rss=693MB gcDuringLoad=1 (216ms) lookup=199ns compact=false
|
||||
run jdk26-default/3 idle=13MB live=320MB rss=701MB gcDuringLoad=1 (206ms) lookup=181ns compact=false
|
||||
run jdk26-default/4 idle=13MB live=320MB rss=647MB gcDuringLoad=1 (208ms) lookup=210ns compact=false
|
||||
run jdk26-default/5 idle=13MB live=320MB rss=697MB gcDuringLoad=1 (198ms) lookup=188ns compact=false
|
||||
MED jdk26-default idle=13MB live=320MB rss=693MB gcDuringLoad=1 gcMs=206 lookupNs=199
|
||||
run jdk26-compact/1 idle=12MB live=274MB rss=419MB gcDuringLoad=0 (0ms) lookup=250ns compact=true
|
||||
run jdk26-compact/2 idle=11MB live=273MB rss=645MB gcDuringLoad=1 (209ms) lookup=176ns compact=true
|
||||
run jdk26-compact/3 idle=11MB live=273MB rss=416MB gcDuringLoad=0 (0ms) lookup=271ns compact=true
|
||||
run jdk26-compact/4 idle=11MB live=273MB rss=418MB gcDuringLoad=0 (0ms) lookup=256ns compact=true
|
||||
run jdk26-compact/5 idle=11MB live=273MB rss=615MB gcDuringLoad=1 (207ms) lookup=237ns compact=true
|
||||
MED jdk26-compact idle=11MB live=273MB rss=419MB gcDuringLoad=0 gcMs=0 lookupNs=250
|
||||
run jdk27-default/1 idle=12MB live=273MB rss=413MB gcDuringLoad=0 (0ms) lookup=298ns compact=true
|
||||
run jdk27-default/2 idle=12MB live=272MB rss=412MB gcDuringLoad=0 (0ms) lookup=223ns compact=true
|
||||
run jdk27-default/3 idle=12MB live=273MB rss=413MB gcDuringLoad=0 (0ms) lookup=318ns compact=true
|
||||
run jdk27-default/4 idle=11MB live=273MB rss=594MB gcDuringLoad=1 (202ms) lookup=194ns compact=true
|
||||
run jdk27-default/5 idle=12MB live=273MB rss=409MB gcDuringLoad=0 (0ms) lookup=257ns compact=true
|
||||
MED jdk27-default idle=12MB live=273MB rss=413MB gcDuringLoad=0 gcMs=0 lookupNs=257
|
||||
run jdk27-optout/1 idle=13MB live=319MB rss=454MB gcDuringLoad=0 (0ms) lookup=267ns compact=false
|
||||
run jdk27-optout/2 idle=13MB live=319MB rss=461MB gcDuringLoad=0 (0ms) lookup=261ns compact=false
|
||||
run jdk27-optout/3 idle=13MB live=320MB rss=459MB gcDuringLoad=0 (0ms) lookup=256ns compact=false
|
||||
run jdk27-optout/4 idle=13MB live=320MB rss=616MB gcDuringLoad=1 (191ms) lookup=184ns compact=false
|
||||
run jdk27-optout/5 idle=13MB live=320MB rss=454MB gcDuringLoad=0 (0ms) lookup=260ns compact=false
|
||||
MED jdk27-optout idle=13MB live=320MB rss=459MB gcDuringLoad=0 gcMs=0 lookupNs=260
|
||||
|
||||
== medians of 5 runs per configuration (idle = live set of the idle app; live = after loading the catalog)
|
||||
MED jdk25-default idle=12MB live=320MB rss=462MB gcDuringLoad=0 gcMs=0 lookupNs=246
|
||||
MED jdk25-compact idle=11MB live=273MB rss=415MB gcDuringLoad=0 gcMs=0 lookupNs=256
|
||||
MED jdk26-default idle=13MB live=320MB rss=693MB gcDuringLoad=1 gcMs=206 lookupNs=199
|
||||
MED jdk26-compact idle=11MB live=273MB rss=419MB gcDuringLoad=0 gcMs=0 lookupNs=250
|
||||
MED jdk27-default idle=12MB live=273MB rss=413MB gcDuringLoad=0 gcMs=0 lookupNs=257
|
||||
MED jdk27-optout idle=13MB live=320MB rss=459MB gcDuringLoad=0 gcMs=0 lookupNs=260
|
||||
|
||||
== the same runs, grouped by whether a collection ran while the catalog was loading (all configurations together)
|
||||
no collection during load runs=19 rss MB: min 409 / median 418 / max 462 lookup ns: min 223 / median 257 / max 318
|
||||
a collection during load runs=11 rss MB: min 594 / median 665 / max 701 lookup ns: min 158 / median 188 / max 237
|
||||
@@ -0,0 +1,12 @@
|
||||
== bytes per instance (= #bytes / #instances) from jcmd GC.class_histogram after loading 2,000,000 products
|
||||
class JDK 26 default JDK 27 default JDK 27 opt-out
|
||||
com.ankurm.memory.Product 40.0 32.0 40.0
|
||||
java.util.HashMap$Node 32.0 24.0 32.0
|
||||
java.lang.Long 24.0 16.0 24.0
|
||||
java.lang.String 24.0 24.0 24.0
|
||||
[B 32.7 32.6 32.7
|
||||
|
||||
== total bytes of those five classes
|
||||
jdk26-default 309088088 bytes (294.8 MB)
|
||||
jdk27-default 260853264 bytes (248.8 MB)
|
||||
jdk27-optout 309272896 bytes (294.9 MB)
|
||||
@@ -0,0 +1,40 @@
|
||||
== JDK 25
|
||||
java 25.0.4.1 compact headers: false
|
||||
id offset 16
|
||||
priceCents offset 12
|
||||
stock offset 24
|
||||
active offset 28
|
||||
sku offset 32
|
||||
first field starts at byte 12 (so the header is at most 12 bytes)
|
||||
a hard-coded 12-byte header would be right here
|
||||
|
||||
== JDK 26
|
||||
java 26.0.2.1 compact headers: false
|
||||
id offset 16
|
||||
priceCents offset 12
|
||||
stock offset 24
|
||||
active offset 28
|
||||
sku offset 32
|
||||
first field starts at byte 12 (so the header is at most 12 bytes)
|
||||
a hard-coded 12-byte header would be right here
|
||||
|
||||
== JDK 27
|
||||
java 27 compact headers: true
|
||||
id offset 8
|
||||
priceCents offset 16
|
||||
stock offset 20
|
||||
active offset 24
|
||||
sku offset 28
|
||||
first field starts at byte 8 (so the header is at most 8 bytes)
|
||||
a hard-coded 12-byte header would be WRONG here
|
||||
|
||||
== JDK 27 -XX:-UseCompactObjectHeaders
|
||||
java 27 compact headers: false
|
||||
id offset 16
|
||||
priceCents offset 12
|
||||
stock offset 24
|
||||
active offset 28
|
||||
sku offset 32
|
||||
first field starts at byte 12 (so the header is at most 12 bytes)
|
||||
a hard-coded 12-byte header would be right here
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# 2000000 products; 2 CPUs; 3 runs per cell; rss is the median of the runs that finished
|
||||
== Heap set to 75% of the container (-XX:MaxRAMPercentage=75)
|
||||
limit heap JDK 26 default JDK 27 default JDK 27 opt-out
|
||||
400MB 300MB 0/3 ok [1x java-OOM 2x no-result-in-25s] 3/3 ok rss 408MB 0/3 ok [3x killed]
|
||||
416MB 312MB 0/3 ok [3x java-OOM] 3/3 ok rss 415MB 0/3 ok [3x java-OOM]
|
||||
432MB 324MB 0/3 ok [3x java-OOM] 3/3 ok rss 406MB 3/3 ok rss 450MB
|
||||
448MB 336MB 3/3 ok rss 454MB 3/3 ok rss 418MB 3/3 ok rss 458MB
|
||||
512MB 384MB 3/3 ok rss 468MB 3/3 ok rss 417MB 3/3 ok rss 464MB
|
||||
704MB 528MB 3/3 ok rss 527MB 3/3 ok rss 431MB 3/3 ok rss 465MB
|
||||
|
||||
== Heap left at the JVM default (25% of the container)
|
||||
limit heap JDK 26 default JDK 27 default JDK 27 opt-out
|
||||
1024MB 256MB 0/3 ok [3x java-OOM] 0/3 ok [3x java-OOM] 0/3 ok [3x java-OOM]
|
||||
1152MB 288MB 0/3 ok [3x java-OOM] 3/3 ok rss 410MB 0/3 ok [3x java-OOM]
|
||||
1216MB 304MB 0/3 ok [3x java-OOM] 3/3 ok rss 409MB 0/3 ok [3x java-OOM]
|
||||
1280MB 320MB 0/3 ok [3x java-OOM] 3/3 ok rss 408MB 3/3 ok rss 444MB
|
||||
1536MB 384MB 3/3 ok rss 465MB 3/3 ok rss 417MB 3/3 ok rss 463MB
|
||||
2048MB 512MB 3/3 ok rss 485MB 3/3 ok rss 435MB 3/3 ok rss 486MB
|
||||
|
||||
# runs that needed a startup retry (the JVM never came up, for a reason other than OOM or a kernel kill): 0
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2000000 products, container = 1 GB / 1 CPU (cgroup), -XX:MaxRAMPercentage=50 (512 MB heap), NMT summary on, medians of 3 runs
|
||||
== jdk26-default (the JVM chose)
|
||||
collectors=[Copy, MarkSweepCompact] cpus=1
|
||||
startup=4.808s live=320MB rss=529MB loadMillis=695 gcDuringLoad=9 (523ms)
|
||||
NMT: GC committed=1730 KB Total committed=597325 KB
|
||||
|
||||
== jdk26 -XX:+UseG1GC
|
||||
collectors=[G1 Young Generation, G1 Concurrent GC, G1 Old Generation] cpus=1
|
||||
startup=4.8s live=320MB rss=486MB loadMillis=909 gcDuringLoad=12 (517ms)
|
||||
NMT: GC committed=46627 KB Total committed=642767 KB
|
||||
|
||||
== jdk27-default (the JVM chose)
|
||||
collectors=[G1 Young Generation, G1 Concurrent GC, G1 Old Generation] cpus=1
|
||||
startup=4.8s live=272MB rss=436MB loadMillis=700 gcDuringLoad=8 (472ms)
|
||||
NMT: GC committed=43443 KB Total committed=493486 KB
|
||||
|
||||
== jdk27 -XX:+UseSerialGC
|
||||
collectors=[Copy, MarkSweepCompact] cpus=1
|
||||
startup=5.101s live=271MB rss=482MB loadMillis=692 gcDuringLoad=9 (495ms)
|
||||
NMT: GC committed=1730 KB Total committed=597439 KB
|
||||
|
||||
== jdk27 -XX:+UseSerialGC, compact off
|
||||
collectors=[Copy, MarkSweepCompact] cpus=1
|
||||
startup=4.706s live=318MB rss=530MB loadMillis=630 gcDuringLoad=9 (487ms)
|
||||
NMT: GC committed=1730 KB Total committed=597465 KB
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# JDK 27, -XX:-UseCompactObjectHeaders -XX:MaxRAMPercentage=75, container limit 400 MB, 2 CPUs; 2,000,000 products
|
||||
== attempt 1: /load answered HTTP 000; process gone, exit 137
|
||||
memory.failcnt=155 memory.max_usage=400MB limit=400MB oom_kill 1
|
||||
|
||||
== attempt 2: /load answered HTTP 500; process still alive
|
||||
memory.failcnt=0 memory.max_usage=397MB limit=400MB oom_kill 0
|
||||
|
||||
== attempt 3: /load answered HTTP 000; process gone, exit 137
|
||||
memory.failcnt=182 memory.max_usage=400MB limit=400MB oom_kill 1
|
||||
|
||||
== attempt 4: /load answered HTTP 000; process gone, exit 137
|
||||
memory.failcnt=101 memory.max_usage=400MB limit=400MB oom_kill 1
|
||||
|
||||
== the kernel log (dmesg): what it says about the processes that disappeared above
|
||||
Memory cgroup out of memory: Killed process 21173 (java) total-vm:2923028kB, anon-rss:407580kB, file-rss:27492kB, shmem-rss:0kB, UID:0 pgtables:1128kB oom_score_adj:0
|
||||
Memory cgroup out of memory: Killed process 21325 (java) total-vm:2920972kB, anon-rss:407492kB, file-rss:27304kB, shmem-rss:0kB, UID:0 pgtables:1128kB oom_score_adj:0
|
||||
Memory cgroup out of memory: Killed process 21400 (java) total-vm:2920972kB, anon-rss:407424kB, file-rss:27080kB, shmem-rss:0kB, UID:0 pgtables:1128kB oom_score_adj:0
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>jdk27-memory</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>jdk27-memory</name>
|
||||
<description>A Spring Boot catalog service used to measure heap footprint on JDK 25, 26 and 27</description>
|
||||
|
||||
<properties>
|
||||
<!-- Compiled once for 25 so the same jar runs on 25, 26 and 27: only the JVM changes between runs. -->
|
||||
<java.version>25</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>catalog</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in output/. Needs three JDKs and, for the container runs, root plus a writable
|
||||
# cgroup v1 hierarchy (the sandbox this was written in has no Docker daemon, so the limits are set by hand
|
||||
# the way Docker sets them). Takes about 45 minutes.
|
||||
# JDK25=/path JDK26=/path JDK27=/path ./run.sh
|
||||
# FAST=1 ./run.sh one run per configuration, for a quick look
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
ORIG_JTO="${JAVA_TOOL_OPTIONS:-}" # kept only for Maven, which may need a proxy or trust store
|
||||
source scripts/env.sh
|
||||
RUNS=5; CRUNS=3
|
||||
if [ -n "${FAST:-}" ]; then RUNS=1; CRUNS=1; fi
|
||||
|
||||
if [ ! -f target/catalog.jar ]; then
|
||||
JAVA_HOME="$JDK25" JAVA_TOOL_OPTIONS="$ORIG_JTO" mvn -q -B -DskipTests package
|
||||
fi
|
||||
mkdir -p output
|
||||
bash scripts/sample.sh > output/00-sample-endpoints.txt
|
||||
bash scripts/flags.sh > output/01-jvm-defaults.txt
|
||||
bash scripts/footprint.sh 2000000 "$RUNS" > output/02-footprint.txt
|
||||
bash scripts/classes.sh > output/03-classes.txt
|
||||
bash scripts/offsets.sh > output/04-offsets.txt
|
||||
bash scripts/container-sizing.sh 2000000 "$CRUNS" > output/05-container-sizing.txt
|
||||
bash scripts/one-cpu.sh 2000000 "$CRUNS" > output/06-one-cpu.txt
|
||||
bash scripts/oom-check.sh > output/07-oom-check.txt
|
||||
echo done
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bytes per instance of the classes that dominate the heap, from GC.class_histogram of the runs made by
|
||||
# footprint.sh (kept in target/hist). The point: which classes shrink, and which do not.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
H="$MOD/target/hist"
|
||||
echo "== bytes per instance (= #bytes / #instances) from jcmd GC.class_histogram after loading 2,000,000 products"
|
||||
printf '%-40s %14s %14s %14s\n' "class" "JDK 26 default" "JDK 27 default" "JDK 27 opt-out"
|
||||
for cls in 'com.ankurm.memory.Product' 'java.util.HashMap$Node' 'java.lang.Long' 'java.lang.String' '[B'; do
|
||||
printf '%-40s' "$cls"
|
||||
for cfg in jdk26-default jdk27-default jdk27-optout; do
|
||||
awk -v c="$cls" '$4==c {printf " %14.1f", $3/$2; found=1} END{if(!found) printf " %14s","?"}' "$H/$cfg.txt"
|
||||
done
|
||||
echo
|
||||
done
|
||||
echo
|
||||
echo "== total bytes of those five classes"
|
||||
for cfg in jdk26-default jdk27-default jdk27-optout; do
|
||||
awk -v cfg="$cfg" '$4=="com.ankurm.memory.Product"||$4=="java.util.HashMap$Node"||$4=="java.lang.Long"||$4=="java.lang.String"||$4=="[B" {s+=$3} END{printf "%-16s %d bytes (%.1f MB)\n", cfg, s, s/1048576}' "$H/$cfg.txt"
|
||||
done
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# The smallest container that can hold the catalog, before and after. For each memory limit the app is started
|
||||
# inside a cgroup with that limit and asked to load N products; the table says what happened.
|
||||
# ok the load finished
|
||||
# java-OOM the JVM threw OutOfMemoryError: the heap (a fraction of the limit) was too small
|
||||
# killed(...) the kernel killed the process: total memory, not just heap, went over the limit
|
||||
# no-result-in-25s the JVM stayed alive but the load (about a second when healthy) did not finish in 25
|
||||
# seconds: the heap is so tight that the collector runs continuously and makes no progress
|
||||
# Each cell is RUNS runs ("3/3 ok" = all three finished); a cell near the boundary can be mixed.
|
||||
# usage: container-sizing.sh [N products] [runs per cell]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"; RETRIES=0
|
||||
MB=$((1024*1024))
|
||||
|
||||
sweep() { # title, jvm-flags-for-all, limits...
|
||||
local title="$1" common="$2"; shift 2
|
||||
echo "== $title"
|
||||
printf '%-8s %-8s %-30s %-30s %-30s\n' "limit" "heap" "JDK 26 default" "JDK 27 default" "JDK 27 opt-out"
|
||||
for lim in "$@"; do
|
||||
local cells=() heap="?"
|
||||
for cfg in "26|$JDK26|" "27|$JDK27|" "27o|$JDK27|-XX:-UseCompactObjectHeaders"; do
|
||||
IFS='|' read -r tag jh fl <<<"$cfg"
|
||||
local ok=0 fails=() rss=()
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
d="$TMP/$tag-$lim-$r"
|
||||
CG_MEM=$((lim*MB)) CG_CPUS=2 run_app "$jh" "$N" "$d" $common $fl >/dev/null 2>&1
|
||||
h=$(kv "$d/memory-idle.txt" maxHeapMB); [ -n "$h" ] && heap="$h"
|
||||
[ -f "$d/retries" ] && RETRIES=$((RETRIES+1))
|
||||
out=$(cat "$d/outcome.txt")
|
||||
if [ "$out" = ok ]; then ok=$((ok+1)); rss+=("$(kv "$d/memory.txt" rssMB)"); else fails+=("${out%%(*}"); fi
|
||||
done
|
||||
cell="$ok/$RUNS ok"
|
||||
if [ $ok -gt 0 ]; then cell="$cell rss $(printf '%s\n' "${rss[@]}" | median)MB"; fi
|
||||
if [ ${#fails[@]} -gt 0 ]; then cell="$cell [$(printf '%s\n' "${fails[@]}" | sort | uniq -c | awk '{printf "%s%sx %s",sep,$1,$2; sep=" "}')]"; fi
|
||||
cells+=("$cell")
|
||||
done
|
||||
printf '%-8s %-8s %-30s %-30s %-30s\n' "${lim}MB" "${heap}MB" "${cells[0]}" "${cells[1]}" "${cells[2]}"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# ONLY=75 or ONLY=25 runs one of the two sweeps (each takes about 12 minutes); default is both.
|
||||
ONLY="${ONLY:-both}"
|
||||
echo "# $N products; 2 CPUs; $RUNS runs per cell; rss is the median of the runs that finished"
|
||||
if [ "$ONLY" != 25 ]; then
|
||||
sweep "Heap set to 75% of the container (-XX:MaxRAMPercentage=75)" "-XX:MaxRAMPercentage=75" 400 416 432 448 512 704
|
||||
fi
|
||||
if [ "$ONLY" != 75 ]; then
|
||||
sweep "Heap left at the JVM default (25% of the container)" "" 1024 1152 1216 1280 1536 2048
|
||||
fi
|
||||
echo "# runs that needed a startup retry (the JVM never came up, for a reason other than OOM or a kernel kill): $RETRIES"
|
||||
rm -rf "$TMP"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Where the three JDKs live. Override with environment variables; nothing else is hard-coded.
|
||||
: "${JDK25:=/tmp/tools/j25/jdk-25.0.4.1+1}"
|
||||
: "${JDK26:=$(ls -d /tmp/tools/j26/jdk-26* 2>/dev/null | head -1)}"
|
||||
: "${JDK27:=/tmp/tools/j27/jdk-27+35}"
|
||||
export JDK25 JDK26 JDK27
|
||||
# The runs must not inherit proxy/trust-store options meant for Maven.
|
||||
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# What each JDK decides by default, and what the flags do. Every line is the JVM's own answer
|
||||
# (java -XX:+PrintFlagsFinal), not something inferred from documentation.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
show() { # label jdk flags... (set CG_MEM/CG_CPUS to run inside a container-style cgroup)
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local out cg=""
|
||||
if [ -n "${CG_MEM:-}" ]; then
|
||||
cg="fl$$-$RANDOM"; cg_create "$cg" "$CG_MEM" "${CG_CPUS:-2}"
|
||||
out=$(bash -c "$CG_JOIN" _ "$cg" "$jh/bin/java" "$@" -XX:+PrintFlagsFinal -version 2>&1)
|
||||
cg_remove "$cg"
|
||||
else
|
||||
out=$("$jh/bin/java" "$@" -XX:+PrintFlagsFinal -version 2>&1)
|
||||
fi
|
||||
if ! echo "$out" | grep -q 'UseCompactObjectHeaders'; then
|
||||
printf '%-52s -> JVM refused to start:\n' "$label"; echo "$out" | head -3 | sed 's/^/ /'; return
|
||||
fi
|
||||
local ch gc heap
|
||||
ch=$(echo "$out" | awk '$2=="UseCompactObjectHeaders"{print $4}')
|
||||
gc=$(echo "$out" | awk '($2=="UseG1GC"||$2=="UseSerialGC"||$2=="UseParallelGC"||$2=="UseZGC") && $4=="true"{print $2}' | tr '\n' ' ')
|
||||
heap=$(echo "$out" | awk '$2=="MaxHeapSize"{printf "%d", $4/1048576}')
|
||||
printf '%-52s -> compact=%-5s collector=%-13s MaxHeapSize=%sMB\n' "$label" "$ch" "$gc" "$heap"
|
||||
echo "$out" | grep -E '^(OpenJDK 64-Bit Server VM warning|Error|Unrecognized)' | sed 's/^/ JVM says: /' | head -2
|
||||
}
|
||||
|
||||
for v in 25 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
echo "== JDK $v: $("$jh/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
show "default (this machine: 2 CPUs)" "$jh"
|
||||
show "-XX:+UseCompactObjectHeaders" "$jh" -XX:+UseCompactObjectHeaders
|
||||
show "-XX:-UseCompactObjectHeaders" "$jh" -XX:-UseCompactObjectHeaders
|
||||
echo
|
||||
done
|
||||
|
||||
echo "== The same JDKs in a container: 1 GB memory limit, 1 CPU (a cgroup, set the way Docker sets it)"
|
||||
for v in 25 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=1 show "JDK $v, 1 GB, 1 CPU" "$jh"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=1 show "JDK $v, 1 GB, 1 CPU, MaxRAMPercentage=75" "$jh" -XX:MaxRAMPercentage=75
|
||||
done
|
||||
echo
|
||||
echo "== 1 GB, 2 CPUs"
|
||||
for v in 26 27; do
|
||||
jh_var="JDK$v"; jh="${!jh_var}"
|
||||
CG_MEM=$((1024*1024*1024)) CG_CPUS=2 show "JDK $v, 1 GB, 2 CPUs" "$jh"
|
||||
done
|
||||
echo
|
||||
|
||||
echo "== -XX:MaxRAM: the flag many people used to fake a container limit"
|
||||
show "JDK 26 -XX:MaxRAM=1g" "$JDK26" -XX:MaxRAM=1g
|
||||
show "JDK 27 -XX:MaxRAM=1g" "$JDK27" -XX:MaxRAM=1g
|
||||
echo
|
||||
|
||||
echo "== Compact headers with each collector on JDK 27"
|
||||
for gc in UseG1GC UseParallelGC UseSerialGC UseZGC; do
|
||||
show "JDK 27 -XX:+$gc" "$JDK27" -XX:+$gc
|
||||
done
|
||||
echo
|
||||
|
||||
echo "== Compressed pointers on JDK 27"
|
||||
show "JDK 27 -XX:-UseCompressedClassPointers" "$JDK27" -XX:-UseCompressedClassPointers
|
||||
show "JDK 27 -XX:-UseCompressedOops" "$JDK27" -XX:-UseCompressedOops
|
||||
show "JDK 27 -Xmx32g (a large explicit heap)" "$JDK27" -Xmx32g
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Heap footprint of the same jar on JDK 25, 26 and 27, with compact object headers on and off.
|
||||
# usage: footprint.sh [N products] [runs per configuration]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"
|
||||
MEDS=(); ALL=()
|
||||
|
||||
row() { # label jdk flags...
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local idle=() live=() rss=() gcn=() gcms=() ns=() start=()
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
run_app "$jh" "$N" "$TMP/$label-$r" -Xms1g -Xmx1g "$@" || { echo "$label run $r FAILED"; return; }
|
||||
local d="$TMP/$label-$r"
|
||||
idle+=("$(kv "$d/memory-idle.txt" heapLiveAfterGcMB)")
|
||||
live+=("$(kv "$d/memory.txt" heapLiveAfterGcMB)")
|
||||
rss+=("$(kv "$d/memory.txt" rssMB)")
|
||||
gcn+=("$(kv "$d/load.txt" gcCountDuringLoad)")
|
||||
gcms+=("$(kv "$d/load.txt" gcMillisDuringLoad)")
|
||||
ns+=("$(kv "$d/lookups.txt" nsPerLookup)")
|
||||
ALL+=("${gcn[-1]} ${rss[-1]} ${ns[-1]}")
|
||||
printf 'run %s/%s idle=%sMB live=%sMB rss=%sMB gcDuringLoad=%s (%sms) lookup=%sns compact=%s\n' "$label" "$r" \
|
||||
"${idle[-1]}" "${live[-1]}" "${rss[-1]}" "${gcn[-1]}" "${gcms[-1]}" "${ns[-1]}" "$(kv "$d/memory.txt" UseCompactObjectHeaders)"
|
||||
done
|
||||
MEDS+=("$(printf 'MED %-22s idle=%sMB live=%sMB rss=%sMB gcDuringLoad=%s gcMs=%s lookupNs=%s' "$label" \
|
||||
"$(printf '%s\n' "${idle[@]}" | median)" "$(printf '%s\n' "${live[@]}" | median)" "$(printf '%s\n' "${rss[@]}" | median)" \
|
||||
"$(printf '%s\n' "${gcn[@]}" | median)" "$(printf '%s\n' "${gcms[@]}" | median)" "$(printf '%s\n' "${ns[@]}" | median)")")
|
||||
echo "${MEDS[-1]}"
|
||||
}
|
||||
|
||||
echo "# $(date -u +%F) ${N} products, -Xms1g -Xmx1g, G1, ${RUNS} runs per configuration; MED lines are medians"
|
||||
echo "# JDK25=$("$JDK25/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
echo "# JDK26=$("$JDK26/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
echo "# JDK27=$("$JDK27/bin/java" -version 2>&1 | sed -n 2p)"
|
||||
row jdk25-default "$JDK25"
|
||||
row jdk25-compact "$JDK25" -XX:+UseCompactObjectHeaders
|
||||
row jdk26-default "$JDK26"
|
||||
row jdk26-compact "$JDK26" -XX:+UseCompactObjectHeaders
|
||||
row jdk27-default "$JDK27"
|
||||
row jdk27-optout "$JDK27" -XX:-UseCompactObjectHeaders
|
||||
echo
|
||||
echo "== medians of $RUNS runs per configuration (idle = live set of the idle app; live = after loading the catalog)"
|
||||
printf '%s\n' "${MEDS[@]}"
|
||||
echo
|
||||
stats() { sort -n | awk '{a[NR]=$1} END{printf "min %d / median %d / max %d", a[1], a[int((NR+1)/2)], a[NR]}'; }
|
||||
echo "== the same runs, grouped by whether a collection ran while the catalog was loading (all configurations together)"
|
||||
for g in zero some; do
|
||||
if [ $g = zero ]; then sel='$1==0'; label="no collection during load"; else sel='$1>0'; label="a collection during load"; fi
|
||||
n=$(printf '%s\n' "${ALL[@]}" | awk "$sel" | wc -l)
|
||||
printf '%-27s runs=%-3s rss MB: %s lookup ns: %s\n' "$label" "$n" \
|
||||
"$(printf '%s\n' "${ALL[@]}" | awk "$sel {print \$2}" | stats)" "$(printf '%s\n' "${ALL[@]}" | awk "$sel {print \$3}" | stats)"
|
||||
done
|
||||
# keep one histogram per JVM for the class-level table
|
||||
mkdir -p "$MOD/target/hist"
|
||||
for c in jdk26-default jdk27-default jdk27-optout; do cp "$TMP/$c-1/histogram.txt" "$MOD/target/hist/$c.txt"; done
|
||||
rm -rf "$TMP"
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers. Source this; do not run it.
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MOD="$(cd "$HERE/.." && pwd)"
|
||||
source "$HERE/env.sh"
|
||||
JAR="$MOD/target/catalog.jar"
|
||||
|
||||
# ---- containers -----------------------------------------------------------------------------------------
|
||||
# There is no Docker daemon in the sandbox, so limits are set the way Docker sets them: a cgroup with a
|
||||
# memory limit and a CPU quota. The JVM's container support reads exactly those files (cgroup v1 here).
|
||||
CGROOT=/sys/fs/cgroup
|
||||
cg_create() { # NAME MEMORY_BYTES CPUS
|
||||
mkdir -p "$CGROOT/memory/$1" "$CGROOT/cpu/$1"
|
||||
echo "$2" >"$CGROOT/memory/$1/memory.limit_in_bytes"
|
||||
echo 100000 >"$CGROOT/cpu/$1/cpu.cfs_period_us"
|
||||
echo $(( $3 * 100000 )) >"$CGROOT/cpu/$1/cpu.cfs_quota_us"
|
||||
}
|
||||
cg_remove() { rmdir "$CGROOT/memory/$1" "$CGROOT/cpu/$1" 2>/dev/null; }
|
||||
# In run_app the JVM is started as: bash -c '<join the cgroup>; exec java ...' &
|
||||
# so that $! is the JVM's own pid (a function wrapper would make $! the wrapper's pid).
|
||||
CG_JOIN='n=$1; shift; echo $$ >'"$CGROOT"'/memory/$n/cgroup.procs; echo $$ >'"$CGROOT"'/cpu/$n/cgroup.procs; exec "$@"'
|
||||
|
||||
# run_app JAVA_HOME N OUTDIR [jvm flags...]
|
||||
# Starts the catalog on that JVM, loads N products, and leaves these files in OUTDIR:
|
||||
# startup.txt memory-idle.txt load.txt memory.txt lookups.txt histogram.txt outcome.txt
|
||||
# Set CG_MEM (bytes) and CG_CPUS to run inside a container-style cgroup. Set NMT=1 for native memory tracking.
|
||||
# Returns 1 if the load did not complete; outcome.txt says why: java-OOM, killed (kernel OOM kill),
|
||||
# no-result-in-25s (the JVM is alive but collecting garbage without making progress; a healthy load takes
|
||||
# about a second), or error.
|
||||
run_app_once() {
|
||||
local jh="$1" n="$2" out="$3"; shift 3
|
||||
local port=$((20000 + RANDOM % 20000))
|
||||
local cg=""
|
||||
mkdir -p "$out"
|
||||
local extra=()
|
||||
if [ -n "${NMT:-}" ]; then extra+=(-XX:NativeMemoryTracking=summary); fi
|
||||
if [ -n "${CG_MEM:-}" ]; then
|
||||
cg="jm$$-$RANDOM"; cg_create "$cg" "$CG_MEM" "${CG_CPUS:-2}"
|
||||
bash -c "$CG_JOIN" _ "$cg" "$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
|
||||
else
|
||||
"$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
|
||||
fi
|
||||
local pid=$!
|
||||
local up=0
|
||||
for _ in $(seq 1 400); do
|
||||
kill -0 $pid 2>/dev/null || break
|
||||
if curl -sf "localhost:$port/ping" >/dev/null 2>&1; then up=1; break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
local rc=0
|
||||
if [ $up = 0 ]; then
|
||||
wait $pid 2>/dev/null; local st=$?
|
||||
if grep -q OutOfMemoryError "$out/app.log"; then echo "java-OOM-at-startup" >"$out/outcome.txt"
|
||||
elif [ $st -ge 128 ]; then echo "killed(exit $st)" >"$out/outcome.txt"; else echo "error(exit $st)" >"$out/outcome.txt"; fi
|
||||
: >"$out/startup.txt"; [ -n "$cg" ] && cg_remove "$cg"; return 1
|
||||
fi
|
||||
grep -o 'Started MemoryApplication in [0-9.]* seconds' "$out/app.log" >"$out/startup.txt"
|
||||
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory-idle.txt"
|
||||
curl -s --max-time 25 "localhost:$port/load?n=$n" >"$out/load.txt"; local crc=$?
|
||||
if ! grep -q '^loaded=' "$out/load.txt"; then
|
||||
rc=1
|
||||
if [ $crc -eq 28 ]; then echo "no-result-in-25s" >"$out/outcome.txt"
|
||||
elif ! kill -0 $pid 2>/dev/null; then wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"
|
||||
elif grep -q OutOfMemoryError "$out/app.log" "$out/load.txt"; then echo "java-OOM" >"$out/outcome.txt"
|
||||
else echo "error" >"$out/outcome.txt"; fi
|
||||
else
|
||||
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory.txt"
|
||||
curl -s --max-time 60 "localhost:$port/lookups?n=2000000" >"$out/lookups.txt"
|
||||
"$jh/bin/jcmd" $pid GC.class_histogram >"$out/histogram.txt" 2>&1
|
||||
if [ -n "${NMT:-}" ]; then "$jh/bin/jcmd" $pid VM.native_memory summary >"$out/nmt.txt" 2>&1; fi
|
||||
if kill -0 $pid 2>/dev/null; then echo "ok" >"$out/outcome.txt"; else wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"; rc=1; fi
|
||||
fi
|
||||
kill $pid 2>/dev/null; wait $pid 2>/dev/null
|
||||
[ -n "$cg" ] && cg_remove "$cg"
|
||||
return $rc
|
||||
}
|
||||
|
||||
# run_app: run_app_once, but a run whose JVM never came up for a reason that is neither OutOfMemoryError
|
||||
# nor a kernel kill (in practice: the random port was taken) is retried, and the retry is counted in
|
||||
# $out/retries so that a summary can say so instead of hiding it.
|
||||
run_app() {
|
||||
local out="$3" tries=0
|
||||
while :; do
|
||||
run_app_once "$@"; local rc=$?
|
||||
if [ $rc -ne 0 ] && grep -q '^error(exit' "$out/outcome.txt" 2>/dev/null && [ ! -s "$out/startup.txt" ] && [ $tries -lt 2 ]; then
|
||||
tries=$((tries+1)); echo $tries >"$out/retries"; continue
|
||||
fi
|
||||
return $rc
|
||||
done
|
||||
}
|
||||
|
||||
# kv FILE KEY -> value
|
||||
kv() { grep "^$2=" "$1" | head -1 | cut -d= -f2-; }
|
||||
|
||||
# median of numbers on stdin
|
||||
median() { sort -n | awk '{a[NR]=$1} END{ if(NR%2){print a[(NR+1)/2]} else {printf "%d\n",(a[NR/2]+a[NR/2+1])/2} }'; }
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
cd "$MOD/src/offsets"
|
||||
for cfg in "25||$JDK25" "26||$JDK26" "27||$JDK27" "27|-XX:-UseCompactObjectHeaders|$JDK27"; do
|
||||
IFS='|' read -r v fl jh <<<"$cfg"
|
||||
echo "== JDK $v $fl"
|
||||
"$jh/bin/java" $fl --sun-misc-unsafe-memory-access=allow Offsets.java 2>&1
|
||||
echo
|
||||
done
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# JEP 523 in a small container: 1 CPU, 1 GB. JDK 26 picks Serial here, JDK 27 picks G1. What does the app
|
||||
# cost under each collector, and what does the JVM say it spends outside the heap (Native Memory Tracking)?
|
||||
# usage: one-cpu.sh [N products] [runs]
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
N="${1:-2000000}"; RUNS="${2:-3}"
|
||||
TMP="$(mktemp -d)"
|
||||
MB=$((1024*1024))
|
||||
export NMT=1 CG_MEM=$((1024*MB)) CG_CPUS=1
|
||||
|
||||
nmt() { # file -> "GC committed KB|Total committed KB"
|
||||
local gc tot
|
||||
gc=$(awk '/^- *GC \(reserved/ {gsub(/[^0-9=]/," "); n=split($0,a,"="); print a[3]+0; exit}' "$1")
|
||||
tot=$(awk '/^Total: reserved/ {gsub(/[^0-9=]/," "); n=split($0,a,"="); print a[3]+0; exit}' "$1")
|
||||
echo "$gc|$tot"
|
||||
}
|
||||
|
||||
row() { # label jdk flags...
|
||||
local label="$1" jh="$2"; shift 2
|
||||
local st=() live=() rss=() gcn=() gcms=() ld=() gck=() totk=() coll="" cpus=""
|
||||
for r in $(seq 1 "$RUNS"); do
|
||||
d="$TMP/$label-$r"
|
||||
run_app "$jh" "$N" "$d" -XX:MaxRAMPercentage=50 "$@" >/dev/null 2>&1 || { echo "$label run $r FAILED: $(cat "$d/outcome.txt")"; return; }
|
||||
coll=$(kv "$d/memory.txt" collectors); cpus=$(kv "$d/memory.txt" availableProcessors)
|
||||
st+=("$(grep -o '[0-9.]*' "$d/startup.txt" | tail -1)")
|
||||
live+=("$(kv "$d/memory.txt" heapLiveAfterGcMB)"); rss+=("$(kv "$d/memory.txt" rssMB)")
|
||||
gcn+=("$(kv "$d/load.txt" gcCountDuringLoad)"); gcms+=("$(kv "$d/load.txt" gcMillisDuringLoad)")
|
||||
ld+=("$(kv "$d/load.txt" loadMillis)")
|
||||
IFS='|' read -r g t <<<"$(nmt "$d/nmt.txt")"; gck+=("$g"); totk+=("$t")
|
||||
done
|
||||
m() { printf '%s\n' "$@" | median; }
|
||||
printf '%s\n collectors=%s cpus=%s\n' "== $label" "$coll" "$cpus"
|
||||
printf ' startup=%ss live=%sMB rss=%sMB loadMillis=%s gcDuringLoad=%s (%sms)\n' \
|
||||
"$(m "${st[@]}")" "$(m "${live[@]}")" "$(m "${rss[@]}")" "$(m "${ld[@]}")" "$(m "${gcn[@]}")" "$(m "${gcms[@]}")"
|
||||
printf ' NMT: GC committed=%s KB Total committed=%s KB\n\n' "$(m "${gck[@]}")" "$(m "${totk[@]}")"
|
||||
}
|
||||
|
||||
echo "# $N products, container = 1 GB / 1 CPU (cgroup), -XX:MaxRAMPercentage=50 (512 MB heap), NMT summary on, medians of $RUNS runs"
|
||||
row "jdk26-default (the JVM chose)" "$JDK26"
|
||||
row "jdk26 -XX:+UseG1GC" "$JDK26" -XX:+UseG1GC
|
||||
row "jdk27-default (the JVM chose)" "$JDK27"
|
||||
row "jdk27 -XX:+UseSerialGC" "$JDK27" -XX:+UseSerialGC
|
||||
row "jdk27 -XX:+UseSerialGC, compact off" "$JDK27" -XX:+UseSerialGC -XX:-UseCompactObjectHeaders
|
||||
rm -rf "$TMP"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# What does "killed" mean in the container sweep? Runs the edge case (JDK 27, compact headers off, 400 MB
|
||||
# container, heap 75%) a few times and reads the cgroup's own counters and the kernel log afterwards, so the
|
||||
# claim "the kernel's memory controller killed it" rests on the kernel's say-so and not on an exit code.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
KILLED=()
|
||||
echo "# JDK 27, -XX:-UseCompactObjectHeaders -XX:MaxRAMPercentage=75, container limit 400 MB, 2 CPUs; 2,000,000 products"
|
||||
for attempt in 1 2 3 4; do
|
||||
cg="oomchk$attempt"
|
||||
cg_create "$cg" $((400*1024*1024)) 2
|
||||
port=$((30000 + RANDOM % 5000))
|
||||
bash -c "$CG_JOIN" _ "$cg" "$JDK27/bin/java" -XX:MaxRAMPercentage=75 -XX:-UseCompactObjectHeaders -jar "$JAR" --server.port=$port >/dev/null 2>&1 &
|
||||
pid=$!
|
||||
for _ in $(seq 1 120); do curl -sf localhost:$port/ping >/dev/null 2>&1 && break; sleep 0.25; done
|
||||
resp=$(curl -s --max-time 25 -o /dev/null -w '%{http_code}' "localhost:$port/load?n=2000000")
|
||||
sleep 1
|
||||
if kill -0 $pid 2>/dev/null; then state="process still alive"; else wait $pid 2>/dev/null; state="process gone, exit $?"; KILLED+=("$pid"); fi
|
||||
echo "== attempt $attempt: /load answered HTTP $resp; $state"
|
||||
echo " memory.failcnt=$(cat $CGROOT/memory/$cg/memory.failcnt) memory.max_usage=$(( $(cat $CGROOT/memory/$cg/memory.max_usage_in_bytes) / 1048576 ))MB limit=$(( $(cat $CGROOT/memory/$cg/memory.limit_in_bytes) / 1048576 ))MB $(grep '^oom_kill ' $CGROOT/memory/$cg/memory.oom_control)"
|
||||
kill $pid 2>/dev/null; wait $pid 2>/dev/null
|
||||
cg_remove "$cg"
|
||||
echo
|
||||
done
|
||||
echo "== the kernel log (dmesg): what it says about the processes that disappeared above"
|
||||
for p in "${KILLED[@]:-}"; do
|
||||
[ -n "$p" ] && dmesg 2>/dev/null | grep "Memory cgroup out of memory: Killed process $p " | sed 's/^\[[ 0-9.]*\] //'
|
||||
done
|
||||
exit 0
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# A transcript of the service's own endpoints on JDK 27, so the post can show what a request and the
|
||||
# /memory report look like. Port 18081 is fixed so the transcript reads the same every time.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
PORT=18081
|
||||
"$JDK27/bin/java" -Xms1g -Xmx1g -jar "$JAR" --server.port=$PORT >/dev/null 2>&1 &
|
||||
PID=$!
|
||||
for _ in $(seq 1 200); do curl -sf "localhost:$PORT/ping" >/dev/null 2>&1 && break; sleep 0.25; done
|
||||
for path in "/memory" "/load?n=2000000" "/memory" "/products/42" "/products/2000000"; do
|
||||
echo "\$ curl -s -w ' [HTTP %{http_code}]\n' localhost:$PORT$path"
|
||||
curl -s -w ' [HTTP %{http_code}]\n' "localhost:$PORT$path" | sed 's/^ \[HTTP/[HTTP/'
|
||||
echo
|
||||
done
|
||||
kill $PID 2>/dev/null; wait $PID 2>/dev/null
|
||||
exit 0
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ankurm.memory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* The in-memory catalog. Keys are boxed {@code Long}s on purpose: {@code Map<Long, Product>} is
|
||||
* what most services write, and each entry costs a HashMap node, a Long and a Product.
|
||||
*/
|
||||
@Component
|
||||
public class Catalog {
|
||||
|
||||
private volatile Map<Long, Product> byId = Map.of();
|
||||
|
||||
/** Replaces the catalog with {@code n} generated products. */
|
||||
public void load(int n) {
|
||||
Map<Long, Product> m = HashMap.newHashMap(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
long id = i;
|
||||
m.put(id, new Product(id, 100 + (i % 9_900), i % 500, (i & 7) != 0, "SKU-" + (100_000_000L + i)));
|
||||
}
|
||||
this.byId = m;
|
||||
}
|
||||
|
||||
public Product find(long id) {
|
||||
return byId.get(id);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return byId.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ankurm.memory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
public class CatalogController {
|
||||
|
||||
private final Catalog catalog;
|
||||
|
||||
public CatalogController(Catalog catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
|
||||
/** The endpoint a client would call. */
|
||||
@GetMapping("/products/{id}")
|
||||
public Product product(@PathVariable long id) {
|
||||
Product p = catalog.find(id);
|
||||
if (p == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/** Builds the catalog and reports what the collector did while it happened. */
|
||||
@GetMapping(value = "/load", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String load(@RequestParam int n) {
|
||||
long gc0 = MemoryReport.gcCount();
|
||||
long ms0 = MemoryReport.gcMillis();
|
||||
long t0 = System.nanoTime();
|
||||
catalog.load(n);
|
||||
long loadMs = (System.nanoTime() - t0) / 1_000_000;
|
||||
return "loaded=" + n + "\nloadMillis=" + loadMs
|
||||
+ "\ngcCountDuringLoad=" + (MemoryReport.gcCount() - gc0)
|
||||
+ "\ngcMillisDuringLoad=" + (MemoryReport.gcMillis() - ms0) + "\n";
|
||||
}
|
||||
|
||||
/** Forces a full collection, then prints the live set. This is the number the post compares. */
|
||||
@GetMapping(value = "/memory", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String memory() {
|
||||
Map<String, String> m = MemoryReport.snapshot(catalog.size());
|
||||
return m.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("\n", "", "\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Random lookups inside the JVM, so the number is the cost of chasing pointers through the map and
|
||||
* not the cost of HTTP. Not a load test: a probe for "did the smaller heap change lookup cost".
|
||||
*/
|
||||
@GetMapping(value = "/lookups", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String lookups(@RequestParam int n) {
|
||||
int size = catalog.size();
|
||||
long sum = 0;
|
||||
long x = 88172645463325252L;
|
||||
for (int warm = 0; warm < 2; warm++) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
x ^= x << 13; x ^= x >>> 7; x ^= x << 17;
|
||||
Product p = catalog.find(Math.floorMod(x, size));
|
||||
sum += p.stock();
|
||||
}
|
||||
}
|
||||
long t0 = System.nanoTime();
|
||||
for (int i = 0; i < n; i++) {
|
||||
x ^= x << 13; x ^= x >>> 7; x ^= x << 17;
|
||||
Product p = catalog.find(Math.floorMod(x, size));
|
||||
sum += p.stock();
|
||||
}
|
||||
long ns = System.nanoTime() - t0;
|
||||
return "lookups=" + n + "\nnsPerLookup=" + (ns / n) + "\nchecksum=" + sum + "\n";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/ping", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
public String ping() {
|
||||
return "ok\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ankurm.memory;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* A deliberately ordinary Spring Boot web application. It holds a product catalog in memory,
|
||||
* which is the shape of a lot of real services: many small objects that live for a long time.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class MemoryApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MemoryApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.ankurm.memory;
|
||||
|
||||
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||
import java.lang.management.GarbageCollectorMXBean;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** Reads what the running JVM says about itself. Everything printed by /memory comes from here. */
|
||||
final class MemoryReport {
|
||||
|
||||
private MemoryReport() {
|
||||
}
|
||||
|
||||
static long gcCount() {
|
||||
long c = 0;
|
||||
for (GarbageCollectorMXBean b : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
c += Math.max(0, b.getCollectionCount());
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
static long gcMillis() {
|
||||
long t = 0;
|
||||
for (GarbageCollectorMXBean b : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
t += Math.max(0, b.getCollectionTime());
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
static String vmOption(String name) {
|
||||
try {
|
||||
return ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class).getVMOption(name).getValue();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return "(no such flag)";
|
||||
}
|
||||
}
|
||||
|
||||
/** Resident set size from /proc, in MB; -1 where /proc is not available. */
|
||||
static long rssMb() {
|
||||
try {
|
||||
for (String line : Files.readAllLines(Path.of("/proc/self/status"))) {
|
||||
if (line.startsWith("VmRSS:")) {
|
||||
return Long.parseLong(line.replaceAll("\\D+", "")) / 1024;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// not Linux
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Collects twice so the number is the live set, not whatever garbage happens to be around. */
|
||||
static Map<String, String> snapshot(int products) {
|
||||
System.gc();
|
||||
System.gc();
|
||||
var rt = Runtime.getRuntime();
|
||||
var m = new LinkedHashMap<String, String>();
|
||||
m.put("java.version", System.getProperty("java.version"));
|
||||
m.put("UseCompactObjectHeaders", vmOption("UseCompactObjectHeaders"));
|
||||
m.put("collectors", ManagementFactory.getGarbageCollectorMXBeans().stream()
|
||||
.map(GarbageCollectorMXBean::getName).toList().toString());
|
||||
m.put("availableProcessors", Integer.toString(rt.availableProcessors()));
|
||||
m.put("maxHeapMB", Long.toString(rt.maxMemory() >> 20));
|
||||
m.put("products", Integer.toString(products));
|
||||
m.put("heapLiveAfterGcMB", Long.toString((rt.totalMemory() - rt.freeMemory()) >> 20));
|
||||
m.put("heapCommittedMB", Long.toString(rt.totalMemory() >> 20));
|
||||
m.put("rssMB", Long.toString(rssMb()));
|
||||
m.put("gcCountTotal", Long.toString(gcCount()));
|
||||
m.put("gcMillisTotal", Long.toString(gcMillis()));
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.ankurm.memory;
|
||||
|
||||
/** One catalog entry: a long, two ints, a boolean and a String. Nothing exotic. */
|
||||
public record Product(long id, int priceCents, int stock, boolean active, String sku) {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Where does the first field of an object start? Code that hard-codes "12 bytes of header" gets this wrong
|
||||
* on JDK 27. Run with: java --sun-misc-unsafe-memory-access=allow Offsets.java
|
||||
* (The flag silences the JDK 24+ warning about sun.misc.Unsafe; nothing here reads or writes memory.)
|
||||
*/
|
||||
public class Offsets {
|
||||
|
||||
static class P {
|
||||
long id;
|
||||
int priceCents;
|
||||
int stock;
|
||||
boolean active;
|
||||
Object sku;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static void main(String[] args) throws Exception {
|
||||
Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
|
||||
f.setAccessible(true);
|
||||
sun.misc.Unsafe u = (sun.misc.Unsafe) f.get(null);
|
||||
System.out.println("java " + System.getProperty("java.version")
|
||||
+ " compact headers: " + java.lang.management.ManagementFactory
|
||||
.getPlatformMXBean(com.sun.management.HotSpotDiagnosticMXBean.class)
|
||||
.getVMOption("UseCompactObjectHeaders").getValue());
|
||||
long first = Long.MAX_VALUE;
|
||||
for (Field x : P.class.getDeclaredFields()) {
|
||||
long off = u.objectFieldOffset(x);
|
||||
first = Math.min(first, off);
|
||||
System.out.printf(" %-11s offset %2d%n", x.getName(), off);
|
||||
}
|
||||
System.out.println(" first field starts at byte " + first + " (so the header is at most " + first + " bytes)");
|
||||
System.out.println(" a hard-coded 12-byte header would be " + (first == 12 ? "right" : "WRONG") + " here");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user