Skip to content

8344332: (bf) Migrate DirectByteBuffer to use java.lang.ref.Cleaner #22165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions src/java.base/share/classes/java/lang/ref/Reference.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand All @@ -25,12 +25,10 @@

package java.lang.ref;

import jdk.internal.misc.Unsafe;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import jdk.internal.access.JavaLangRefAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.ref.Cleaner;

/**
* Abstract base class for reference objects. This class defines the
Expand Down Expand Up @@ -199,11 +197,6 @@ private static class ReferenceHandler extends Thread {
}

public void run() {
// pre-load and initialize Cleaner class so that we don't
// get into trouble later in the run loop if there's
// memory shortage while loading/initializing it lazily.
Unsafe.getUnsafe().ensureClassInitialized(Cleaner.class);

while (true) {
processPendingReferences();
}
Expand Down Expand Up @@ -253,18 +246,7 @@ private static void processPendingReferences() {
Reference<?> ref = pendingList;
pendingList = ref.discovered;
ref.discovered = null;

if (ref instanceof Cleaner) {
((Cleaner)ref).clean();
// Notify any waiters that progress has been made.
// This improves latency for nio.Bits waiters, which
// are the only important ones.
synchronized (processPendingLock) {
processPendingLock.notifyAll();
}
} else {
ref.enqueueFromPending();
}
ref.enqueueFromPending();
}
// Notify any waiters of completion of current round.
synchronized (processPendingLock) {
Expand Down
117 changes: 73 additions & 44 deletions src/java.base/share/classes/java/nio/Bits.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ static boolean unaligned() {
private static final AtomicLong COUNT = new AtomicLong();
private static volatile boolean MEMORY_LIMIT_SET;

private static final Object RESERVE_SLOW_LOCK = new Object();

// max. number of sleeps during try-reserving with exponentially
// increasing delay before throwing OutOfMemoryError:
// 1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)
Expand All @@ -107,71 +109,98 @@ static boolean unaligned() {
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, long cap) {

if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1) {
MAX_MEMORY = VM.maxDirectMemory();
MEMORY_LIMIT_SET = true;
}

// optimist!
// Optimistic path: enough memory to satisfy allocation.
if (tryReserveMemory(size, cap)) {
return;
}

final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// Short on memory, with potentially many threads competing for it.
// To alleviate progress races, acquire the lock and go slow.
synchronized (RESERVE_SLOW_LOCK) {
reserveMemorySlow(size, cap);
}
}

static void reserveMemorySlow(long size, long cap) {
// Slow path under the lock. This code would try to trigger cleanups and
// sense if cleaning was performed. Since the failure mode is OOME,
// there is no need to rush.
//
// If this code is modified, make sure a stress test like DirectBufferAllocTest
// performs well.

// Semi-optimistic attempt after acquiring the slow-path lock.
if (tryReserveMemory(size, cap)) {
return;
}

// No free memory. We need to trigger cleanups and wait for them to make progress.
// This requires triggering the GC and waiting for eventual buffer cleanups
// or the absence of any profitable cleanups.
//
// To do this efficiently, we need to wait for several activities to run:
// 1. GC needs to discover dead references and hand them over to Reference
// processing thread. This activity can be asynchronous and can complete after
// we unblock from System.gc().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding cleared references to the pending list is always completed before the
GC invocation completes. Doing otherwise would break or significantly
complicate Reference.waitForReferenceProcessing(), and to no good purpose.
That function should only return false when all references cleared by a prior
GC have been enqueued in their respective queues. There are tests that depend
on that. (I looked, and so far as I can tell, none of the extant GCs violate
this.)

// 2. Reference processing thread needs to process dead references and enqueue them
// to Cleaner thread. This activity is normally concurrent with the rest of
// Java code, and is subject to reference processing thread having time to process.
// 3. Cleaner thread needs to process the enqueued references and call cleanables
// on dead buffers. Like (2), this activity is also concurrent, and relies on
// Cleaner getting time to act.
//
// It is somewhat simple to wait for Reference processing and Cleaner threads to be idle.
// However, that is not a good indicator they have processed buffers since our last
// System.gc() request: they may not have started yet after System.gc() unblocked,
// or have not yet seen that previous step ran. It is Really Hard (tm) to coordinate
// all these activities.
//
// Instead, we are checking directly if Cleaner have acted on since our last System.gc():
// install the canary, call System.gc(), wait for canary to get processed (dead). This
// signals that since our last call to System.gc(), steps (1) and (2) have finished, and
// step (3) is currently in progress.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The canary having been processed doesn't tell us anything definitive about
step (2), because steps (2) and (3) occur concurrently. The reference
processing thread transfers references to their respective queues, and the
cleaner thread processes references that have been placed in its queue, both
running at the same time. All we know is that the canary got transferred and
cleaned. There may or many not have been other references similarly
transferred and cleaned. There may or may not be more references in the
pending list, in the cleaner queue, or both. That the canary has been cleaned
doesn't give us any information about either the pending list or the cleaner
queue.

//
// The last bit is a corner case: since canary is not ordered with other buffer cleanups,
// it is possible that canary gets dead before the rest of the buffers get cleaned. This
// corner case would be handled with a normal retry attempt, after trying to allocate.
// If allocation succeeds even after partial cleanup, we are done. If it does not, we get
// to try again, this time reliably getting the results of the first cleanup run. Not

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After trying again, all we know is that both the previous and the new canary
have been processed. We don't know anything about other references, either
from the latest or preceeding GCs. Consider this unfortunate scenario. The
first canary ended up at the head of the pending list. The reference
processing thread transfered it to the cleaner queue and then stalled. The
cleaner thread processed the first canary. The waiting thread noted that,
retried and failed the reservation, and retried the GC and wait for the
canary. The retry canary also happened to end up at the front of the updated
pending list. The reference processor transferred it and again stalled. The
cleaner thread processed the retry canary. No real cleaning has happened,
i.e. we have not reliably gotten the results of the first cleanup run.

// handling this case specially simplifies implementation.

boolean interrupted = false;
try {
BufferCleaner.Canary canary = null;

// Retry allocation until success or there are no more
// references (including Cleaners that might free direct
// buffer memory) to process and allocation still fails.
boolean refprocActive;
do {
long sleepTime = 1;
for (int sleeps = 0; sleeps < MAX_SLEEPS; sleeps++) {
if (canary == null || canary.isDead()) {
// If canary is not yet initialized, we have not triggered a cleanup.
// If canary is dead, there was progress, and it was not enough.
// Trigger GC -> Reference processing -> Cleaner again.
canary = BufferCleaner.newCanary();
System.gc();
}

// Exponentially back off waiting for Cleaner to catch up.
try {
refprocActive = jlra.waitForReferenceProcessing();
Thread.sleep(sleepTime);
sleepTime *= 2;
} catch (InterruptedException e) {
// Defer interrupts and keep trying.
interrupted = true;
refprocActive = true;
}
if (tryReserveMemory(size, cap)) {
return;
}
} while (refprocActive);

// trigger VM's Reference processing
System.gc();

// A retry loop with exponential back-off delays.
// Sometimes it would suffice to give up once reference
// processing is complete. But if there are many threads
// competing for memory, this gives more opportunities for
// any given thread to make progress. In particular, this
// seems to be enough for a stress test like
// DirectBufferAllocTest to (usually) succeed, while
// without it that test likely fails. Since failure here
// ends in OOME, there's no need to hurry.
long sleepTime = 1;
int sleeps = 0;
while (true) {

// See if we can satisfy the allocation now.
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
try {
if (!jlra.waitForReferenceProcessing()) {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
}
} catch (InterruptedException e) {
interrupted = true;
}
}

// no luck
// No luck:
throw new OutOfMemoryError
("Cannot reserve "
+ size + " bytes of direct buffer memory (allocated: "
Expand Down
84 changes: 84 additions & 0 deletions src/java.base/share/classes/java/nio/BufferCleaner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package java.nio;

import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;

/**
* Handles buffer cleaners.
*/
class BufferCleaner {
private static final Cleaner CLEANER = Cleaner.create();

private BufferCleaner() {
// No instantiation.
}

/**
* Register a new cleanable for object and associated action.
*
* @param obj object to track
* @param action cleanup action
* @return associated cleanable
*/
static Cleanable register(Object obj, Runnable action) {
if (action != null) {
return CLEANER.register(obj, action);
} else {
return null;
}
}

/**
* Sets up a new canary on the same cleaner. When canary is dead,
* it is a signal that cleaner had acted.
*
* @return a canary
*/
static Canary newCanary() {
Canary canary = new Canary();
register(new Object(), canary);
return canary;
}

/**
* A canary.
*/
static class Canary implements Runnable {
volatile boolean dead;

@Override
public void run() {
dead = true;
}

public boolean isDead() {
return dead;
}
}

}
36 changes: 23 additions & 13 deletions src/java.base/share/classes/java/nio/Direct-X-Buffer.java.template
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -30,14 +30,14 @@ package java.nio;
import java.io.FileDescriptor;
import java.lang.foreign.MemorySegment;
import java.lang.ref.Reference;
import java.lang.ref.Cleaner.Cleanable;
import java.util.Objects;
import jdk.internal.foreign.AbstractMemorySegmentImpl;
import jdk.internal.foreign.MemorySessionImpl;
import jdk.internal.foreign.SegmentFactories;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.misc.ScopedMemoryAccess.ScopedAccessError;
import jdk.internal.misc.VM;
import jdk.internal.ref.Cleaner;
import sun.nio.ch.DirectBuffer;


Expand Down Expand Up @@ -78,18 +78,28 @@ class Direct$Type$Buffer$RW$$BO$
}

public void run() {
UNSAFE.freeMemory(address);
Bits.unreserveMemory(size, capacity);
try {
UNSAFE.freeMemory(address);
Bits.unreserveMemory(size, capacity);
} catch (Throwable x) {
// Long-standing behavior: when deallocation fails, VM exits.
if (System.err != null) {
new Error("Cleaner terminated abnormally", x).printStackTrace();
}
System.exit(1);
}
}
}

private final Cleaner cleaner;
private final Cleanable cleanable;

public Cleaner cleaner() { return cleaner; }
public Cleanable cleanable() {
return cleanable;
}

#else[byte]

public Cleaner cleaner() { return null; }
public Cleanable cleanable() { return null; }

#end[byte]

Expand Down Expand Up @@ -122,7 +132,7 @@ class Direct$Type$Buffer$RW$$BO$
address = base;
}
try {
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
cleanable = BufferCleaner.register(this, new Deallocator(base, size, cap));
} catch (Throwable t) {
// Prevent leak if the Deallocator or Cleaner fail for any reason
UNSAFE.freeMemory(base);
Expand All @@ -144,7 +154,7 @@ class Direct$Type$Buffer$RW$$BO$
Direct$Type$Buffer(long addr, int cap, Object ob, MemorySegment segment) {
super(-1, 0, cap, cap, segment);
address = addr;
cleaner = null;
cleanable = null;
att = ob;
}

Expand All @@ -154,7 +164,7 @@ class Direct$Type$Buffer$RW$$BO$
Direct$Type$Buffer(long addr, int cap, Object ob, FileDescriptor fd, boolean isSync, MemorySegment segment) {
super(-1, 0, cap, cap, fd, isSync, segment);
address = addr;
cleaner = null;
cleanable = null;
att = ob;
}

Expand All @@ -165,7 +175,7 @@ class Direct$Type$Buffer$RW$$BO$
Direct$Type$Buffer(long addr, long cap) {
super(-1, 0, checkCapacity(cap), (int)cap, null);
address = addr;
cleaner = null;
cleanable = null;
att = null;
}

Expand Down Expand Up @@ -197,7 +207,7 @@ class Direct$Type$Buffer$RW$$BO$
#if[rw]
super(-1, 0, cap, cap, fd, isSync, segment);
address = addr;
cleaner = Cleaner.create(this, unmapper);
cleanable = BufferCleaner.register(this, unmapper);
att = null;
#else[rw]
super(cap, addr, fd, unmapper, isSync, segment);
Expand All @@ -224,7 +234,7 @@ class Direct$Type$Buffer$RW$$BO$
segment);
address = ((Buffer)db).address + off;
#if[byte]
cleaner = null;
cleanable = null;
#end[byte]
Object attachment = db.attachment();
att = (attachment == null ? db : attachment);
Expand Down
Loading