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