-
Notifications
You must be signed in to change notification settings - Fork 6.1k
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
Changes from all commits
dd25e5a
dc021ce
0d32ea1
b115ec4
f8ecbd6
76003b5
d33e9eb
25cb645
2c2c4df
1b8b3ff
f1ab212
7cb3e7b
2831bc8
bcfab1b
dc3dab7
05a2457
2580b5e
a6ffce2
249ac2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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(). | ||
// 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The canary having been processed doesn't tell us anything definitive about |
||
// | ||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
// 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()) { | ||
shipilev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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); | ||
shipilev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: " | ||
|
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 { | ||
shipilev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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.)