add support for diabling gc on returned objects

This commit is contained in:
Chris Plummer 2026-06-08 07:31:35 -07:00
parent 27f27dac96
commit afbc5c90b6
8 changed files with 416 additions and 8 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
@ -3353,4 +3353,7 @@ JDWP "Java(tm) Debug Wire Protocol"
"otherwise, all threads started. ")
(Constant INVOKE_NONVIRTUAL = 0x02
"otherwise, normal virtual invoke (instance methods only)")
(Constant INVOKE_DISABLE_COllECTION = 0x04
"otherwise, the instance returned (if any) and exception thrown (if any) "
"may be collected")
)

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 2024, 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
@ -125,6 +125,9 @@ public interface ClassType extends ReferenceType {
/** Perform method invocation with only the invoking thread resumed */
static final int INVOKE_SINGLE_THREADED = 0x1;
/** Perform perform the equivalent of ObjectReference.disableCollection() on
any ObjectReference returned, including any exception thrown. */
static final int INVOKE_DISABLE_COLLECTION = 0x4;
/**
* Invokes the specified static {@link Method} in the

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
@ -143,6 +143,9 @@ public interface ObjectReference extends Value {
static final int INVOKE_SINGLE_THREADED = 0x1;
/** Perform non-virtual method invocation */
static final int INVOKE_NONVIRTUAL = 0x2;
/** Perform perform the equivalent of ObjectReference.disableCollection() on
any ObjectReference returned, including any exception thrown. */
static final int INVOKE_DISABLE_COLLECTION = 0x4;
/**
* Invokes the specified {@link Method} on this object in the

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
@ -235,6 +235,19 @@ public final class ClassTypeImpl extends InvokableTypeImpl
vm.notifySuspend();
}
/*
* If there is a returned Object or an exception Object, make sure GC
* is disabled if requested.
*/
if ((options & INVOKE_DISABLE_COLLECTION) != 0) {
// Account for implicit disableCollection() done by the debug agent.
if (ret.exception != null) {
ret.exception.incrementGcDisableCount();
} else {
ret.newObject.incrementGcDisableCount();
}
}
if (ret.exception != null) {
throw new InvocationException(ret.exception);
} else {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 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
@ -37,6 +37,7 @@ import com.sun.jdi.InterfaceType;
import com.sun.jdi.InvalidTypeException;
import com.sun.jdi.InvocationException;
import com.sun.jdi.Method;
import com.sun.jdi.ObjectReference;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.VMCannotBeModifiedException;
@ -124,6 +125,22 @@ abstract class InvokableTypeImpl extends ReferenceTypeImpl {
if ((options & ClassType.INVOKE_SINGLE_THREADED) == 0) {
vm.notifySuspend();
}
/*
* If there is a returned Object or an exception Object, make sure GC
* is disabled if requested.
*/
if ((options & ClassType.INVOKE_DISABLE_COLLECTION) != 0) {
// Account for implicit disableCollection() done by the debug agent.
if (ret.getException() != null) {
ret.getException().incrementGcDisableCount();
} else {
if (ret.getResult() instanceof ObjectReferenceImpl obj) {
obj.incrementGcDisableCount();
}
}
}
if (ret.getException() != null) {
throw new InvocationException(ret.getException());
} else {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
@ -424,6 +424,21 @@ public class ObjectReferenceImpl extends ValueImpl
vm.notifySuspend();
}
/*
* If there is a returned Object or an exception Object, make sure GC
* is disabled if requested.
*/
if ((options & INVOKE_DISABLE_COLLECTION) != 0) {
// Account for implicit disableCollection() done by the debug agent.
if (ret.exception != null) {
ret.exception.incrementGcDisableCount();
} else {
if (ret.returnValue instanceof ObjectReferenceImpl obj) {
obj.incrementGcDisableCount();
}
}
}
if (ret.exception != null) {
throw new InvocationException(ret.exception);
} else {
@ -431,6 +446,11 @@ public class ObjectReferenceImpl extends ValueImpl
}
}
/* leave synchronized to keep count accurate */
synchronized void incrementGcDisableCount() {
gcDisableCount++;
}
/* leave synchronized to keep count accurate */
public synchronized void disableCollection() {
if (gcDisableCount == 0) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1998, 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
@ -29,6 +29,7 @@
#include "threadControl.h"
#include "outStream.h"
#include "signature.h"
#include "commonRef.h"
static jrawMonitorID invokerLock;
@ -726,6 +727,7 @@ invoker_completeInvokeRequest(jthread thread)
jint id;
InvokeRequest *request;
jboolean detached;
jbyte options;
jboolean mustReleaseReturnValue = JNI_FALSE;
JDI_ASSERT(thread);
@ -752,9 +754,12 @@ invoker_completeInvokeRequest(jthread thread)
request->started = JNI_FALSE;
request->available = JNI_TRUE; /* For next time around */
options = request->options;
request->options = 0;
detached = request->detached;
if (!detached) {
if (request->options & JDWP_INVOKE_OPTIONS(SINGLE_THREADED)) {
if (options & JDWP_INVOKE_OPTIONS(SINGLE_THREADED)) {
(void)threadControl_suspendThread(thread, JNI_FALSE);
} else {
(void)threadControl_suspendAll();
@ -817,6 +822,32 @@ invoker_completeInvokeRequest(jthread thread)
(void)outStream_writeValue(env, &out, tag, returnValue);
(void)outStream_writeObjectTag(env, &out, exc);
(void)outStream_writeObjectRef(env, &out, exc);
/*
* Pin the returnValue object and exception object if this invoke has
* JDWP_INVOKE_OPTIONS(DISABLE_COllECTION) enabled.
*/
if (options & JDWP_INVOKE_OPTIONS(DISABLE_COllECTION)) {
if (mustReleaseReturnValue && returnValue.l != NULL) {
jlong id = commonRef_refToID(env, returnValue.l);
//tty_message("return id: %ld", id);
jvmtiError error = commonRef_pin(id);
if (error != JVMTI_ERROR_NONE) {
outStream_setError(&out, map2jdwpError(error));
}
commonRef_release(env, id);
}
if (exc != NULL) {
jlong id = commonRef_refToID(env, exc);
//tty_message("exception id: %ld", id);
jvmtiError error = commonRef_pin(id);
if (error != JVMTI_ERROR_NONE) {
outStream_setError(&out, map2jdwpError(error));
}
commonRef_release(env, id);
}
}
/*
* Delete potentially saved global references for return value
* and exception. This must be done before sending the reply or

View File

@ -0,0 +1,318 @@
/*
* 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.
*/
/**
* @test
* @bug 8311176
* @summary Test INVOKE_DISABLE_COLLECTION flag
* @library /test/lib
* @run build TestScaffold VMConnection TargetListener TargetAdapter jdk.test.whitebox.WhiteBox
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
* @run compile -g InvokeGcDisabledTest.java
* @run driver InvokeGcDisabledTest
* @run driver InvokeGcDisabledTest stress
*/
import com.sun.jdi.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
import java.util.*;
import jdk.test.whitebox.WhiteBox;
/********** target program **********/
class InvokeGcDisabledTarg {
static boolean stressMode = false;
private static final WhiteBox WB = WhiteBox.getWhiteBox();
private static volatile boolean stop = false;
public static void main(String[] args){
System.out.println("Howdy!");
if (args.length == 1 && "stress".equals(args[0])) {
System.out.println("debuggee stress mode");
stressMode = true;
}
if (stressMode) {
Thread gcThread = new Thread(() -> {
while (!stop) WB.fullGC();
});
gcThread.start();
}
(new InvokeGcDisabledTarg()).sayHi();
stop = true;
}
void sayHi() {
}
InvokeGcDisabledTarg() {
System.out.println("InvokeGcDisabledTarg::InvokeGcDisabledTarg called");
}
InvokeGcDisabledTarg(boolean ignore) {
System.out.println("InvokeGcDisabledTarg::InvokeGcDisabledTarg for exception called");
throw new RuntimeException("Exception from debuggee");
}
Object newObject() {
System.out.println("InvokeGcDisabledTarg::newObject called");
return new Object();
}
static Object staticNewObject() {
System.out.println("InvokeGcDisabledTarg::staticNewObject called");
return new Object();
}
void throwException() {
System.out.println("InvokeGcDisabledTarg::throwException called");
throw new RuntimeException("Exception from debuggee");
}
void throwStaticException() {
System.out.println("InvokeGcDisabledTarg::throwStaticException called");
throw new RuntimeException("Exception from debuggee");
}
void fullGC() {
WB.fullGC();
}
}
/********** test program **********/
public class InvokeGcDisabledTest extends TestScaffold {
static boolean stressMode = false;
ClassType targetClass;
ThreadReference mainThread;
ObjectReference thisObject;
List<Value> emptyArgs;
List<Value> booleanArg;
Method forceDebuggeeGCMethod = null;
InvokeGcDisabledTest (String args[]) {
super(args);
}
public static void main(String[] args) throws Exception {
if (args.length == 1) {
if ("stress".equals(args[0])) {
System.out.println("debugger stress mode");
stressMode = true;
} else {
throw new RuntimeException("bad argument: " + args[0]);
}
}
try {
new InvokeGcDisabledTest(args).startTests();
} catch (Throwable t) {
t.printStackTrace(System.out);
throw t;
}
}
/********** test assist **********/
void forceDebuggeeGC() throws Exception {
if (forceDebuggeeGCMethod == null) {
forceDebuggeeGCMethod = findMethod(targetClass, "fullGC", "()V");
if (forceDebuggeeGCMethod == null) {
failure("FAILED: Can't find method: \"fullGC\" for class = " + targetClass);
return;
}
}
println("Forcing debuggee full GC");
thisObject.invokeMethod(mainThread, forceDebuggeeGCMethod, emptyArgs, ObjectReference.INVOKE_SINGLE_THREADED);
}
ObjectReference invoke(Method method, InvokeType invokeType, int options, boolean throwsException)
throws Exception {
Value returnValue = null;
options = options | ObjectReference.INVOKE_SINGLE_THREADED;
try {
switch (invokeType) {
case VIRTUAL_INVOKE_METHOD:
returnValue = thisObject.invokeMethod(mainThread, method, emptyArgs, options);
break;
case STATIC_INVOKE_METHOD:
returnValue = targetClass.invokeMethod(mainThread, method, emptyArgs, options);
break;
case NEW_INSTANCE:
returnValue = targetClass.newInstance(mainThread, method,
throwsException ? booleanArg : emptyArgs, options);
break;
}
} catch (InvocationException ie) {
if (!throwsException) {
ie.printStackTrace();
failure("Got Exception: " + ie);
throw ie;
} else {
println("Got expected InvocationException: " + ie.exception());
returnValue = ie.exception();
}
} catch (Exception ee) {
ee.printStackTrace();
failure("Got Exception: " + ee);
throw ee;
}
println(" return val = " + returnValue);
return (ObjectReference)returnValue;
}
void verifyCollected(ObjectReference obj) {
println("Verifying object is collected: " + obj);
if (!obj.isCollected()) {
failure("FAILED: object not collected: " + obj);
}
}
void verifyNotCollected(ObjectReference obj) {
println("Verifying object is not collected: " + obj);
if (obj.isCollected()) {
failure("FAILED: object collected: " + obj);
}
}
private void testInvoke(String invokeMethod, String methodName, String methodSig, InvokeType invokeType,
boolean throwsException, boolean stressMode) throws Exception {
ObjectReference obj;
Method method = findMethod(targetClass, methodName, methodSig);
if (method == null) {
failure("FAILED: Can't find method: \"" + methodName + methodSig + "\" for class = " + targetClass);
return;
}
println("*************************************************************************");
println("* TESTING " + invokeMethod +" on " + targetClass.name() + "." + methodName + methodSig);
println("* throwsException=" + throwsException + " stressMode=" + stressMode);
println("*************************************************************************");
if (!stressMode) {
// Theoretically this could generate an ObjectCollectedException, but shouldn't
// unless we are running in stress mode to trigger a lot of GCs.
println("TEST: Verify disableCollection works on allocated object");
obj = invoke(method, invokeType, 0, throwsException);
obj.disableCollection();
forceDebuggeeGC();
verifyNotCollected(obj);
println("TEST: Verify enableCollection allows allocated object to be collected");
obj.enableCollection();
forceDebuggeeGC();
verifyCollected(obj);
}
println("TEST: Verify INVOKE_DISABLE_COLLECTION disables collection of allocated object");
obj = invoke(method, invokeType, ObjectReference.INVOKE_DISABLE_COLLECTION, throwsException);
forceDebuggeeGC();
verifyNotCollected(obj);
println("TEST: Verify enableCollection allows allocated object to be collected");
obj.enableCollection();
forceDebuggeeGC();
verifyCollected(obj);
}
private enum InvokeType {
VIRTUAL_INVOKE_METHOD,
STATIC_INVOKE_METHOD,
NEW_INSTANCE
}
/********** test core **********/
protected void runTests() throws Exception {
ObjectReference obj;
enableWhiteBoxAPI(); // Allow debuggee to use WhiteBoxAPI
BreakpointEvent bpe = startTo("InvokeGcDisabledTarg", "sayHi", "()V");
targetClass = (ClassType)bpe.location().declaringType();
mainThread = bpe.thread();
StackFrame frame = mainThread.frame(0);
thisObject = frame.thisObject();
emptyArgs = new ArrayList(0);
booleanArg = Arrays.asList(new Value[]{vm().mirrorOf(true)});
mainThread.suspend();
vm().resume();
/*
* We test 3 invocation APIs to make sure that using the INVOKE_DISABLE_COLLECTION
* flag prevents the method result from being collected.
* -ObjectReference.invokeMethod(): We don't differentiate between virtual and
* non-virtual because it uses the same code paths.
* -ClassType.invokeMethod(): Invocation of a static method.
* -ClassType.newInstance(): Invocation of a constructor. We don't test
* InterfaceType.newInstance() because it uses the same code paths.
*
* Each of these APIs can throw an InvocationException, which contains an
* the ObjectReference of the exception thrown by the debuggee, so we also
* need to test each of the above 3 APIs with an exception thrown to make
* sure the INVOKE_DISABLE_COLLECTION flag also works on the exception object.
*/
testInvoke("ObjectReference.invokeMethod()",
"newObject", "()Ljava/lang/Object;",
InvokeType. VIRTUAL_INVOKE_METHOD, false, stressMode);
testInvoke("ObjectReference.invokeMethod()",
"newObject", "()Ljava/lang/Object;",
InvokeType.VIRTUAL_INVOKE_METHOD, true, stressMode);
testInvoke("ClassType.invokeMethod()",
"staticNewObject", "()Ljava/lang/Object;",
InvokeType.STATIC_INVOKE_METHOD,false, stressMode);
testInvoke("ClassType.invokeMethod()",
"staticNewObject", "()Ljava/lang/Object;",
InvokeType.STATIC_INVOKE_METHOD, true, stressMode);
testInvoke("ClassType.newInstance()",
"<init>", "()V",
InvokeType.NEW_INSTANCE, false, stressMode);
testInvoke("ClassType.newInstance()",
"<init>", "(Z)V",
InvokeType.NEW_INSTANCE, true, stressMode);
/*
* resume the target so it can exit.
*/
mainThread.resume();
listenUntilVMDisconnect();
/*
* Deal with results of test.
* Of anything has called failure("foo") testFailed will be true.
*/
if (!testFailed) {
println("InvokeGcDisabledTest: passed");
} else {
throw new Exception("InvokeGcDisabledTest: failed");
}
}
}