oracle/graal · error · IllegalStateException
Already closed
Error message
Already closed
What it means
NativeBinaryOutput uses a sentinel pos == Integer.MIN_VALUE after close()/free to mark the buffer dead. Every write(int)/write(byte[],int,int) calls checkClosed(), which throws IllegalStateException('Already closed') when the sentinel is set. Writing after the unmanaged native memory has been freed is guarded this way because it would otherwise corrupt freed memory.
Source
Thrown at compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/truffle/BinaryOutput.java:627
/**
* Closes the buffer and frees off-heap allocated resources.
*/
@Override
public void close() {
if (unmanaged) {
UnmanagedMemory.free(address);
byteBufferView = null;
address = Word.nullPointer();
length = 0;
unmanaged = false;
pos = Integer.MIN_VALUE;
}
}
private void checkClosed() {
if (pos == Integer.MIN_VALUE) {
throw new IllegalStateException("Already closed");
}
}
private void ensureCapacity(int neededCapacity) {
if (neededCapacity - length > 0) {
byteBufferView = null;
int newCapacity = length << 1;
if (newCapacity - neededCapacity < 0) {
newCapacity = neededCapacity;
}
if (newCapacity - Integer.MAX_VALUE > 0) {
throw new OutOfMemoryError();
}
if (unmanaged) {
address = UnmanagedMemory.realloc(address, Word.unsigned(newCapacity));
} else {
CCharPointer newAddress = UnmanagedMemory.malloc(newCapacity);
memcpy(newAddress, address, pos);View on GitHub (pinned to a66e9ccd1d)
Solutions
- Track ownership: null the reference on close and check for null before writing.
- Move post-close writes to a fresh BinaryOutput instance instead of reusing the closed one.
- Restructure try-with-resources so error reporting happens before the resource closes (write status inside the try, close last).
- Catch IllegalStateException around optional trailing writes and re-open the channel if the payload matters.
Example fix
// before
try (NativeBinaryOutput out = open()) { ... } finally { out.write(statusByte); }
// after (write status before close)
try (NativeBinaryOutput out = open()) {
...
out.write(statusByte);
} Defensive patterns
Strategy: validation
Validate before calling
// Track closure yourself so you never call into the output afterwards if (outRef.get() != null) outRef.get().write(b, off, len);
Try / catch
try {
out.write(b, off, len);
} catch (IllegalStateException e) {
if ("Already closed".equals(e.getMessage())) {
// open a fresh BinaryOutput and rewrite the record
}
} Prevention
- Keep all writes inside the try-with-resources body
- Null the reference on close
- Do error-reporting writes before the resource closes
When it happens
Trigger: Calling any write method on a BinaryOutput whose close() already ran — e.g. a try-with-resources block whose body escapes into a callback that writes later, or a cached output used after an error path closed it.
Common situations: Error-path cleanup closes the stream, then a finally/catch block still tries to write a status record; shared output reused across requests; async completion writing after the resource was released.
Related errors
- Reading handle from a closed scope.
- Partial character at end
- malformed input around byte %d
- malformed input: partial character at end
- Unknown tag %d
AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14).
Data as JSON: /api/errors/96023f4970e283bd.
Report an issue: GitHub.