1 /*
   2  * Copyright (c) 2016 Red Hat Inc.
   3  *
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * This code is free software; you can redistribute it and/or modify it
   7  * under the terms of the GNU General Public License version 2 only, as
   8  * published by the Free Software Foundation.
   9  *
  10  * This code is distributed in the hope that it will be useful, but WITHOUT
  11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  13  * version 2 for more details (a copy is included in the LICENSE file that
  14  * accompanied this code).
  15  *
  16  * You should have received a copy of the GNU General Public License version
  17  * 2 along with this work; if not, write to the Free Software Foundation,
  18  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  19  *
  20  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  21  * or visit www.oracle.com if you need additional information or have any
  22  * questions.
  23  */
  24 
  25 /**
  26  *  @test
  27  *  @bug 8153711
  28  *  @summary JDWP: Memory Leak (global references not deleted after invokeMethod).
  29  *
  30  *  @author Severin Gehwolf <sgehwolf@redhat.com>
  31  *
  32  *  @library ..
  33  *  @run build TestScaffold VMConnection TargetListener TargetAdapter
  34  *  @run compile -g OomDebugTest.java
  35  *  @run main OomDebugTest OomDebugTestTarget test1
  36  *  @run main OomDebugTest OomDebugTestTarget test2
  37  *  @run main OomDebugTest OomDebugTestTarget test3
  38  *  @run main OomDebugTest OomDebugTestTarget test4
  39  *  @run main OomDebugTest OomDebugTestTarget test5
  40  */
  41 import java.io.File;
  42 import java.io.FileInputStream;
  43 import java.io.FileNotFoundException;
  44 import java.io.FileOutputStream;
  45 import java.io.IOException;
  46 import java.util.ArrayList;
  47 import java.util.Arrays;
  48 import java.util.Collections;
  49 import java.util.HashSet;
  50 import java.util.List;
  51 import java.util.Properties;
  52 import java.util.Set;
  53 
  54 import com.sun.jdi.ArrayReference;
  55 import com.sun.jdi.ArrayType;
  56 import com.sun.jdi.ClassType;
  57 import com.sun.jdi.Field;
  58 import com.sun.jdi.InvocationException;
  59 import com.sun.jdi.Method;
  60 import com.sun.jdi.ObjectReference;
  61 import com.sun.jdi.ReferenceType;
  62 import com.sun.jdi.StackFrame;
  63 import com.sun.jdi.VMOutOfMemoryException;
  64 import com.sun.jdi.Value;
  65 import com.sun.jdi.event.BreakpointEvent;
  66 import com.sun.jdi.event.ExceptionEvent;
  67 
  68 /***************** Target program **********************/
  69 
  70 class OomDebugTestTarget {
  71 
  72     OomDebugTestTarget() {
  73         System.out.println("DEBUG: invoked constructor");
  74     }
  75     static class FooCls {
  76         @SuppressWarnings("unused")
  77         private byte[] bytes = new byte[3000000];
  78     };
  79 
  80     FooCls fooCls = new FooCls();
  81     byte[] byteArray = new byte[0];
  82 
  83     void testMethod(FooCls foo) {
  84         System.out.println("DEBUG: invoked 'void testMethod(FooCls)', foo == " + foo);
  85     }
  86 
  87     void testPrimitive(byte[] foo) {
  88         System.out.println("DEBUG: invoked 'void testPrimitive(byte[])', foo == " + foo);
  89     }
  90 
  91     byte[] testPrimitiveArrRetval() {
  92         System.out.println("DEBUG: invoked 'byte[] testPrimitiveArrRetval()'");
  93         return new byte[3000000];
  94     }
  95 
  96     FooCls testFooClsRetval() {
  97         System.out.println("DEBUG: invoked 'FooCls testFooClsRetval()'");
  98         return new FooCls();
  99     }
 100 
 101     public void entry() {}
 102 
 103     public static void main(String[] args){
 104         System.out.println("DEBUG: OomDebugTestTarget.main");
 105         new OomDebugTestTarget().entry();
 106     }
 107 }
 108 
 109 /***************** Test program ************************/
 110 
 111 public class OomDebugTest extends TestScaffold {
 112 
 113     private static final String[] ALL_TESTS = new String[] {
 114             "test1", "test2", "test3", "test4", "test5"
 115     };
 116     private static final Set<String> ALL_TESTS_SET = new HashSet<String>();
 117     static {
 118         ALL_TESTS_SET.addAll(Arrays.asList(ALL_TESTS));
 119     }
 120     private static final String TEST_CLASSES = System.getProperty("test.classes", ".");
 121     private static final File RESULT_FILE = new File(TEST_CLASSES, "results.properties");
 122     private static final String LAST_TEST = ALL_TESTS[ALL_TESTS.length - 1];
 123     private ReferenceType targetClass;
 124     private ObjectReference thisObject;
 125     private int failedTests;
 126     private final String testMethod;
 127 
 128     public OomDebugTest(String[] args) {
 129         super(args);
 130         if (args.length != 2) {
 131             throw new RuntimeException("Test failed unexpectedly.");
 132         }
 133         this.testMethod = args[1];
 134     }
 135 
 136     @Override
 137     protected void runTests() throws Exception {
 138         try {
 139             addListener(new TargetAdapter() {
 140 
 141                 @Override
 142                 public void exceptionThrown(ExceptionEvent event) {
 143                     String name = event.exception().referenceType().name();
 144                     System.err.println("DEBUG: Exception thrown in debuggee was: " + name);
 145                 }
 146             });
 147             /*
 148              * Get to the top of entry()
 149              * to determine targetClass and mainThread
 150              */
 151             BreakpointEvent bpe = startTo("OomDebugTestTarget", "entry", "()V");
 152             targetClass = bpe.location().declaringType();
 153 
 154             mainThread = bpe.thread();
 155 
 156             StackFrame frame = mainThread.frame(0);
 157             thisObject = frame.thisObject();
 158             java.lang.reflect.Method m = findTestMethod();
 159             m.invoke(this);
 160         } catch (NoSuchMethodException e) {
 161             e.printStackTrace();
 162             failure();
 163         } catch (SecurityException e) {
 164             e.printStackTrace();
 165             failure();
 166         }
 167         /*
 168          * resume the target, listening for events
 169          */
 170         listenUntilVMDisconnect();
 171     }
 172 
 173     private java.lang.reflect.Method findTestMethod()
 174             throws NoSuchMethodException, SecurityException {
 175         return OomDebugTest.class.getDeclaredMethod(testMethod);
 176     }
 177 
 178     private void failure() {
 179         failedTests++;
 180     }
 181 
 182     /*
 183      * Test case: Object reference as method parameter.
 184      */
 185     @SuppressWarnings("unused") // called via reflection
 186     private void test1() throws Exception {
 187         System.out.println("DEBUG: ------------> Running test1");
 188         try {
 189             Field field = targetClass.fieldByName("fooCls");
 190             ClassType clsType = (ClassType)field.type();
 191             Method constructor = getConstructorForClass(clsType);
 192             for (int i = 0; i < 15; i++) {
 193                 @SuppressWarnings({ "rawtypes", "unchecked" })
 194                 ObjectReference objRef = clsType.newInstance(mainThread,
 195                                                              constructor,
 196                                                              new ArrayList(0),
 197                                                              ObjectReference.INVOKE_NONVIRTUAL);
 198                 if (objRef.isCollected()) {
 199                     System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP.");
 200                     continue;
 201                 }
 202                 invoke("testMethod", "(LOomDebugTestTarget$FooCls;)V", objRef);
 203             }
 204         } catch (InvocationException e) {
 205             handleFailure(e);
 206         }
 207     }
 208 
 209     /*
 210      * Test case: Array reference as method parameter.
 211      */
 212     @SuppressWarnings("unused") // called via reflection
 213     private void test2() throws Exception {
 214         System.out.println("DEBUG: ------------> Running test2");
 215         try {
 216             Field field = targetClass.fieldByName("byteArray");
 217             ArrayType arrType = (ArrayType)field.type();
 218 
 219             for (int i = 0; i < 15; i++) {
 220                 ArrayReference byteArrayVal = arrType.newInstance(3000000);
 221                 if (byteArrayVal.isCollected()) {
 222                     System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP.");
 223                     continue;
 224                 }
 225                 invoke("testPrimitive", "([B)V", byteArrayVal);
 226             }
 227         } catch (VMOutOfMemoryException e) {
 228             defaultHandleOOMFailure(e);
 229         }
 230     }
 231 
 232     /*
 233      * Test case: Array reference as return value.
 234      */
 235     @SuppressWarnings("unused") // called via reflection
 236     private void test3() throws Exception {
 237         System.out.println("DEBUG: ------------> Running test3");
 238         try {
 239             for (int i = 0; i < 15; i++) {
 240                 invoke("testPrimitiveArrRetval",
 241                        "()[B",
 242                        Collections.EMPTY_LIST,
 243                        vm().mirrorOfVoid());
 244             }
 245         } catch (InvocationException e) {
 246             handleFailure(e);
 247         }
 248     }
 249 
 250     /*
 251      * Test case: Object reference as return value.
 252      */
 253     @SuppressWarnings("unused") // called via reflection
 254     private void test4() throws Exception {
 255         System.out.println("DEBUG: ------------> Running test4");
 256         try {
 257             for (int i = 0; i < 15; i++) {
 258                 invoke("testFooClsRetval",
 259                        "()LOomDebugTestTarget$FooCls;",
 260                        Collections.EMPTY_LIST,
 261                        vm().mirrorOfVoid());
 262             }
 263         } catch (InvocationException e) {
 264             handleFailure(e);
 265         }
 266     }
 267 
 268     /*
 269      * Test case: Constructor
 270      */
 271     @SuppressWarnings({ "unused", "unchecked", "rawtypes" }) // called via reflection
 272     private void test5() throws Exception {
 273         System.out.println("DEBUG: ------------> Running test5");
 274         try {
 275             ClassType type = (ClassType)thisObject.type();
 276             for (int i = 0; i < 15; i++) {
 277                 type.newInstance(mainThread,
 278                                  findMethod(targetClass, "<init>", "()V"),
 279                                  new ArrayList(0),
 280                                  ObjectReference.INVOKE_NONVIRTUAL);
 281             }
 282         } catch (InvocationException e) {
 283             handleFailure(e);
 284         }
 285     }
 286 
 287     private Method getConstructorForClass(ClassType clsType) {
 288         List<Method> methods = clsType.methodsByName("<init>");
 289         if (methods.size() != 1) {
 290             throw new RuntimeException("FAIL. Expected only one, the default, constructor");
 291         }
 292         return methods.get(0);
 293     }
 294 
 295     private void handleFailure(InvocationException e) {
 296         // There is no good way to see the OOME diagnostic message in the target since the
 297         // TestScaffold might throw an exception while trying to print the stack trace. I.e
 298         // it might get a a VMDisconnectedException before the stack trace printing finishes.
 299         System.err.println("FAILURE: InvocationException thrown. Trying to determine cause...");
 300         defaultHandleOOMFailure(e);
 301     }
 302 
 303     private void defaultHandleOOMFailure(Exception e) {
 304         e.printStackTrace();
 305         failure();
 306     }
 307 
 308     @SuppressWarnings({ "rawtypes", "unchecked" })
 309     void invoke(String methodName, String methodSig, Value value)
 310             throws Exception {
 311         List args = new ArrayList(1);
 312         args.add(value);
 313         invoke(methodName, methodSig, args, value);
 314     }
 315 
 316     void invoke(String methodName,
 317                 String methodSig,
 318                 @SuppressWarnings("rawtypes") List args,
 319                 Value value) throws Exception {
 320         Method method = findMethod(targetClass, methodName, methodSig);
 321         if ( method == null) {
 322             failure("FAILED: Can't find method: "
 323                     + methodName  + " for class = " + targetClass);
 324             return;
 325         }
 326         invoke(method, args, value);
 327     }
 328 
 329     @SuppressWarnings({ "rawtypes", "unchecked" })
 330     void invoke(Method method, List args, Value value) throws Exception {
 331         thisObject.invokeMethod(mainThread, method, args, 0);
 332         System.out.println("DEBUG: Done invoking method via debugger.");
 333     }
 334 
 335     Value fieldValue(String fieldName) {
 336         Field field = targetClass.fieldByName(fieldName);
 337         return thisObject.getValue(field);
 338     }
 339 
 340     // Determine the pass/fail status on some heuristic and don't fail the
 341     // test if < 3 of the total number of tests (currently 5) fail. This also
 342     // has the nice side effect that all tests are first attempted and only
 343     // all tests ran an overall pass/fail status is determined.
 344     private static void determineOverallTestStatus(OomDebugTest oomTest)
 345                                    throws IOException, FileNotFoundException {
 346         Properties resultProps = new Properties();
 347         if (!RESULT_FILE.exists()) {
 348             RESULT_FILE.createNewFile();
 349         }
 350         FileInputStream fin = null;
 351         try {
 352             fin = new FileInputStream(RESULT_FILE);
 353             resultProps.load(fin);
 354             resultProps.put(oomTest.testMethod,
 355                             Integer.toString(oomTest.failedTests));
 356         } finally {
 357             if (fin != null) {
 358                 fin.close();
 359             }
 360         }
 361         System.out.println("DEBUG: Finished running test '"
 362                            + oomTest.testMethod + "'.");
 363         if (LAST_TEST.equals(oomTest.testMethod)) {
 364             System.out.println("DEBUG: Determining overall test status.");
 365             Set<String> actualTestsRun = new HashSet<String>();
 366             int totalTests = ALL_TESTS.length;
 367             int failedTests = 0;
 368             for (Object key: resultProps.keySet()) {
 369                 actualTestsRun.add((String)key);
 370                 Object propVal = resultProps.get(key);
 371                 int value = Integer.parseInt((String)propVal);
 372                 failedTests += value;
 373             }
 374             if (!ALL_TESTS_SET.equals(actualTestsRun)) {
 375                 String errorMsg = "Test failed! Expected to run tests '"
 376                         + ALL_TESTS_SET + "', but only these were run '"
 377                         + actualTestsRun + "'";
 378                 throw new RuntimeException(errorMsg);
 379             }
 380             if (failedTests >= 3) {
 381                 String errorMsg = "Test failed. Expected < 3 sub-tests to fail "
 382                                   + "for a pass. Got " + failedTests
 383                                   + " failed tests out of " + totalTests + ".";
 384                 throw new RuntimeException(errorMsg);
 385             }
 386             RESULT_FILE.delete();
 387             System.out.println("All " + totalTests + " tests passed.");
 388         } else {
 389             System.out.println("DEBUG: More tests to run. Coninuing.");
 390             FileOutputStream fout = null;
 391             try {
 392                 fout = new FileOutputStream(RESULT_FILE);
 393                 resultProps.store(fout, "Storing results after test "
 394                                          + oomTest.testMethod);
 395             } finally {
 396                 if (fout != null) {
 397                     fout.close();
 398                 }
 399             }
 400         }
 401     }
 402 
 403     public static void main(String[] args) throws Exception {
 404         System.setProperty("test.vm.opts", "-Xmx40m"); // Set debuggee VM option
 405         OomDebugTest oomTest = new OomDebugTest(args);
 406         try {
 407             oomTest.startTests();
 408         } catch (Throwable e) {
 409             System.out.println("DEBUG: Got exception for test run. " + e);
 410             e.printStackTrace();
 411             oomTest.failure();
 412         }
 413         determineOverallTestStatus(oomTest);
 414     }
 415 
 416 }