From afbc5c90b66ce8c40bdb0d2565d7f511678f227c Mon Sep 17 00:00:00 2001 From: Chris Plummer Date: Mon, 8 Jun 2026 07:31:35 -0700 Subject: [PATCH] add support for diabling gc on returned objects --- src/java.se/share/data/jdwp/jdwp.spec | 5 +- .../share/classes/com/sun/jdi/ClassType.java | 5 +- .../classes/com/sun/jdi/ObjectReference.java | 5 +- .../com/sun/tools/jdi/ClassTypeImpl.java | 15 +- .../com/sun/tools/jdi/InvokableTypeImpl.java | 19 +- .../sun/tools/jdi/ObjectReferenceImpl.java | 22 +- .../share/native/libjdwp/invoker.c | 35 +- .../jdk/com/sun/jdi/InvokeGcDisabledTest.java | 318 ++++++++++++++++++ 8 files changed, 416 insertions(+), 8 deletions(-) create mode 100644 test/jdk/com/sun/jdi/InvokeGcDisabledTest.java diff --git a/src/java.se/share/data/jdwp/jdwp.spec b/src/java.se/share/data/jdwp/jdwp.spec index 11e3160ec97..54276dc3b7d 100644 --- a/src/java.se/share/data/jdwp/jdwp.spec +++ b/src/java.se/share/data/jdwp/jdwp.spec @@ -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") ) diff --git a/src/jdk.jdi/share/classes/com/sun/jdi/ClassType.java b/src/jdk.jdi/share/classes/com/sun/jdi/ClassType.java index 8ee6ed575bb..0c91af6d987 100644 --- a/src/jdk.jdi/share/classes/com/sun/jdi/ClassType.java +++ b/src/jdk.jdi/share/classes/com/sun/jdi/ClassType.java @@ -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 diff --git a/src/jdk.jdi/share/classes/com/sun/jdi/ObjectReference.java b/src/jdk.jdi/share/classes/com/sun/jdi/ObjectReference.java index f53b8acbb8b..43bbedd2e3d 100644 --- a/src/jdk.jdi/share/classes/com/sun/jdi/ObjectReference.java +++ b/src/jdk.jdi/share/classes/com/sun/jdi/ObjectReference.java @@ -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 diff --git a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ClassTypeImpl.java b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ClassTypeImpl.java index e2b7da2854b..650fa2fab48 100644 --- a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ClassTypeImpl.java +++ b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ClassTypeImpl.java @@ -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 { diff --git a/src/jdk.jdi/share/classes/com/sun/tools/jdi/InvokableTypeImpl.java b/src/jdk.jdi/share/classes/com/sun/tools/jdi/InvokableTypeImpl.java index 3f4f58ccb2f..d503bfc4c04 100644 --- a/src/jdk.jdi/share/classes/com/sun/tools/jdi/InvokableTypeImpl.java +++ b/src/jdk.jdi/share/classes/com/sun/tools/jdi/InvokableTypeImpl.java @@ -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 { diff --git a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java index f71591b8ef5..9bed41b945d 100644 --- a/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java +++ b/src/jdk.jdi/share/classes/com/sun/tools/jdi/ObjectReferenceImpl.java @@ -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) { diff --git a/src/jdk.jdwp.agent/share/native/libjdwp/invoker.c b/src/jdk.jdwp.agent/share/native/libjdwp/invoker.c index 1dcb73500d7..02e8ca68f0c 100644 --- a/src/jdk.jdwp.agent/share/native/libjdwp/invoker.c +++ b/src/jdk.jdwp.agent/share/native/libjdwp/invoker.c @@ -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 diff --git a/test/jdk/com/sun/jdi/InvokeGcDisabledTest.java b/test/jdk/com/sun/jdi/InvokeGcDisabledTest.java new file mode 100644 index 00000000000..ef9dc417425 --- /dev/null +++ b/test/jdk/com/sun/jdi/InvokeGcDisabledTest.java @@ -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 emptyArgs; + List 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()", + "", "()V", + InvokeType.NEW_INSTANCE, false, stressMode); + testInvoke("ClassType.newInstance()", + "", "(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"); + } + } +}