--- /dev/null 2016-10-10 09:17:56.995265204 +0200 +++ new/test/com/sun/jdi/oom/OomDebugTest.java 2016-10-10 17:10:09.828681923 +0200 @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2016 Red Hat Inc. + * + * 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 8153711 + * @summary JDWP: Memory Leak (global references not deleted after invokeMethod). + * + * @author Severin Gehwolf + * + * @library .. + * @run build TestScaffold VMConnection TargetListener TargetAdapter + * @run compile -g OomDebugTest.java + * @run shell OomDebugTestSetup.sh + * @run main OomDebugTest OomDebugTestTarget test1 + * @run main OomDebugTest OomDebugTestTarget test2 + * @run main OomDebugTest OomDebugTestTarget test3 + * @run main OomDebugTest OomDebugTestTarget test4 + * @run main OomDebugTest OomDebugTestTarget test5 + */ +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import com.sun.jdi.ArrayReference; +import com.sun.jdi.ArrayType; +import com.sun.jdi.ClassType; +import com.sun.jdi.Field; +import com.sun.jdi.InvocationException; +import com.sun.jdi.Method; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StackFrame; +import com.sun.jdi.VMOutOfMemoryException; +import com.sun.jdi.Value; +import com.sun.jdi.event.BreakpointEvent; +import com.sun.jdi.event.ExceptionEvent; + +/***************** Target program **********************/ + +class OomDebugTestTarget { + + OomDebugTestTarget() { + System.out.println("DEBUG: invoked constructor"); + } + static class FooCls { + @SuppressWarnings("unused") + private byte[] bytes = new byte[3000000]; + }; + + FooCls fooCls = new FooCls(); + byte[] byteArray = new byte[0]; + + void testMethod(FooCls foo) { + System.out.println("DEBUG: invoked 'void testMethod(FooCls)', foo == " + foo); + } + + void testPrimitive(byte[] foo) { + System.out.println("DEBUG: invoked 'void testPrimitive(byte[])', foo == " + foo); + } + + byte[] testPrimitiveArrRetval() { + System.out.println("DEBUG: invoked 'byte[] testPrimitiveArrRetval()'"); + return new byte[3000000]; + } + + FooCls testFooClsRetval() { + System.out.println("DEBUG: invoked 'FooCls testFooClsRetval()'"); + return new FooCls(); + } + + public void entry() {} + + public static void main(String[] args){ + System.out.println("DEBUG: OomDebugTestTarget.main"); + new OomDebugTestTarget().entry(); + } +} + +/***************** Test program ************************/ + +public class OomDebugTest extends TestScaffold { + + private static final String[] ALL_TESTS = new String[] { + "test1", "test2", "test3", "test4", "test5" + }; + private static final Set ALL_TESTS_SET = new HashSet(); + static { + ALL_TESTS_SET.addAll(Arrays.asList(ALL_TESTS)); + } + private static final String TEST_CLASSES = System.getProperty("test.classes", "."); + private static final File RESULT_FILE = new File(TEST_CLASSES, "results.properties"); + private static final String LAST_TEST = ALL_TESTS[ALL_TESTS.length - 1]; + private ReferenceType targetClass; + private ObjectReference thisObject; + private int failedTests; + private final String testMethod; + + public OomDebugTest(String[] args) { + super(args); + if (args.length != 2) { + throw new RuntimeException("Test failed unexpectedly."); + } + this.testMethod = args[1]; + } + + @Override + protected void runTests() throws Exception { + try { + addListener(new TargetAdapter() { + + @Override + public void exceptionThrown(ExceptionEvent event) { + String name = event.exception().referenceType().name(); + System.err.println("DEBUG: Exception thrown in debuggee was: " + name); + } + }); + /* + * Get to the top of entry() + * to determine targetClass and mainThread + */ + BreakpointEvent bpe = startTo("OomDebugTestTarget", "entry", "()V"); + targetClass = bpe.location().declaringType(); + + mainThread = bpe.thread(); + + StackFrame frame = mainThread.frame(0); + thisObject = frame.thisObject(); + java.lang.reflect.Method m = findTestMethod(); + m.invoke(this); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + failure(); + } catch (SecurityException e) { + e.printStackTrace(); + failure(); + } + /* + * resume the target, listening for events + */ + listenUntilVMDisconnect(); + } + + private java.lang.reflect.Method findTestMethod() + throws NoSuchMethodException, SecurityException { + return OomDebugTest.class.getDeclaredMethod(testMethod); + } + + private void failure() { + failedTests++; + } + + /* + * Test case: Object reference as method parameter. + */ + @SuppressWarnings("unused") // called via reflection + private void test1() throws Exception { + System.out.println("DEBUG: ------------> Running test1"); + try { + Field field = targetClass.fieldByName("fooCls"); + ClassType clsType = (ClassType)field.type(); + Method constructor = getConstructorForClass(clsType); + for (int i = 0; i < 15; i++) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + ObjectReference objRef = clsType.newInstance(mainThread, + constructor, + new ArrayList(0), + ObjectReference.INVOKE_NONVIRTUAL); + if (objRef.isCollected()) { + System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP."); + continue; + } + invoke("testMethod", "(LOomDebugTestTarget$FooCls;)V", objRef); + } + } catch (InvocationException e) { + handleFailure(e); + } + } + + /* + * Test case: Array reference as method parameter. + */ + @SuppressWarnings("unused") // called via reflection + private void test2() throws Exception { + System.out.println("DEBUG: ------------> Running test2"); + try { + Field field = targetClass.fieldByName("byteArray"); + ArrayType arrType = (ArrayType)field.type(); + + for (int i = 0; i < 15; i++) { + ArrayReference byteArrayVal = arrType.newInstance(3000000); + if (byteArrayVal.isCollected()) { + System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP."); + continue; + } + invoke("testPrimitive", "([B)V", byteArrayVal); + } + } catch (VMOutOfMemoryException e) { + defaultHandleOOMFailure(e); + } + } + + /* + * Test case: Array reference as return value. + */ + @SuppressWarnings("unused") // called via reflection + private void test3() throws Exception { + System.out.println("DEBUG: ------------> Running test3"); + try { + for (int i = 0; i < 15; i++) { + invoke("testPrimitiveArrRetval", + "()[B", + Collections.EMPTY_LIST, + vm().mirrorOfVoid()); + } + } catch (InvocationException e) { + handleFailure(e); + } + } + + /* + * Test case: Object reference as return value. + */ + @SuppressWarnings("unused") // called via reflection + private void test4() throws Exception { + System.out.println("DEBUG: ------------> Running test4"); + try { + for (int i = 0; i < 15; i++) { + invoke("testFooClsRetval", + "()LOomDebugTestTarget$FooCls;", + Collections.EMPTY_LIST, + vm().mirrorOfVoid()); + } + } catch (InvocationException e) { + handleFailure(e); + } + } + + /* + * Test case: Constructor + */ + @SuppressWarnings({ "unused", "unchecked", "rawtypes" }) // called via reflection + private void test5() throws Exception { + System.out.println("DEBUG: ------------> Running test5"); + try { + ClassType type = (ClassType)thisObject.type(); + for (int i = 0; i < 15; i++) { + type.newInstance(mainThread, + findMethod(targetClass, "", "()V"), + new ArrayList(0), + ObjectReference.INVOKE_NONVIRTUAL); + } + } catch (InvocationException e) { + handleFailure(e); + } + } + + private Method getConstructorForClass(ClassType clsType) { + List methods = clsType.methodsByName(""); + if (methods.size() != 1) { + throw new RuntimeException("FAIL. Expected only one, the default, constructor"); + } + return methods.get(0); + } + + private void handleFailure(InvocationException e) { + // There is no good way to see the OOME diagnostic message in the target since the + // TestScaffold might throw an exception while trying to print the stack trace. I.e + // it might get a a VMDisconnectedException before the stack trace printing finishes. + System.err.println("FAILURE: InvocationException thrown. Trying to determine cause..."); + defaultHandleOOMFailure(e); + } + + private void defaultHandleOOMFailure(Exception e) { + e.printStackTrace(); + failure(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + void invoke(String methodName, String methodSig, Value value) + throws Exception { + List args = new ArrayList(1); + args.add(value); + invoke(methodName, methodSig, args, value); + } + + void invoke(String methodName, + String methodSig, + @SuppressWarnings("rawtypes") List args, + Value value) throws Exception { + Method method = findMethod(targetClass, methodName, methodSig); + if ( method == null) { + failure("FAILED: Can't find method: " + + methodName + " for class = " + targetClass); + return; + } + invoke(method, args, value); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + void invoke(Method method, List args, Value value) throws Exception { + thisObject.invokeMethod(mainThread, method, args, 0); + System.out.println("DEBUG: Done invoking method via debugger."); + } + + Value fieldValue(String fieldName) { + Field field = targetClass.fieldByName(fieldName); + return thisObject.getValue(field); + } + + // Determine the pass/fail status on some heuristic and don't fail the + // test if < 3 of the total number of tests (currently 5) fail. This also + // has the nice side effect that all tests are first attempted and only + // all tests ran an overall pass/fail status is determined. + private static void determineOverallTestStatus(OomDebugTest oomTest) + throws IOException, FileNotFoundException { + Properties resultProps = new Properties(); + if (!RESULT_FILE.exists()) { + RESULT_FILE.createNewFile(); + } + FileInputStream fin = null; + try { + fin = new FileInputStream(RESULT_FILE); + resultProps.load(fin); + resultProps.put(oomTest.testMethod, + Integer.toString(oomTest.failedTests)); + } finally { + if (fin != null) { + fin.close(); + } + } + System.out.println("DEBUG: Finished running test '" + + oomTest.testMethod + "'."); + if (LAST_TEST.equals(oomTest.testMethod)) { + System.out.println("DEBUG: Determining overall test status."); + Set actualTestsRun = new HashSet(); + int totalTests = ALL_TESTS.length; + int failedTests = 0; + for (Object key: resultProps.keySet()) { + actualTestsRun.add((String)key); + Object propVal = resultProps.get(key); + int value = Integer.parseInt((String)propVal); + failedTests += value; + } + if (!ALL_TESTS_SET.equals(actualTestsRun)) { + String errorMsg = "Test failed! Expected to run tests '" + + ALL_TESTS_SET + "', but only these were run '" + + actualTestsRun + "'"; + throw new RuntimeException(errorMsg); + } + if (failedTests >= 3) { + String errorMsg = "Test failed. Expected < 3 sub-tests to fail " + + "for a pass. Got " + failedTests + + " failed tests out of " + totalTests + "."; + throw new RuntimeException(errorMsg); + } + RESULT_FILE.delete(); + System.out.println("All " + totalTests + " tests passed."); + } else { + System.out.println("DEBUG: More tests to run. Coninuing."); + FileOutputStream fout = null; + try { + fout = new FileOutputStream(RESULT_FILE); + resultProps.store(fout, "Storing results after test " + + oomTest.testMethod); + } finally { + if (fout != null) { + fout.close(); + } + } + } + } + + public static void main(String[] args) throws Exception { + System.setProperty("test.vm.opts", "-Xmx40m"); // Set debuggee VM option + OomDebugTest oomTest = new OomDebugTest(args); + try { + oomTest.startTests(); + } catch (Throwable e) { + System.out.println("DEBUG: Got exception for test run. " + e); + e.printStackTrace(); + oomTest.failure(); + } + determineOverallTestStatus(oomTest); + } + +}