Repository navigation
Releases: hooji/DurableJavaThreads
Release list
v1.5.0
What's New
Portable snapshots
Snapshots now carry richer JVM metadata and can optionally bundle the user's class file bytes for fully self-contained restore.
SnapshotEnvironmentgains six new fields populated from system properties at freeze time:javaRuntimeVersion(e.g.21.0.2+13-LTS),javaVendor(Temurin / Oracle / Corretto / GraalVM),javaVmName(HotSpot / OpenJ9 / GraalVM),javaSpecificationVersion,osArch, andarchDataModel. A restore-side tool can tell an operator exactly which JDK to provision instead of just "Java 21".- New opt-in
Durable.setEmbedClassBytecodes(boolean)toggle. When enabled, every user class reachable from the snapshot is bundled insideSnapshotEnvironment.ClassEntry.bytecode. On restore,EmbeddedClassInstallerwrites a temp jar and callsInstrumentation.appendToSystemClassLoaderSearchso missing classes become loadable. Default is off (smaller snapshots for the common same-classpath case); turn on for portable checkpoints.
Backwards compatible: older snapshots deserialize with the new fields null via Java's standard missing-field handling (SUID unchanged).
Internal refactors (no behavior change)
JdiHelpersplit into three focused files:PortEnumerator(per-OS listening-port enumeration),PortProber(parallel JDI probes + nonce verification with hard per-probe timeouts),SelfConnection(singleton self-attach lifecycle). Makes the self-attach subsystem easier to navigate and independently extractable.- New
internal/handlers/package: every JDK-type quirk (collections,StringBuilder, boxed primitives,BigDecimal/UUID/java.time.*, enums,java.lang.Objectsentinel, opaque fail-fast, regular-object Objenesis path) now lives in a singleTypeHandlerfile that pairs capture and restore.JdiHeapWalkerdropped from ~1240 lines to ~160;HeapRestorerfrom ~450 to ~210. Ordering-sensitive dispatch documented inTypeHandlerRegistry.
API additions
Durable.setEmbedClassBytecodes(boolean)/Durable.isEmbedClassBytecodes()SnapshotEnvironment.javaRuntimeVersion(),javaVendor(),javaVmName(),javaSpecificationVersion(),osArch(),archDataModel()SnapshotEnvironment.ClassEntry.bytecode()
Tests
232 unit tests (+13), 46 E2E tests (+1). Every JDK-type handler retains its prior correctness; new tests cover flag toggling, snapshot round-trip with embedded bytes, and environment field round-trip.
v1.4.1
What's New
EnumSet & EnumMap Support
- Full freeze/restore support for
EnumSet(bothRegularEnumSetandJumboEnumSet) andEnumMap - EnumSet elements captured via bitmask reading; EnumMap entries via keyUniverse/vals arrays
Improved Type Safety
- Better
UncapturableTypeExceptionmessages for threading types (ThreadLocal, locks, latches, barriers, atomics, executors/thread pools) - All unsupported JDK types fail fast with actionable advice
Code Improvements
DurableTransformer: replaced fragile per-class exclusion list with package-level prefix + whitelistJdiValueConverter: removed unusedrestoredHeap/heapRestorerparametersNullRef: singleton withreadResolve()reduces allocation during heap walkingObjenesisHolder: shared singleton eliminates per-call instantiationwaitForThreadAtMethodpolling interval reduced from 10ms to 1ms for faster restore
Documentation
- All 7 architecture docs updated to reflect current codebase
- Stale references to removed dead code cleaned up
- Audit items marked as resolved
Tests
- 219 unit tests, 44 E2E integration tests (4 new unit + 1 new E2E for EnumSet/EnumMap)
v1.4.0
v1.4.0 — Architecture Documentation & Enterprise Hardening
New Documentation
-
Architecture Document (
docs/ARCHITECTURE.md) — comprehensive technical
description of the library internals: bytecode instrumentation pipeline,
freeze/restore data flow, JDWP port discovery, thread safety model,
serialization format, and error handling strategy -
Code Audit (
docs/CODE_AUDIT.md) — 19 findings across 4 severity levels
covering potential bugs, robustness improvements, simplification opportunities,
and enterprise hardening items
Bug Fixes & Hardening
- GC protection retry loop —
JdiLocalSetter.disableCollection()now retries
up to 5 times whenObjectCollectedExceptionis thrown during the narrow window
between JDI resolve and object pinning. Previously, a single retry was attempted
and if it also failed, the restore silently proceeded with an unpinned reference.
Now throws a clear error after exhausting all attempts. This hardens restore
against extreme GC pressure (e.g., ZGC with small regions under heavy allocation).
Tests
All 258 tests pass (215 unit + 43 E2E).
v1.3.5
v1.3.5 — Major Refactoring, Audit Fixes, and Robustness Improvements
Architecture Refactoring
ThreadRestorer decomposition (Stages 1–4):
- Extract
SnapshotValidator— bytecode/structure hash validation (Stage 1) - Extract
ReflectionHelpers— method lookup, descriptor matching, bottom frame invocation (Stage 2a–2e) - Extract
JdiValueConverter— stateless snapshot ObjectRef → JDI Value conversion (Stage 3) - Extract
JdiLocalSetter— JDI local variable setting with frame matching, GC pinning, class preloading (Stage 4a–4c) - ThreadRestorer reduced from ~1150 lines to ~375 lines
PrologueInjector decomposition (v1.3.1):
- Extract
OperandStackSimulator— JVM operand stack simulation for type tracking - Extract
PrologueEmitter— replay prologue bytecode emission - Extract
PrologueTypes— shared data types - PrologueInjector reduced from ~1300 lines to ~280 lines
Bug Fixes
- FrameSnapshot equals/hashCode/toString —
lambdaBridgeInterfacefield was missing from all three methods, causing two snapshots differing only in lambda bridge interface to compare as equal - Snapshot handler deadlock — moved
handler.accept()afterthreadRef.resume()so the handler no longer runs while the frozen thread is JDI-suspended; eliminates deadlock if handler acquires locks held by the frozen thread - FreezeFlag thread ID reuse — replaced
Set<Long>(thread IDs) withSet<Thread>using IdentityHashMap; eliminates false positives from JVM thread ID recycling - Broken slot reflection —
captureLocalsusedgetMethod("slot")on a package-private method, which always threwNoSuchMethodExceptionand silently defaulted every variable's slot to 0; removed the dead reflection entirely (restore matches by name, not slot)
Dead Code Removal
- Removed 4 dead
catch(Exception ignored)blocks in ThreadFreezer that were confirmed to never fire by running all 258 tests without them - Removed dead if/else branch in
HeapRestorer.populateArray(both branches identical) - Removed unused
isInfrastructureFramestatic import from ThreadRestorer
Enterprise Improvements
- Configurable timeouts via system properties:
durable.freeze.timeout.ms(default 30s) — caller thread wait for freeze workerdurable.jdi.wait.timeout.ms(default 30s) — JDI worker wait for replay threaddurable.restore.timeout.seconds(default 300s) — go-latch wait (existing)
CI
- Removed ubuntu JDK 10 from matrix (intermittent Zulu failures unrelated to our code)
- Bumped actions/checkout, actions/setup-java, actions/cache from v4 to v5 (Node.js 24)
Tests
All 258 tests pass (215 unit + 43 E2E) across JDK 8–25 on Linux, macOS, and Windows.
v1.3.1
What's New in v1.3.1
JDK 10 compatibility fixes
- JDWP port detection:
getPid()now usesProcessHandle.current().pid()on JDK 9+ instead ofManagementFactory.getRuntimeMXBean()which fails to initialize on JDK 10-14 in some environments.isJdwpOnCommandLine()now returnstrue(assume JDWP present) instead offalsewhen ManagementFactory fails, so the port scanner runs instead of falling back to the wrong default port. - JDI stderr noise: Filtered
"Ignoring cmd"diagnostic messages from JDK 10's JDWP transport layer in all E2E tests.
CI matrix
98 test combinations: JDK 8–25 on Linux and Windows, JDK 11–25 (minus 12/14) on macOS. Build + stress tests on all.
Test results
- 131 unit tests: all pass
- 40 E2E integration tests: all pass
v1.3.0
What's New in v1.3.0
Lambda frame support
Lambdas on the call stack are now fully supported for freeze/restore. Previously, any $$Lambda frame threw LambdaFrameException. Now the library transparently handles them.
Supported patterns:
- Lambda as
Runnable/Callablethread target - Lambda with captured variables from enclosing scope
- Lambda passed as a callback to user methods
- Method references (
this::method) as functional interfaces
How it works: The library skips $$Lambda dispatch frames during freeze, detects the functional interface via JDI, and creates a java.lang.reflect.Proxy bridge at restore time. The proxy delegates to the compiler-generated synthetic method (e.g., Foo.lambda$doWork$0), whose replay prologue takes over. Captured variables — which become parameters of the synthetic method — are restored by JDI in the single-pass restore.
Not supported: Stream pipelines (stream().filter().map().collect()) and deeply-chained functional APIs.
invokedynamic instrumentation fix
Fixed VerifyError in methods that create lambdas (e.g., new Thread(() -> work(), "name")). The operand stack simulator now tracks NEW as an uninitialized reference type. Invokes whose sub-stack contains uninitialized refs (between NEW and <init>) get no-op stubs instead of invalid bytecode merge paths.
Also removed legacy invokedynamic workarounds — stubs now uniformly jump to BEFORE_INVOKE for all invoke types.
Test results
- 131 unit tests: all pass
- 40 E2E integration tests: all pass (4 new lambda tests)
v1.2.0
What's New in v1.2.0
Single-pass restore architecture
The restore operation has been rewritten to use a single-pass JDI local-variable setting strategy. Previously, restore used a two-phase approach (Phase 1: rebuild call stack + set parameters, Phase 2: per-frame localsReady() blocking). The new architecture eliminates Phase 2 entirely.
How it works:
- ALL frames (deepest and non-deepest) now jump to
BEFORE_INVOKEin their original code sections during replay - The deepest frame's invoke is the
freeze()call itself — during restore,freeze()detects restore mode via a ThreadLocal flag and blocks on a go-latch - With every frame in its original code section, all local variables are naturally in scope
- JDI sets ALL locals in ALL frames in a single pass
- When
RestoredThread.resume()releases the go-latch,freeze()returns normally and user code continues
Benefits:
- Simpler, more reliable restore — no per-frame Phase 2 synchronization
- Eliminated slot-reuse conflicts —
emitLocalVariables()reverted to params-only-wide scope - Removed dead code: the
localsReady()/resumePoint()blocking machinery is no longer used by the restore path
API improvements
Durable.restore()simple overloads now restore, resume, and join automatically- Advanced overload returns
RestoredThreadhandle with explicitresume()control - All JDI work completes before
restore()returns — no background threads
Bug fixes
- Fixed VerifyError from slot-reuse in
emitLocalDefaults()— now tracks actual store instructions for type inference instead of defaulting untracked slots to Object - Excluded
com/intellij/from instrumentation (IDE runtime classes)
Test results
- 130 unit tests: all pass (including 3 new slot-reuse verification tests)
- 35 E2E integration tests: all pass
v1.1.0
What's New in v1.1.0
Immutable JDK type support
Freeze/restore now correctly handles immutable JDK types whose final fields can't be set via Objenesis. Each type is captured via JDI field reads and reconstructed via parse/valueOf on restore.
Supported types: BigDecimal, BigInteger, UUID, LocalDate, LocalTime, LocalDateTime, Instant, Duration, ZonedDateTime, OffsetDateTime, Period, Year, YearMonth, MonthDay, ZoneOffset, ZoneId, URI
Enum support
Enum constants are captured by name and restored via Enum.valueOf(), preserving:
==identity (critical for switch statements and comparisons)ordinal()values and custom enum fields- Use as
HashMap/HashSetkeys - Works for both user-defined enums and JDK enums like
TimeUnit - Correctly handles Java 8 enum constants with method overrides (anonymous subclasses)
Fail-fast validation (UncapturableTypeException)
freeze() now throws UncapturableTypeException immediately when it encounters a type that cannot be restored correctly:
| Type | Advice |
|---|---|
Optional |
Use T directly with null instead |
| Unmodifiable collections | Use mutable collections |
Thread, ClassLoader |
Remove from captured state |
| I/O / network types | Close or remove before freezing |
Pattern |
Store pattern string, recompile after restore |
JDI reliability improvements
- Double-suspend — All JDI suspend/resume operations use suspend count = 2 for resilience against spurious JDI resume events
- Event draining — Pending JDI events from prior freeze/restore cycles are drained before each suspend to prevent races
- Exponential backoff — Retry with up to 10 attempts (50ms → 800ms backoff) on
InvalidStackFrameException - Pre-load classes — All classes referenced by snapshot locals are force-loaded before frame manipulation, preventing
ClassNotLoadedExceptionduring restore - JDWP bypass for Java 8 — When JDI's classloader lookup fails for bootstrap-loaded classes on Java 8, the library sets slot values directly via JDWP reflection, bypassing the broken client-side type check
- Automatic class loading — Classes referenced by local variables are automatically force-loaded via
Class.forName()through JDI when aClassNotLoadedExceptionis encountered during restore
Bug fixes
- Fixed boxed primitives (Integer, Long, etc.) inside collections losing their values during restore
- Fixed latch deadlock when freeze worker thread completed before caller reached wait
- Fixed JDI connection races during concurrent freeze attempts
- Fixed field mutations being duplicated during skip-pass replay
- Fixed resume stub using dummy receiver instead of heap-restored object
CI coverage expansion
Test matrix now covers 7 JDK versions × 3 platforms = 21 combinations:
- JDK versions: 8, 9, 11, 17 (LTS), 21 (LTS), 24, 25
- Platforms: Ubuntu, macOS (ARM), Windows
- 127 unit tests + 35 integration tests per combination
v1.0.8
What's New
Fix: Java 8 restore failures (enum == identity, ClassNotLoadedException)
Fixed enum restore failing on Java 8 with InvalidStackFrameException during JDI Phase 2 local variable setting.
Root cause: On Java 8, java.util.concurrent.TimeUnit (and potentially other classes) weren't loaded in the restore JVM. When setValue() threw ClassNotLoadedException, the fallback forceLoadClass() used invokeMethod to run Class.forName() — this implicitly resumes the thread, invalidating all cached StackFrame references. The subsequent retry setValue() then crashed with InvalidStackFrameException, causing the restore to fail silently (no enum output, no exception propagated).
Fixes:
-
Pre-load all snapshot classes — Before any frame manipulation, iterate through all snapshot locals, extract class names from type descriptors, and force-load any unloaded classes via JDI. This eliminates
ClassNotLoadedExceptionduringsetValue(), avoiding the frame-invalidatingforceLoadClasscall entirely. -
Double-suspend in restore path — Apply the same double-suspend mitigation from v1.0.7's freeze fix to the restore path's Phase 1 and Phase 2
suspend()/resume()calls.
Testing
- 127 unit tests, 35 integration tests across Java 8/9/11/25 × Ubuntu/macOS/Windows
v1.0.7
What's New
Fix: JDI thread suspension race in multi-cycle freeze (CI fix)
Fixed InvalidStackFrameException: Thread has been resumed failures that occurred across all platforms (Ubuntu, Windows, macOS) during multi-cycle freeze/restore tests.
Root cause: When the JDI connection is reused across freeze cycles (by design — vm.dispose() is not called to avoid spurious stderr output), pending events from prior cycles can race with threadRef.suspend() and resume the thread before frame data is read.
Fix (three-pronged):
- Drain pending JDI events before each suspend attempt — clears stale resume-triggering events accumulated from prior freeze/restore cycles
- Double-suspend the thread (suspend count = 2) so a single spurious resume event doesn't drop the count to 0 and allow the thread to run
- Exponential backoff with up to 10 retries (50ms → 800ms) instead of fixed 50ms × 5 attempts
Also included (from v1.0.6)
- Freeze/restore of immutable JDK types (BigDecimal, UUID, java.time.*, URI)
- Enum capture by name with
==identity preservation - Fail-fast
UncapturableTypeExceptionfor types that can't be restored correctly - Automatic class loading via JDI during cross-JVM restore
- Fixed boxed primitive restoration and latch deadlock
Testing
- 127 unit tests, 35 integration tests across Java 8/9/11/25 × Ubuntu/macOS/Windows