Skip to content

Release Jackson BufferRecycler to its pool in encoders#37059

Merged
bclozel merged 1 commit into
spring-projects:7.0.xfrom
samueldlightfoot:jackson-recycler-release
Jul 16, 2026
Merged

Release Jackson BufferRecycler to its pool in encoders#37059
bclozel merged 1 commit into
spring-projects:7.0.xfrom
samueldlightfoot:jackson-recycler-release

Conversation

@samueldlightfoot

@samueldlightfoot samueldlightfoot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Both encoders acquire a pooled BufferRecycler via factory._getBufferRecycler() but never return it. Jackson 3 changed the default pool from a ThreadLocal to a per-factory ConcurrentDequePool that only refills on an explicit releaseToPool(), so the gap leaves the pool empty and every encode allocates a fresh recycler and buffers.

Hold the recycler in a local and release it once the generator and byte builder are done: in encodeValue's finally, and in the streaming path's doAfterTerminate after generator.close() and byteBuilder.release(). releaseToPool() is idempotent, so sharing the recycler with the generator cannot double-release.

Allocation profile

Setup: Bytes allocated per encodeValue call, measured with HotSpot ThreadMXBean.getThreadAllocatedBytes() single-threaded at steady state (200k warmup, 500k ops/round × 7, median), using a default Jackson 3 JsonMapper; the only difference between arms is the added BufferRecycler.releaseToPool().

Payload (JSON size) Before After Saving
Single Pojo (25 B) 18,720 B/op 800 B/op −17,920 B/op (−96%)
200-Pojo list (9,381 B) 45,992 B/op 27,578 B/op −18,414 B/op (−40%)

The fix removes a fixed ~18 KB of recycler-buffer allocation per encode — constant in absolute terms, so the percentage just tracks payload size.

@samueldlightfoot samueldlightfoot force-pushed the jackson-recycler-release branch from 7745212 to 7a3adb1 Compare July 16, 2026 14:58
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jul 16, 2026
@samueldlightfoot samueldlightfoot force-pushed the jackson-recycler-release branch 2 times, most recently from 5ec43f3 to 3b84f6d Compare July 16, 2026 15:11
@bclozel bclozel changed the base branch from main to 7.0.x July 16, 2026 16:59
@bclozel bclozel self-assigned this Jul 16, 2026
@bclozel bclozel added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jul 16, 2026
@bclozel bclozel added this to the 7.0.9 milestone Jul 16, 2026
Both encoders acquire a pooled BufferRecycler via
factory._getBufferRecycler() but never return it. Jackson 3 changed the
default pool from a ThreadLocal to a per-factory ConcurrentDequePool that
only refills on an explicit releaseToPool(), so the gap leaves the pool
empty and every encode allocates a fresh recycler and buffers.

Hold the recycler in a local and release it once the generator and byte
builder are done: in encodeValue's finally, and in the streaming path's
doAfterTerminate after generator.close() and byteBuilder.release().
releaseToPool() is idempotent, so sharing the recycler with the generator
cannot double-release.

Closes spring-projectsgh-37059

Signed-off-by: samlightfoot <samueldlightfoot@gmail.com>
@bclozel bclozel force-pushed the jackson-recycler-release branch from 3b84f6d to a2feb9f Compare July 16, 2026 17:06
@bclozel bclozel closed this in a2feb9f Jul 16, 2026
@bclozel bclozel merged commit a2feb9f into spring-projects:7.0.x Jul 16, 2026
1 check passed
@bclozel

bclozel commented Jul 16, 2026

Copy link
Copy Markdown
Member

Thanks so much @samueldlightfoot for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: bug A general bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants