JVM GC and Native Memory
The JVM garbage collector can eventually reclaim an unreachable Clorch tensor because JavaCPP attaches a native deallocator to every wrapper object. When the GC finalises the Java wrapper, LibTorch frees the underlying allocation. However, the GC has no visibility into:- Native heap pressure — a
[4096 4096]float32 tensor occupies 64 MB of native RAM, but the JVM sees a tiny Java object. - CUDA VRAM pressure — GPU memory exhaustion does not trigger JVM collection.
- Timing — finalisation is not guaranteed to run before the next allocation, especially under steady allocation pressure.
When to Use with-torch
The table below maps workload patterns to the right strategy:
The key insight is granularity:
with-torch should wrap the smallest repeated unit that creates temporary tensors, not every individual operation.
Canonical Training Loop
Create the model and optimizer once before the loop. Scope only batch-local outputs, losses, and intermediates. Finish each scope with a JVM scalar ornil so no tensor escapes:
with-torch scope — they are long-lived JVM references managed by the GC. Batch intermediates (prediction, loss) are created inside the scope, and item-float extracts a plain JVM Float as the final result, so the scope releases all native allocations when the block exits.
How with-torch Works
with-torch opens a JavaCPP PointerScope. Every native pointer allocated while the scope is open is registered with that scope. Before the scope closes, Clorch calls retain! recursively on the final result — the value of the last expression — to remove it from the scope’s ownership. When the scope closes, every remaining registered pointer is immediately deallocated. The final result is returned to the caller and is now managed by the JVM GC.
Returning maps and collections works too. retain! traverses map values and collection elements, so returning a structured result is safe:
The “accidentally retaining” anti-pattern
This loop retains one tensor per iteration and then silently discards it:nil:
Explicit Retention
retain! and rescue-pointers!
When a tensor needs to escape through state that with-torch cannot traverse — an atom, a delay, a closure, a cache, or any arbitrary Java object — call retain! before the scope closes:
rescue-pointers! is an alias for retain! — use whichever name reads more clearly in context.
Simpler alternative: return from with-torch
When possible, just return the tensor as the with-torch result:
retain!.
Manual Release
release! immediately deallocates a pointer without waiting for the GC or a scope boundary. It recursively handles maps and collections:
Interactive Session Scopes
For REPL development, creating awith-torch around every expression is impractical. Instead, open a long-lived session scope for the current thread:
Forcing a GC Pass
gc! calls System/gc and System/runFinalization to prod the JVM into running pending finalizers:
Lifecycle API Reference
Rules of Thumb
- Keep model, optimizer, and all intentionally long-lived tensors outside iteration scopes.
- Use one
with-torchper allocating batch, generation step, or sampler step — not one per individual operation. - End a scope with a JVM scalar or
nilunless a tensor must escape; this is the simplest way to guarantee no accidental retention. - Use
retain!when a tensor must escape through an atom, closure, cache, or other side-effect thatwith-torchcannot discover. - Use
release!only when ownership is clear and unambiguous — prefer scopes. - Do not depend on
gc!for steady-state memory bounds; it is a diagnostic tool. - In REPL sessions, use
start-session!/stop-session!rather than sprinklingwith-torcharound every expression.