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