| /* |
| * 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 <sgehwolf@redhat.com> |
| * |
| * @library .. |
| * @run build TestScaffold VMConnection TargetListener TargetAdapter |
| * @run compile -g OomDebugTest.java |
| * @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<String> ALL_TESTS_SET = new HashSet<String>(); |
| 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, "<init>", "()V"), |
| new ArrayList(0), |
| ObjectReference.INVOKE_NONVIRTUAL); |
| } |
| } catch (InvocationException e) { |
| handleFailure(e); |
| } |
| } |
| |
| private Method getConstructorForClass(ClassType clsType) { |
| List<Method> methods = clsType.methodsByName("<init>"); |
| 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<String> actualTestsRun = new HashSet<String>(); |
| 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); |
| } |
| |
| } |