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 }