diff --git a/src/java.base/share/classes/java/nio/Bits.java b/src/java.base/share/classes/java/nio/Bits.java index a9a22d6079e..07dd6a91890 100644 --- a/src/java.base/share/classes/java/nio/Bits.java +++ b/src/java.base/share/classes/java/nio/Bits.java @@ -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 diff --git a/src/java.base/share/classes/java/nio/BufferCleaner.java b/src/java.base/share/classes/java/nio/BufferCleaner.java new file mode 100644 index 00000000000..ddacecbcf63 --- /dev/null +++ b/src/java.base/share/classes/java/nio/BufferCleaner.java @@ -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 + 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 queue = new ReferenceQueue(); + 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; + } +} diff --git a/src/java.base/share/classes/java/nio/Direct-X-Buffer.java.template b/src/java.base/share/classes/java/nio/Direct-X-Buffer.java.template index 12bc0103d1b..8bd943f4e67 100644 --- a/src/java.base/share/classes/java/nio/Direct-X-Buffer.java.template +++ b/src/java.base/share/classes/java/nio/Direct-X-Buffer.java.template @@ -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); diff --git a/src/java.base/share/classes/jdk/internal/misc/Unsafe.java b/src/java.base/share/classes/jdk/internal/misc/Unsafe.java index dba2e6fa7ed..1aff92eb36d 100644 --- a/src/java.base/share/classes/jdk/internal/misc/Unsafe.java +++ b/src/java.base/share/classes/jdk/internal/misc/Unsafe.java @@ -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; diff --git a/src/java.base/share/classes/sun/nio/Cleaner.java b/src/java.base/share/classes/sun/nio/Cleaner.java new file mode 100644 index 00000000000..8be99705d06 --- /dev/null +++ b/src/java.base/share/classes/sun/nio/Cleaner.java @@ -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(); +} diff --git a/src/java.base/share/classes/sun/nio/ch/DirectBuffer.java b/src/java.base/share/classes/sun/nio/ch/DirectBuffer.java index 8fcea1eb57d..794ea7a0a0b 100644 --- a/src/java.base/share/classes/sun/nio/ch/DirectBuffer.java +++ b/src/java.base/share/classes/sun/nio/ch/DirectBuffer.java @@ -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 { diff --git a/src/java.base/share/classes/sun/nio/ch/FileChannelImpl.java b/src/java.base/share/classes/sun/nio/ch/FileChannelImpl.java index 240405c2f7c..7f37ad36452 100644 --- a/src/java.base/share/classes/sun/nio/ch/FileChannelImpl.java +++ b/src/java.base/share/classes/sun/nio/ch/FileChannelImpl.java @@ -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 diff --git a/test/micro/org/openjdk/bench/java/nio/DirectByteBufferChurn.java b/test/micro/org/openjdk/bench/java/nio/DirectByteBufferChurn.java new file mode 100644 index 00000000000..73a6a6744cb --- /dev/null +++ b/test/micro/org/openjdk/bench/java/nio/DirectByteBufferChurn.java @@ -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); + } + +} diff --git a/test/micro/org/openjdk/bench/java/nio/DirectByteBufferGC.java b/test/micro/org/openjdk/bench/java/nio/DirectByteBufferGC.java new file mode 100644 index 00000000000..bef9d2d9805 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/nio/DirectByteBufferGC.java @@ -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 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(); + } + +}