Look a wrapper class up without boxing its class id - #2960
Merged
Conversation
pyricau
force-pushed
the
strip-without-boxing-class-ids
branch
from
August 20, 2026 14:52
70bf737 to
a26aeea
Compare
Whether an instance wraps a primitive is a lookup by class id, and that lookup went to a Map<Long, PrimitiveWrapperClass>, so hashing the key boxed the id of every instance dump, LOAD_CLASS and CLASS_DUMP record. That is 24 bytes per record: 431 MB of the 437 MB that stripping a 1.4 GB heap dump of 17961453 instances allocated. There's one primitive wrapper class per primitive type, so the ids fit in an 8 long array that is cheaper to scan than to hash. What stripping allocates no longer grows with the heap dump, which matters on Android, where stripping runs inside the app whose heap was just dumped. Output is byte for byte identical on the Android heap dumps in our test resources and on heap dumps taken from a JVM with both identifier sizes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pyricau
force-pushed
the
strip-without-boxing-class-ids
branch
from
August 21, 2026 13:52
a26aeea to
7340b35
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
While benchmarking
HprofPrimitiveArrayStripperagainst hprof-redact I noticed the bytes it allocates grow with the heap dump, and traced it to one boxedLongper record.What it was
Whether an instance wraps a primitive is a lookup by class id, and that lookup went to a
Map<Long, PrimitiveWrapperClass>. Hashing aLongkey boxes it, and class ids are far outsideInteger/Long's valueOf cache, so every instance dump,LOAD_CLASSandCLASS_DUMPrecord allocated a 24 byteLongthat died immediately.JFR names the site directly —
Long.valueOfunderHprofPrimitiveArrayStripper.stripPrimitiveArrays, 41.9 MB of the 49.8 MB one strip allocated — and counting the records that reach a lookup accounts for the total to within 224 bytes:What it is now
There are only ever 8 primitive wrapper classes, so the ids found so far are held in a
LongArrayand scanned. Scanning ≤ 8 longs is cheaper than hashing one, and it allocates nothing.leak_asynctask_o.hprof, 8 MB, 65,814 instancescompose_leak.hprof, 25 MB, 152,199 instancesAllocation no longer grows with the heap dump. Wall clock is unchanged (2.92–2.99 s vs 2.94–3.08 s on the 1.4 GB dump) — bump allocation is cheap and these objects died young — so the win is GC pressure, which is what matters on Android, where
HeapAnalysisConfig(stripHeapDump = true)strips inside the app whose heap was just dumped.Correctness
Output is byte for byte identical before and after on six heap dumps:
leak_asynctask_o.hprof,leak_asynctask_m.hprofandcompose_leak.hproffrom our test resources, plus three taken from a JVM, covering both identifier sizes. The existing stripper tests,detektandcheckKotlinAbipass; the new class is private, so there's no ABI change.No new test, deliberately
A regression here is invisible: the output stays correct and only the garbage comes back. The test that would catch it has to measure allocation, and the measurement is JIT sensitive — C2's escape analysis scalar replaces this very box during a JVM's first strip and stops doing so afterwards, which is what made the first round of every benchmark read 36 MB instead of 437 MB. A test would need a warm-up strip and would still read differently under
-XX:TieredStopAtLevel=1or-XX:-DoEscapeAnalysis. Happy to add one if you'd rather have it with those caveats.🤖 Generated with Claude Code