mirror of
https://github.com/openjdk/jdk.git
synced 2026-06-08 11:35:21 +00:00
Co-authored-by: Tobias Holenstein <tholenstein@openjdk.org> Co-authored-by: Vladimir Ivanov <vlivanov@openjdk.org> Reviewed-by: dlong, epeter
354 lines
13 KiB
Java
354 lines
13 KiB
Java
/*
|
|
* Copyright (c) 2026, 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.
|
|
*
|
|
* 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 compiler.c2;
|
|
|
|
import java.lang.ref.Cleaner;
|
|
import java.lang.ref.Reference;
|
|
import java.util.Arrays;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import jdk.internal.misc.Unsafe;
|
|
|
|
import compiler.lib.ir_framework.*;
|
|
|
|
/*
|
|
* @test
|
|
* @bug 8290892
|
|
* @summary Tests to ensure that reachabilityFence() correctly keeps objects from being collected prematurely.
|
|
* @modules java.base/jdk.internal.misc
|
|
* @library /test/lib /
|
|
* @run main/othervm -Xbatch compiler.c2.ReachabilityFenceTest
|
|
*/
|
|
public class ReachabilityFenceTest {
|
|
private static final int SIZE = 100;
|
|
|
|
static final boolean[] STATUS = new boolean[2];
|
|
|
|
interface MyBuffer {
|
|
byte get(int offset);
|
|
}
|
|
static class MyBufferOnHeap implements MyBuffer {
|
|
|
|
private static int current = 0;
|
|
private final static byte[][] payload = new byte[10][];
|
|
|
|
private final int id;
|
|
|
|
public MyBufferOnHeap() {
|
|
// Get a unique id, allocate memory, and save the address in the payload array.
|
|
id = current++;
|
|
payload[id] = new byte[SIZE];
|
|
|
|
// Initialize buffer
|
|
for (int i = 0; i < SIZE; ++i) {
|
|
put(i, (byte) 42);
|
|
}
|
|
|
|
// Register a cleaner to free the memory when the buffer is garbage collected.
|
|
int lid = id; // Capture current value
|
|
Cleaner.create().register(this, () -> { free(lid); });
|
|
|
|
System.out.println("Created new buffer of size = " + SIZE + " with id = " + id);
|
|
}
|
|
|
|
private static void free(int id) {
|
|
System.out.println("Freeing buffer with id = " + id);
|
|
for (int i = 0; i < SIZE; ++i) {
|
|
payload[id][i] = (byte)0;
|
|
}
|
|
payload[id] = null;
|
|
|
|
synchronized (STATUS) {
|
|
STATUS[0] = true;
|
|
STATUS.notifyAll();
|
|
}
|
|
}
|
|
|
|
public void put(int offset, byte b) {
|
|
payload[id][offset] = b;
|
|
}
|
|
|
|
public byte get(int offset) {
|
|
try {
|
|
return payload[id][offset];
|
|
} finally {
|
|
Reference.reachabilityFence(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
static class MyBufferOffHeap implements MyBuffer {
|
|
private static Unsafe UNSAFE = Unsafe.getUnsafe();
|
|
|
|
private static int current = 0;
|
|
private static long payload[] = new long[10];
|
|
|
|
private final int id;
|
|
|
|
public MyBufferOffHeap() {
|
|
// Get a unique id, allocate memory, and save the address in the payload array.
|
|
id = current++;
|
|
payload[id] = UNSAFE.allocateMemory(SIZE);
|
|
|
|
// Initialize buffer
|
|
for (int i = 0; i < SIZE; ++i) {
|
|
put(i, (byte) 42);
|
|
}
|
|
|
|
// Register a cleaner to free the memory when the buffer is garbage collected
|
|
int lid = id; // Capture current value
|
|
Cleaner.create().register(this, () -> { free(lid); });
|
|
|
|
System.out.println("Created new buffer of size = " + SIZE + " with id = " + id);
|
|
}
|
|
|
|
private static void free(int id) {
|
|
System.out.println("Freeing buffer with id = " + id);
|
|
for (int i = 0; i < SIZE; ++i) {
|
|
UNSAFE.putByte(payload[id] + i, (byte)0);
|
|
}
|
|
// UNSAFE.freeMemory(payload[id]); // don't deallocate backing memory to avoid crashes
|
|
payload[id] = 0;
|
|
|
|
synchronized (STATUS) {
|
|
STATUS[1] = true;
|
|
STATUS.notifyAll();
|
|
}
|
|
}
|
|
|
|
public void put(int offset, byte b) {
|
|
UNSAFE.putByte(payload[id] + offset, b);
|
|
}
|
|
|
|
public byte get(int offset) {
|
|
try {
|
|
return UNSAFE.getByte(payload[id] + offset);
|
|
} finally {
|
|
Reference.reachabilityFence(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
static MyBufferOffHeap bufferOff = new MyBufferOffHeap();
|
|
static MyBufferOnHeap bufferOn = new MyBufferOnHeap();
|
|
|
|
static long[] counters = new long[4];
|
|
|
|
@ForceInline
|
|
static boolean test(MyBuffer buf) {
|
|
if (buf == null) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < SIZE; i++) {
|
|
// The access is split into base address load (payload[id]), offset computation, and data load.
|
|
// While offset is loop-variant, payload[id] is not and can be hoisted.
|
|
// If bufferOff and payload[id] loads are hoisted outside outermost loop, it eliminates all usages of
|
|
// myBuffer oop inside the loop and bufferOff can be GCed at the safepoint on outermost loop back branch.
|
|
byte b = buf.get(i); // inlined
|
|
if (b != 42) {
|
|
String msg = "Unexpected value = " + b + ". Buffer was garbage collected before reachabilityFence was reached!";
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ===================================== Off-heap versions ===================================== */
|
|
|
|
@Test
|
|
@Arguments(values = {Argument.NUMBER_42})
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.AFTER_PARSING)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, ">=1"}, phase = CompilePhase.AFTER_LOOP_OPTS)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "0"}, phase = CompilePhase.EXPAND_REACHABILITY_FENCES)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.FINAL_CODE)
|
|
static long testOffHeap1(int limit) {
|
|
for (long j = 0; j < limit; j++) {
|
|
MyBufferOffHeap myBuffer = bufferOff; // local
|
|
if (!test(myBuffer)) {
|
|
return j;
|
|
}
|
|
counters[0] = j;
|
|
} // safepoint on loop backedge does NOT contain myBuffer local as part of its JVM state
|
|
return limit;
|
|
}
|
|
|
|
@Test
|
|
@Arguments(values = {Argument.NUMBER_42})
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "2"}, phase = CompilePhase.AFTER_PARSING)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, ">=2"}, phase = CompilePhase.AFTER_LOOP_OPTS)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "0"}, phase = CompilePhase.EXPAND_REACHABILITY_FENCES)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.FINAL_CODE)
|
|
// Both RF nodes share the same referent and there's a single safepoint on loop-back edge.
|
|
// That's the reason why there are 2 RF nodes after parsing, but 1 RF node at the end.
|
|
static long testOffHeap2(int limit) {
|
|
for (long j = 0; j < limit; j++) {
|
|
MyBufferOffHeap myBuffer = bufferOff; // local
|
|
try {
|
|
if (!test(myBuffer)) {
|
|
return j;
|
|
}
|
|
counters[1] = j;
|
|
} finally {
|
|
Reference.reachabilityFence(myBuffer);
|
|
}
|
|
} // safepoint on loop-back edge does NOT contain myBuffer local as part of its JVM state
|
|
return limit;
|
|
}
|
|
|
|
/* ===================================== On-heap versions ===================================== */
|
|
|
|
@Test
|
|
@Arguments(values = {Argument.NUMBER_42})
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.AFTER_PARSING)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, ">=1"}, phase = CompilePhase.AFTER_LOOP_OPTS)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "0"}, phase = CompilePhase.EXPAND_REACHABILITY_FENCES)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.FINAL_CODE)
|
|
static long testOnHeap1(int limit) {
|
|
for (long j = 0; j < limit; j++) {
|
|
MyBufferOnHeap myBuffer = bufferOn; // local
|
|
if (!test(myBuffer)) {
|
|
return j;
|
|
}
|
|
counters[2] = j;
|
|
} // safepoint on loop backedge does NOT contain myBuffer local as part of its JVM state
|
|
return limit;
|
|
}
|
|
|
|
@Test
|
|
@Arguments(values = {Argument.NUMBER_42})
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "2"}, phase = CompilePhase.AFTER_PARSING)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, ">=2"}, phase = CompilePhase.AFTER_LOOP_OPTS)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "0"}, phase = CompilePhase.EXPAND_REACHABILITY_FENCES)
|
|
@IR(counts = {IRNode.REACHABILITY_FENCE, "1"}, phase = CompilePhase.FINAL_CODE)
|
|
static long testOnHeap2(int limit) {
|
|
for (long j = 0; j < limit; j++) {
|
|
MyBufferOnHeap myBuffer = bufferOn; // local
|
|
try {
|
|
if (!test(myBuffer)) {
|
|
return j;
|
|
}
|
|
counters[3] = j;
|
|
} finally {
|
|
Reference.reachabilityFence(myBuffer);
|
|
}
|
|
} // safepoint on loop backedge does NOT contain myBuffer local as part of its JVM state
|
|
return limit;
|
|
}
|
|
|
|
/* ===================================== Helper methods ===================================== */
|
|
|
|
static void runJavaTestCases() throws Throwable {
|
|
// Warmup to trigger compilations.
|
|
for (int i = 0; i < 10_000; i++) {
|
|
testOffHeap1(10);
|
|
testOffHeap2(10);
|
|
testOnHeap1(10);
|
|
testOnHeap2(10);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
Callable<Long>[] tasks = new Callable[] {
|
|
() -> testOffHeap1(100_000_000),
|
|
() -> testOffHeap2(100_000_000),
|
|
() -> testOnHeap1(100_000_000),
|
|
() -> testOnHeap2(100_000_000),
|
|
};
|
|
int taskCount = tasks.length;
|
|
CountDownLatch latch = new CountDownLatch(taskCount + 1);
|
|
final Thread[] workers = new Thread[taskCount];
|
|
final Throwable[] result = new Throwable[taskCount];
|
|
|
|
for (int i = 0; i < taskCount; i++) {
|
|
final int id = i;
|
|
workers[id] = new Thread(() -> {
|
|
latch.countDown(); // synchronize with main thread
|
|
try {
|
|
System.out.printf("Computation thread #%d has started\n", id);
|
|
long cnt = tasks[id].call();
|
|
System.out.printf("#%d Finished after %d iterations\n", id, cnt);
|
|
} catch (Throwable e) {
|
|
System.out.printf("#%d Finished with an exception %s\n", id, e);
|
|
result[id] = e;
|
|
}
|
|
});
|
|
}
|
|
|
|
for (Thread worker : workers) {
|
|
worker.start();
|
|
}
|
|
|
|
latch.countDown(); // synchronize with worker threads
|
|
|
|
Thread.sleep(100); // let workers proceed
|
|
|
|
// Clear references to buffers and make sure it's garbage collected.
|
|
System.out.printf("Buffers set to null. Waiting for garbage collection. (counters = %s)\n", Arrays.toString(counters));
|
|
bufferOn = null;
|
|
bufferOff = null;
|
|
|
|
System.gc();
|
|
|
|
synchronized (STATUS) {
|
|
do {
|
|
if (STATUS[0] && STATUS[1]) {
|
|
break;
|
|
} else {
|
|
System.out.printf("Waiting for cleanup... (counters = %s)\n", Arrays.toString(counters));
|
|
System.gc();
|
|
STATUS.wait(100);
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
for (Thread worker : workers) {
|
|
worker.join();
|
|
}
|
|
|
|
System.out.printf("Results: %s\n", Arrays.deepToString(result));
|
|
|
|
for (Throwable e : result) {
|
|
if (e != null) {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void runIRTestCases() {
|
|
TestFramework framework = new TestFramework();
|
|
framework.addFlags("--add-exports", "java.base/jdk.internal.misc=ALL-UNNAMED");
|
|
framework.start();
|
|
}
|
|
|
|
public static void main(String[] args) throws Throwable {
|
|
try {
|
|
runIRTestCases();
|
|
runJavaTestCases();
|
|
System.out.println("TEST PASSED");
|
|
} catch (Throwable e) {
|
|
System.out.println("TEST FAILED");
|
|
throw e;
|
|
}
|
|
}
|
|
}
|