1 /* 2 * Copyright (c) 2003, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* 25 * @test 26 * @bug 4530538 27 * @summary Basic unit test of memory management testing: 28 * 1) setUsageThreshold() and getUsageThreshold() 29 * 2) test low memory detection on the old generation. 30 * 31 * @author Mandy Chung 32 * 33 * @library /lib/testlibrary/ 34 * @modules java.management 35 * @build jdk.testlibrary.* LowMemoryTest MemoryUtil RunUtil 36 * @run main/timeout=600 LowMemoryTest 37 * @requires vm.opt.ExplicitGCInvokesConcurrent != "true" 38 * @requires vm.opt.ExplicitGCInvokesConcurrentAndUnloadsClasses != "true" 39 * @requires vm.opt.DisableExplicitGC != "true" 40 */ 41 42 import java.lang.management.*; 43 import java.util.*; 44 import java.util.concurrent.Phaser; 45 import javax.management.*; 46 import javax.management.openmbean.CompositeData; 47 import jdk.testlibrary.ProcessTools; 48 import jdk.testlibrary.JDKToolFinder; 49 import jdk.testlibrary.Utils; 50 51 public class LowMemoryTest { 52 private static final MemoryMXBean mm = ManagementFactory.getMemoryMXBean(); 53 private static final List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans(); 54 private static final Phaser phaser = new Phaser(2); 55 private static MemoryPoolMXBean mpool = null; 56 private static boolean trace = false; 57 private static boolean testFailed = false; 58 private static final int NUM_TRIGGERS = 5; 59 private static final int NUM_CHUNKS = 2; 60 private static final int YOUNG_GEN_SIZE = 8 * 1024 * 1024; 61 private static long chunkSize; 62 private static final String classMain = "LowMemoryTest$TestMain"; 63 64 /** 65 * Run the test multiple times with different GC versions. 66 * First with default command line specified by the framework. 67 * Then with GC versions specified by the test. 68 */ 69 public static void main(String a[]) throws Throwable { 70 // Use a low young gen size to ensure that the 71 // allocated objects are put in the old gen. 72 final String nmFlag = "-Xmn" + YOUNG_GEN_SIZE; 73 // Using large pages will change the young gen size, 74 // make sure we don't use them for this test. 75 final String lpFlag = "-XX:-UseLargePages"; 76 // Prevent G1 from selecting a large heap region size, 77 // since that would change the young gen size. 78 final String g1Flag = "-XX:G1HeapRegionSize=1m"; 79 80 // Runs the test collecting subprocess I/O while it's running. 81 traceTest(classMain + ", -XX:+UseSerialGC", nmFlag, lpFlag, "-XX:+UseSerialGC"); 82 traceTest(classMain + ", -XX:+UseParallelGC", nmFlag, lpFlag, "-XX:+UseParallelGC"); 83 traceTest(classMain + ", -XX:+UseG1GC", nmFlag, lpFlag, "-XX:+UseG1GC", g1Flag); 84 traceTest(classMain + ", -XX:+UseConcMarkSweepGC", nmFlag, lpFlag, "-XX:+UseConcMarkSweepGC"); 85 } 86 87 /* 88 * Creating command-line for running subprocess JVM: 89 * 90 * JVM command line is like: 91 * {test_jdk}/bin/java {defaultopts} -cp {test.class.path} {testopts} main 92 * 93 * {defaultopts} are the default java options set by the framework. 94 * 95 * @param testOpts java options specified by the test. 96 */ 97 private static List<String> buildCommandLine(String... testOpts) { 98 List<String> opts = new ArrayList<>(); 99 opts.add(JDKToolFinder.getJDKTool("java")); 100 opts.addAll(Arrays.asList(Utils.getTestJavaOpts())); 101 opts.add("-cp"); 102 opts.add(System.getProperty("test.class.path", "test.class.path")); 103 opts.add("-XX:+PrintGCDetails"); 104 opts.addAll(Arrays.asList(testOpts)); 105 opts.add(classMain); 106 107 return opts; 108 } 109 110 /** 111 * Runs LowMemoryTest$TestMain with the passed options and redirects subprocess 112 * standard I/O to the current (parent) process. This provides a trace of what 113 * happens in the subprocess while it is runnning (and before it terminates). 114 * 115 * @param prefixName the prefix string for redirected outputs 116 * @param testOpts java options specified by the test. 117 */ 118 private static void traceTest(String prefixName, 119 String... testOpts) 120 throws Throwable { 121 122 // Building command-line 123 List<String> opts = buildCommandLine(testOpts); 124 125 // We activate all tracing in subprocess 126 opts.add("trace"); 127 128 // Launch separate JVM subprocess 129 String[] optsArray = opts.toArray(new String[0]); 130 ProcessBuilder pb = new ProcessBuilder(optsArray); 131 System.out.println("\n========= Tracing of subprocess " + prefixName + " ========="); 132 Process p = ProcessTools.startProcess(prefixName, pb); 133 134 // Handling end of subprocess 135 try { 136 int exitCode = p.waitFor(); 137 if (exitCode != 0) { 138 throw new RuntimeException( 139 "Subprocess unexpected exit value of [" + exitCode + "]. Expected 0.\n"); 140 } 141 } catch (InterruptedException e) { 142 throw new RuntimeException("Parent process interrupted with exception : \n " + e + " :" ); 143 } 144 145 146 } 147 148 private static volatile boolean listenerInvoked = false; 149 static class SensorListener implements NotificationListener { 150 @Override 151 public void handleNotification(Notification notif, Object handback) { 152 String type = notif.getType(); 153 if (type.equals(MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED) || 154 type.equals(MemoryNotificationInfo. 155 MEMORY_COLLECTION_THRESHOLD_EXCEEDED)) { 156 157 MemoryNotificationInfo minfo = MemoryNotificationInfo. 158 from((CompositeData) notif.getUserData()); 159 160 MemoryUtil.printMemoryNotificationInfo(minfo, type); 161 listenerInvoked = true; 162 } 163 } 164 } 165 166 static class TestListener implements NotificationListener { 167 private boolean isRelaxed = false; 168 private int triggers = 0; 169 private final long[] count = new long[NUM_TRIGGERS * 2]; 170 private final long[] usedMemory = new long[NUM_TRIGGERS * 2]; 171 172 public TestListener() { 173 isRelaxed = ManagementFactory.getRuntimeMXBean().getInputArguments().contains("-XX:+UseConcMarkSweepGC"); 174 } 175 176 @Override 177 public void handleNotification(Notification notif, Object handback) { 178 MemoryNotificationInfo minfo = MemoryNotificationInfo. 179 from((CompositeData) notif.getUserData()); 180 count[triggers] = minfo.getCount(); 181 usedMemory[triggers] = minfo.getUsage().getUsed(); 182 triggers++; 183 } 184 public void checkResult() throws Exception { 185 if (!checkValue(triggers, NUM_TRIGGERS)) { 186 throw new RuntimeException("Unexpected number of triggers = " + 187 triggers + " but expected to be " + NUM_TRIGGERS); 188 } 189 190 for (int i = 0; i < triggers; i++) { 191 if (!checkValue(count[i], i + 1)) { 192 throw new RuntimeException("Unexpected count of" + 193 " notification #" + i + 194 " count = " + count[i] + 195 " but expected to be " + (i+1)); 196 } 197 if (usedMemory[i] < newThreshold) { 198 throw new RuntimeException("Used memory = " + 199 usedMemory[i] + " is less than the threshold = " + 200 newThreshold); 201 } 202 } 203 } 204 205 private boolean checkValue(int value, int target) { 206 return checkValue((long)value, target); 207 } 208 209 private boolean checkValue(long value, int target) { 210 if (!isRelaxed) { 211 return value == target; 212 } else { 213 return value >= target; 214 } 215 } 216 } 217 218 private static long newThreshold; 219 220 private static class TestMain { 221 public static void main(String args[]) throws Exception { 222 if (args.length > 0 && args[0].equals("trace")) { 223 trace = true; 224 } 225 226 // Find the Old generation which supports low memory detection 227 ListIterator iter = pools.listIterator(); 228 while (iter.hasNext()) { 229 MemoryPoolMXBean p = (MemoryPoolMXBean) iter.next(); 230 if (p.getType() == MemoryType.HEAP && 231 p.isUsageThresholdSupported()) { 232 mpool = p; 233 if (trace) { 234 System.out.println("Selected memory pool for low memory " + 235 "detection."); 236 MemoryUtil.printMemoryPool(mpool); 237 } 238 break; 239 } 240 } 241 242 TestListener listener = new TestListener(); 243 SensorListener l2 = new SensorListener(); 244 NotificationEmitter emitter = (NotificationEmitter) mm; 245 emitter.addNotificationListener(listener, null, null); 246 emitter.addNotificationListener(l2, null, null); 247 248 Thread allocator = new AllocatorThread(); 249 Thread sweeper = new SweeperThread(); 250 251 // The chunk size needs to be larger than YOUNG_GEN_SIZE, 252 // otherwise we will get intermittent failures when objects 253 // end up in the young gen instead of the old gen. 254 final long epsilon = 1024; 255 chunkSize = YOUNG_GEN_SIZE + epsilon; 256 257 MemoryUsage mu = mpool.getUsage(); 258 newThreshold = mu.getUsed() + (chunkSize * NUM_CHUNKS); 259 260 // Sanity check. Make sure the new threshold isn't too large. 261 // Tweak the test if this fails. 262 final long headRoom = chunkSize * 2; 263 final long max = mu.getMax(); 264 if (max != -1 && newThreshold > max - headRoom) { 265 throw new RuntimeException("TEST FAILED: newThreshold: " + newThreshold + 266 " is too near the maximum old gen size: " + max + 267 " used: " + mu.getUsed() + " headRoom: " + headRoom); 268 } 269 270 System.out.println("Setting threshold for " + mpool.getName() + 271 " from " + mpool.getUsageThreshold() + " to " + newThreshold + 272 ". Current used = " + mu.getUsed()); 273 274 mpool.setUsageThreshold(newThreshold); 275 276 if (mpool.getUsageThreshold() != newThreshold) { 277 throw new RuntimeException("TEST FAILED: " + 278 "Threshold for Memory pool " + mpool.getName() + 279 "is " + mpool.getUsageThreshold() + " but expected to be" + 280 newThreshold); 281 } 282 283 284 allocator.start(); 285 // Force Allocator start first 286 phaser.arriveAndAwaitAdvance(); 287 sweeper.start(); 288 289 290 try { 291 allocator.join(); 292 // Wait until AllocatorThread's done 293 phaser.arriveAndAwaitAdvance(); 294 sweeper.join(); 295 } catch (InterruptedException e) { 296 System.out.println("Unexpected exception:" + e); 297 testFailed = true; 298 } 299 300 listener.checkResult(); 301 302 if (testFailed) 303 throw new RuntimeException("TEST FAILED."); 304 305 System.out.println(RunUtil.successMessage); 306 } 307 } 308 309 private static void goSleep(long ms) { 310 try { 311 Thread.sleep(ms); 312 } catch (InterruptedException e) { 313 System.out.println("Unexpected exception:" + e); 314 testFailed = true; 315 } 316 } 317 318 private static final List<Object> objectPool = new ArrayList<>(); 319 static class AllocatorThread extends Thread { 320 public void doTask() { 321 int iterations = 0; 322 int numElements = (int) (chunkSize / 4); // minimal object size 323 while (!listenerInvoked || mpool.getUsage().getUsed() < mpool.getUsageThreshold()) { 324 iterations++; 325 if (trace) { 326 System.out.println(" Iteration " + iterations + 327 ": before allocation " + 328 mpool.getUsage().getUsed()); 329 } 330 331 Object[] o = new Object[numElements]; 332 if (iterations <= NUM_CHUNKS) { 333 // only hold a reference to the first NUM_CHUNKS 334 // allocated objects 335 objectPool.add(o); 336 } 337 338 if (trace) { 339 System.out.println(" " + 340 " after allocation " + 341 mpool.getUsage().getUsed()); 342 } 343 goSleep(100); 344 } 345 } 346 @Override 347 public void run() { 348 for (int i = 1; i <= NUM_TRIGGERS; i++) { 349 // Sync with SweeperThread's second phase. 350 phaser.arriveAndAwaitAdvance(); 351 System.out.println("AllocatorThread is doing task " + i + 352 " phase " + phaser.getPhase()); 353 doTask(); 354 // Sync with SweeperThread's first phase. 355 phaser.arriveAndAwaitAdvance(); 356 System.out.println("AllocatorThread done task " + i + 357 " phase " + phaser.getPhase()); 358 if (testFailed) { 359 return; 360 } 361 } 362 } 363 } 364 365 static class SweeperThread extends Thread { 366 private void doTask() { 367 int iterations = 0; 368 if (trace) { 369 System.out.println("SweeperThread clearing allocated objects."); 370 } 371 372 for (; mpool.getUsage().getUsed() >= 373 mpool.getUsageThreshold();) { 374 // clear all allocated objects and invoke GC 375 objectPool.clear(); 376 mm.gc(); 377 378 if (trace) { 379 iterations++; 380 System.out.println("SweeperThread called " + iterations + 381 " time(s) MemoryMXBean.gc()."); 382 } 383 384 goSleep(100); 385 } 386 } 387 388 @Override 389 public void run() { 390 for (int i = 1; i <= NUM_TRIGGERS; i++) { 391 // Sync with AllocatorThread's first phase. 392 phaser.arriveAndAwaitAdvance(); 393 System.out.println("SweeperThread is doing task " + i + 394 " phase " + phaser.getPhase()); 395 396 doTask(); 397 398 listenerInvoked = false; 399 400 // Sync with AllocatorThread's second phase. 401 phaser.arriveAndAwaitAdvance(); 402 System.out.println("SweeperThread done task " + i + 403 " phase " + phaser.getPhase()); 404 if (testFailed) return; 405 } 406 } 407 } 408 }