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