Hi,
I converted 149 carriers to value classes, taking a Java host layer from
6 to
155, and measured the result on 28-ea+10-569 with --enable-preview, six
A/B-counterbalanced pairs per leg against its own preceding tag.
One scoping note, because it would otherwise be the first thing asked.
The
measured application is a benchmark target, not a production deployment.
What is unusual is the host layer itself. Its object model was designed
after
JEP 401 rather than retrofitted to it: 155 value carriers, buffers held
as
Panama MemorySegments, request paths on virtual threads. Exactly one
identity
record survives the conversion anywhere in its own modules -- a
bootstrap
dependency graph whose Map components are mutated in place, so it cannot
be a
value class. It is reached once at startup and never on the request
path, but
I mention it because "mutable state requires identity" turned out to be
the
only limit the sweep actually hit.
One caveat worth reporting, one question I cannot answer from outside.
1. jdk.CodeCacheStatistics adaptorCount is a coverage measure, not a
code
property -- which cost me two wrong readings before I noticed.
Per-run counts are not stable for a fixed binary. Diffing class-load
sets
between two runs of one arm that differed by 2 adapters: the raw sets
differ by 932 absent and 931 extra (java.lang.invoke LambdaForm and
hidden
classes, whose names carry a per-run address), for a net of -1 that
means
nothing. After excluding generated and hidden classes, the two runs
differ
by exactly two classes and none extra -- a pair of
conditionally-loaded JFR
event classes for one code path. Runs that agree on adaptorCount have
identical stable class sets.
Separately, the count also depends on the recording configuration.
Two
campaigns of the same two binaries, differing only in whether the
host
layer's own JFR event types were enabled (application-defined
jdk.jfr.Event
subclasses, not jdk.* events), give an E->F adapter delta of 150 and
of
144, against 149 converted carriers either way. So there is no stable
adapters-per-carrier relationship to read off; the same conversion
looks
like +1 or -5 depending on what the recording enabled. I have not
shown
that this is the same mechanism as the class-loading difference above
--
the classes load either way once their call site is reached, so if
the two
are related it is through instrumentation rather than loading.
I am not reporting this as a defect. I am reporting it because
adaptorCount
is the obvious metric to reach for when costing a scalarised calling
convention, and it appears to answer a broader question than that.
Chasing
it into the source: jfrPeriodic.cpp sets the field from
CodeCache::adapter_count(bt), which delegates to
CodeHeap::adapter_count()
-- a count of adapter blobs in the code cache, alongside blob_count
and
nmethod_count. So it is the general i2c/c2i signature-adapter
machinery,
rather than a counter added for value classes. The field has no
documented
semantics that I could find, which is how I ended up reading it as
one.
The one thing I could not determine, and it may be the more useful
question. The two classes above are plain final classes extending
jdk.jfr.Event, with String and boolean fields only -- not value
classes,
and JFR's event model does not admit value-typed fields anyway. They
are
also classes the JVM rewrites: jdk.jfr.Event's commit(),
shouldCommit() and
isEnabled() are final and empty in the source, so a registered
subclass is
bytecode-instrumented and gains generated methods. Loading one is
therefore
not a plain class load.
Following that through does not demolish the measurement, though it
moves
the ground under it. AdapterHandlerLibrary generates the i2c/c2i
stubs and
AdapterBlob is where they live, and under Valhalla those adapters are
precisely where a scalarised convention is translated to and from a
buffered one -- unpacking fields into registers inbound, re-buffering
them
outbound. So the counter is general in category, but a delta between
two
builds with identical class sets and identical call sites should
still be
attributable to the convention change. If that reading is wrong, that
is
the thing I would most like corrected.
What I cannot settle is the keying. Blobs are cached per signature,
not per
class, so 150 blobs against 149 converted carriers may be structure
or may
be coincidence, and my two-classes-two-adapters case does not
distinguish
the two. I also have a separate seven-adapter step whose class
cluster I
never isolated.
2. What does an absent NULL_FREE_NON_ATOMIC field layout mean?
With -XX:+UnlockDiagnosticVMOptions -XX:+PrintFieldLayout, a value
record
with two int components (8-byte payload) prints no
NULL_FREE_NON_ATOMIC
layout but does print NULL_FREE_ATOMIC 8/8. That reads inverted,
since
non-atomic is the weaker guarantee. The column is empty for every
carrier
I probed, including ones where NULLABLE_NON_ATOMIC has no size limit
at all
(33/8 for a 32-byte payload), which suggests a generation rule rather
than
a capability limit.
My indirect evidence: ValueClass.newNullRestrictedNonAtomicArray on
an
8-byte two-String carrier returns an array whose layout kind is
NULL_FREE_ATOMIC_FLAT -- a non-atomic request served by an atomic
layout.
But that is array allocation, a different path from field-layout
emission,
so it makes supersession plausible without establishing it.
So: for a value class whose payload fits one word, is the absence of
a
NULL_FREE_NON_ATOMIC field layout because no valid layout exists, or
because the atomic layout supersedes it?
The durable number from all this, for whatever it is worth as a data
point: at
an unchanged loaded-class count, 149 conversions cost +0.18 MB of
Metaspace,
with compressed class space flat to slightly negative and non-class
metadata
carrying the growth. Throughput, CPU per request and p99 did not move
outside
the noise band. The host layer keeps its payload off-heap behind
MemorySegment, so I expected little layout upside; the metadata cost is
the
part I had not seen quantified on a codebase whose object model was
designed
around the JEP.
If such a measurement exists I would be glad to read it.
Full method and raw figures:
https://github.com/exeris-systems/exeris-benchmarks/blob/main/results/reports/2026-08-26-valhalla-carrier-sweep-on-an-off-heap-runtime.md#4-the-measurable-effect-is-a-cost
Arkadiusz Przychocki