8344332: (bf) Migrate DirectByteBuffer away from jdk.internal.ref.Cleaner

Reviewed-by: rriggs, bchristi
This commit is contained in:
Kim Barrett 2025-07-04 04:08:42 +00:00
parent 854de8c9c6
commit 21f2e9a71c
9 changed files with 500 additions and 44 deletions

View File

@ -101,8 +101,15 @@ class Bits { // package-private
// increasing delay before throwing OutOfMemoryError:
// 1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)
// which means that OOME will be thrown after 0.5 s of trying
private static final long INITIAL_SLEEP = 1;
private static final int MAX_SLEEPS = 9;
private static final Object RESERVE_SLOWPATH_LOCK = new Object();
// Token for detecting whether some other thread has done a GC since the
// last time the checking thread went around the retry-with-GC loop.
private static int RESERVE_GC_EPOCH = 0; // Never negative.
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
@ -118,29 +125,45 @@ class Bits { // package-private
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// Don't completely discard interruptions. Instead, record them and
// reapply when we're done here (whether successfully or OOME).
boolean interrupted = false;
try {
// 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 {
// Keep trying to reserve until either succeed or there is no
// further cleaning available from prior GCs. If the latter then
// GC to hopefully find more cleaning to do. Once a thread GCs it
// drops to the later retry with backoff loop.
for (int cleanedEpoch = -1; true; ) {
synchronized (RESERVE_SLOWPATH_LOCK) {
// Test if cleaning for prior GCs (from here) is complete.
// If so, GC to produce more cleaning work, and change
// the token to inform other threads that there may be
// more cleaning work to do. This is done under the lock
// to close a race. We could have multiple threads pass
// the test "simultaneously", resulting in back-to-back
// GCs. For a STW GC the window is small, but for a
// concurrent GC it's quite large. If a thread were to
// somehow be stuck trying to take the lock while enough
// other threads succeeded for the epoch to wrap, it just
// does an excess GC.
if (RESERVE_GC_EPOCH == cleanedEpoch) {
// Increment with overflow to 0, so the value can
// never equal the initial/reset cleanedEpoch value.
RESERVE_GC_EPOCH = Integer.max(0, RESERVE_GC_EPOCH + 1);
System.gc();
break;
}
cleanedEpoch = RESERVE_GC_EPOCH;
}
try {
refprocActive = jlra.waitForReferenceProcessing();
if (tryReserveOrClean(size, cap)) {
return;
}
} catch (InterruptedException e) {
// Defer interrupts and keep trying.
interrupted = true;
refprocActive = true;
cleanedEpoch = -1; // Reset when incomplete.
}
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
@ -151,40 +174,53 @@ class Bits { // package-private
// 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) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
for (int sleeps = 0; true; ) {
try {
if (!jlra.waitForReferenceProcessing()) {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
if (tryReserveOrClean(size, cap)) {
return;
} else if (sleeps < MAX_SLEEPS) {
Thread.sleep(INITIAL_SLEEP << sleeps);
++sleeps; // Only increment if sleep completed.
} else {
throw new OutOfMemoryError
("Cannot reserve "
+ size + " bytes of direct buffer memory (allocated: "
+ RESERVED_MEMORY.get() + ", limit: " + MAX_MEMORY +")");
}
} catch (InterruptedException e) {
interrupted = true;
}
}
// no luck
throw new OutOfMemoryError
("Cannot reserve "
+ size + " bytes of direct buffer memory (allocated: "
+ RESERVED_MEMORY.get() + ", limit: " + MAX_MEMORY +")");
} finally {
// Reapply any deferred interruption.
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
// Try to reserve memory, or failing that, try to make progress on
// cleaning. Returns true if successfully reserved memory, false if
// failed and ran out of cleaning work.
private static boolean tryReserveOrClean(long size, long cap)
throws InterruptedException
{
JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
boolean progressing = true;
while (true) {
if (tryReserveMemory(size, cap)) {
return true;
} else if (BufferCleaner.tryCleaning()) {
progressing = true;
} else if (!progressing) {
return false;
} else {
progressing = jlra.waitForReferenceProcessing();
}
}
}
private static boolean tryReserveMemory(long size, long cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the

View File

@ -0,0 +1,269 @@
/*
* Copyright (c) 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
* 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.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.Objects;
import sun.nio.Cleaner;
/**
* BufferCleaner supports PhantomReference-based management of native memory
* referred to by Direct-XXX-Buffers. Unreferenced DBBs may be garbage
* collected, deactivating the associated PRefs and making them available for
* cleanup here.
*
* There is a configured limit to the amount of memory that may be allocated
* by DBBs. When that limit is reached, the allocator may invoke the garbage
* collector directly to attempt to trigger cleaning here, hopefully
* permitting the allocation to complete. Only if that doesn't free sufficient
* memory does the allocation fail. See java.nio.Bits::reserveMemory() for
* details.
*
* One of the requirements for that approach is having a way to determine that
* deactivated cleaners have been cleaned. java.lang.ref.Cleaner doesn't
* provide such a mechanism, and adding such a mechanism to that class to
* satisfy this unique requirement was deemed undesirable. Instead, this class
* uses the underlying primitives (PhantomReferences, ReferenceQueues) to
* provide the functionality needed for DBB management.
*/
class BufferCleaner {
private static final class PhantomCleaner
extends PhantomReference<Object>
implements Cleaner
{
private final Runnable action;
// Position in the CleanerList.
CleanerList.Node node;
int index;
public PhantomCleaner(Object obj, Runnable action) {
super(obj, queue);
this.action = action;
}
@Override
public void clean() {
if (cleanerList.remove(this)) {
// If being cleaned explicitly by application, rather than via
// reference processing by BufferCleaner, clear the referent so
// reference processing is disabled for this object.
clear();
try {
action.run();
} catch (Throwable x) {
// Long-standing behavior: when cleaning fails, VM exits.
if (System.err != null) {
new Error("nio Cleaner terminated abnormally", x).printStackTrace();
}
System.exit(1);
}
}
}
}
// Cribbed from jdk.internal.ref.CleanerImpl.
static final class CleanerList {
/**
* Capacity for a single node in the list.
* This balances memory overheads vs locality vs GC walking costs.
*/
static final int NODE_CAPACITY = 4096;
/**
* Head node. This is the only node where PhantomCleanabls are
* added to or removed from. This is the only node with variable size,
* all other nodes linked from the head are always at full capacity.
*/
private Node head;
/**
* Cached node instance to provide better behavior near NODE_CAPACITY
* threshold: if list size flips around NODE_CAPACITY, it would reuse
* the cached node instead of wasting and re-allocating a new node all
* the time.
*/
private Node cache;
public CleanerList() {
this.head = new Node();
}
/**
* Insert this PhantomCleaner in the list.
*/
public synchronized void insert(PhantomCleaner phc) {
if (head.size == NODE_CAPACITY) {
// Head node is full, insert new one.
// If possible, pick a pre-allocated node from cache.
Node newHead;
if (cache != null) {
newHead = cache;
cache = null;
} else {
newHead = new Node();
}
newHead.next = head;
head = newHead;
}
assert head.size < NODE_CAPACITY;
// Put the incoming object in head node and record indexes.
final int lastIndex = head.size;
phc.node = head;
phc.index = lastIndex;
head.arr[lastIndex] = phc;
head.size++;
}
/**
* Remove this PhantomCleaner from the list.
*
* @return true if Cleaner was removed or false if not because
* it had already been removed before
*/
public synchronized boolean remove(PhantomCleaner phc) {
if (phc.node == null) {
// Not in the list.
return false;
}
assert phc.node.arr[phc.index] == phc;
// Replace with another element from the head node, as long
// as it is not the same element. This keeps all non-head
// nodes at full capacity.
final int lastIndex = head.size - 1;
assert lastIndex >= 0;
if (head != phc.node || (phc.index != lastIndex)) {
PhantomCleaner mover = head.arr[lastIndex];
mover.node = phc.node;
mover.index = phc.index;
phc.node.arr[phc.index] = mover;
}
// Now we can unlink the removed element.
phc.node = null;
// Remove the last element from the head node.
head.arr[lastIndex] = null;
head.size--;
// If head node becomes empty after this, and there are
// nodes that follow it, replace the head node with another
// full one. If needed, stash the now free node in cache.
if (head.size == 0 && head.next != null) {
Node newHead = head.next;
if (cache == null) {
cache = head;
cache.next = null;
}
head = newHead;
}
return true;
}
/**
* Segment node.
*/
static class Node {
// Array of tracked cleaners, and the amount of elements in it.
final PhantomCleaner[] arr = new PhantomCleaner[NODE_CAPACITY];
int size;
// Linked list structure.
Node next;
}
}
private static final class CleaningThread extends Thread {
public CleaningThread() {}
@Override
public void run() {
while (true) {
try {
Cleaner c = (Cleaner) queue.remove();
c.clean();
} catch (InterruptedException e) {
// Ignore InterruptedException in cleaner thread.
}
}
}
}
/**
* Try to do some cleaning. Takes a cleaner from the queue and executes it.
*
* @return true if a cleaner was found and executed, false if there
* weren't any cleaners in the queue.
*/
public static boolean tryCleaning() {
Cleaner c = (Cleaner) queue.poll();
if (c == null) {
return false;
} else {
c.clean();
return true;
}
}
private static final CleanerList cleanerList = new CleanerList();
private static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
private static CleaningThread cleaningThread = null;
private static void startCleaningThreadIfNeeded() {
synchronized (cleanerList) {
if (cleaningThread != null) {
return;
}
cleaningThread = new CleaningThread();
}
cleaningThread.setDaemon(true);
cleaningThread.start();
}
private BufferCleaner() {}
/**
* Construct a new Cleaner for obj, with the associated action.
*
* @param obj object to track.
* @param action cleanup action for obj.
* @return associated cleaner.
*
*/
public static Cleaner register(Object obj, Runnable action) {
Objects.requireNonNull(obj, "obj");
Objects.requireNonNull(action, "action");
startCleaningThreadIfNeeded();
PhantomCleaner cleaner = new PhantomCleaner(obj, action);
cleanerList.insert(cleaner);
Reference.reachabilityFence(obj);
return cleaner;
}
}

View File

@ -30,6 +30,7 @@ package java.nio;
import java.io.FileDescriptor;
import java.lang.foreign.MemorySegment;
import java.lang.ref.Reference;
import java.nio.BufferCleaner;
import java.util.Objects;
import jdk.internal.foreign.AbstractMemorySegmentImpl;
import jdk.internal.foreign.MemorySessionImpl;
@ -37,7 +38,7 @@ 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.Cleaner;
import sun.nio.ch.DirectBuffer;
@ -122,7 +123,7 @@ class Direct$Type$Buffer$RW$$BO$
address = base;
}
try {
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
cleaner = 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);
@ -197,7 +198,7 @@ class Direct$Type$Buffer$RW$$BO$
#if[rw]
super(-1, 0, cap, cap, fd, isSync, segment);
address = addr;
cleaner = Cleaner.create(this, unmapper);
cleaner = (unmapper == null) ? null : BufferCleaner.register(this, unmapper);
att = null;
#else[rw]
super(cap, addr, fd, unmapper, isSync, segment);

View File

@ -25,9 +25,9 @@
package jdk.internal.misc;
import jdk.internal.ref.Cleaner;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.IntrinsicCandidate;
import sun.nio.Cleaner;
import sun.nio.ch.DirectBuffer;
import java.lang.reflect.Field;

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 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
* 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 sun.nio;
/**
* {@code Cleaner} represents an object and a cleaning action.
*/
public interface Cleaner {
/**
* Unregisters the cleaner and invokes the cleaning action.
* The cleaner's cleaning action is invoked at most once,
* regardless of the number of calls to {@code clean}.
*/
void clean();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2000, 2011, 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
@ -25,7 +25,7 @@
package sun.nio.ch;
import jdk.internal.ref.Cleaner;
import sun.nio.Cleaner;
public interface DirectBuffer {

View File

@ -58,11 +58,11 @@ import jdk.internal.misc.ExtendedMapMode;
import jdk.internal.misc.Unsafe;
import jdk.internal.misc.VM;
import jdk.internal.misc.VM.BufferPool;
import jdk.internal.ref.Cleaner;
import jdk.internal.ref.CleanerFactory;
import jdk.internal.event.FileReadEvent;
import jdk.internal.event.FileWriteEvent;
import jdk.internal.access.foreign.UnmapperProxy;
import sun.nio.Cleaner;
public class FileChannelImpl
extends FileChannel

View File

@ -0,0 +1,48 @@
/*
* 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.
*
* 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 org.openjdk.bench.java.nio;
import java.nio.ByteBuffer;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgs = {"-Xmx256m", "-Xms256m", "-XX:+AlwaysPreTouch"})
public class DirectByteBufferChurn {
@Param({"128", "256", "512", "1024", "2048"})
int recipFreq;
@Benchmark
public Object test() {
boolean direct = ThreadLocalRandom.current().nextInt(recipFreq) == 0;
return direct ? ByteBuffer.allocateDirect(1) : ByteBuffer.allocate(1);
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.
*
* 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 org.openjdk.bench.java.nio;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgs = {"-Xmx1g", "-Xms1g", "-XX:+AlwaysPreTouch"})
public class DirectByteBufferGC {
@Param({"16384", "65536", "262144", "1048576", "4194304"})
int count;
// Make sure all buffers are reachable and available for GC. Buffers
// directly reference their Cleanables, so we do not want to provide
// excess GC parallelism opportunities here, this is why reference
// buffers from a linked list.
//
// This exposes the potential GC parallelism problem in Cleaner lists.
LinkedList<ByteBuffer> buffers;
@Setup
public void setup() {
buffers = new LinkedList<>();
for (int c = 0; c < count; c++) {
buffers.add(ByteBuffer.allocateDirect(1));
}
}
@Benchmark
public void test() {
System.gc();
}
}