View Javadoc
1   /*
2    * Copyright (C) 2019 uwe
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package org.sw4j.sample.memory.heap;
18  
19  import java.io.PrintWriter;
20  import java.io.StringWriter;
21  import java.lang.management.ManagementFactory;
22  import java.util.Random;
23  import java.util.concurrent.ExecutionException;
24  import java.util.concurrent.ExecutorService;
25  import java.util.concurrent.Executors;
26  import java.util.concurrent.ScheduledExecutorService;
27  import java.util.concurrent.ScheduledFuture;
28  import java.util.concurrent.TimeUnit;
29  import java.util.logging.Level;
30  import java.util.logging.Logger;
31  import javax.management.InstanceAlreadyExistsException;
32  import javax.management.MBeanRegistrationException;
33  import javax.management.MBeanServer;
34  import javax.management.MalformedObjectNameException;
35  import javax.management.NotCompliantMBeanException;
36  import javax.management.ObjectName;
37  import org.apache.commons.cli.CommandLine;
38  import org.apache.commons.cli.CommandLineParser;
39  import org.apache.commons.cli.DefaultParser;
40  import org.apache.commons.cli.HelpFormatter;
41  import org.apache.commons.cli.Option;
42  import org.apache.commons.cli.Options;
43  import org.apache.commons.cli.ParseException;
44  
45  /**
46   *
47   * @author Uwe Plonus &lt;u.plonus@gmail.com&gt;
48   */
49  public class HeapSpace implements HeapSpaceMBean {
50  
51      private static final Logger logger = Logger.getLogger(HeapSpace.class.getName());
52  
53      private static final int DEFAULT_BLOCK_SIZE = 4096;
54  
55      private static final int DEFAULT_CREATE_INTERVAL = 200;
56  
57      private static final int DEFAULT_SHORT_LIFETIME = 1000;
58  
59      private static final int DEFAULT_MEDIUM_LIFETIME = 150000;
60  
61      private static final int DEFAULT_LONG_LIFETIME = 600000;
62  
63      private static final double DEFAULT_MEDIUM_PROBABILITY = 15.0;
64  
65      private static final double DEFAULT_LONG_PROBABILITY = 5.0;
66  
67      private static final double MAX_PROBABILITY = 100.0;
68  
69      private boolean stopped = false;
70  
71      private int blockSize;
72  
73      private int createInterval;
74  
75      private int shortLifetime;
76  
77      private int mediumLifetime;
78  
79      private int longLifetime;
80  
81      private double mediumProbability;
82  
83      private double longProbability;
84  
85      public static void main(String... args) throws Exception {
86          new HeapSpace().run(args);
87      }
88  
89      public void run(String... args) throws InterruptedException, ExecutionException {
90          MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
91          try {
92              ObjectName name = new ObjectName(HeapSpace.class.getPackage().getName(), "type",
93                      HeapSpace.class.getSimpleName());
94              mbs.registerMBean(this, name);
95          } catch (InstanceAlreadyExistsException | MBeanRegistrationException |
96                  MalformedObjectNameException | NotCompliantMBeanException exc) {
97              logger.log(Level.WARNING, "Problems creating MBean. Running without MBean creation.", exc);
98          }
99          PossibleConfiguration config = parseCommandLine(args);
100         if (config.hasValues()) {
101             run(config);
102         } else {
103             System.out.println(config.getMessage());
104         }
105     }
106 
107     /**
108      *
109      * @param config must have values.
110      * @throws InterruptedException
111      * @throws ExecutionException 
112      */
113     public void run(PossibleConfiguration config) throws InterruptedException, ExecutionException {
114         if (!config.hasValues()) {
115             throw new IllegalArgumentException("Need a configuration with values");
116         }
117         logger.info("Started HeapSpace");
118 
119         blockSize = config.getBlockSize();
120         createInterval = config.getCreate();
121         shortLifetime = config.getShortLifetime();
122         mediumLifetime = config.getMediumLifetime();
123         longLifetime = config.getLongLifetime();
124         mediumProbability = config.getMediumProbability();
125         longProbability = config.getLongProbability();
126 
127         Random rand = new Random();
128         final ScheduledExecutorService creationScheduler = Executors.newSingleThreadScheduledExecutor();
129         final ExecutorService hashExecutor = Executors.newFixedThreadPool(4);
130         final ScheduledExecutorService cleanUpScheduler = Executors.newSingleThreadScheduledExecutor();
131         Thread.setDefaultUncaughtExceptionHandler((Thread arg0, Throwable arg1) -> {
132             creationScheduler.shutdown();
133             hashExecutor.shutdown();
134             cleanUpScheduler.shutdown();
135             logger.warning("Exiting after unhandled exception in thread");
136             arg1.printStackTrace(System.err);
137         });
138         while (!stopped) {
139             ScheduledFuture<Allocator> futureAllocator =
140                     creationScheduler.schedule(() -> new Allocator(1 + rand.nextInt(blockSize)),
141                             rand.nextInt(getCreateInterval()), TimeUnit.MILLISECONDS);
142             final Allocator allocator = futureAllocator.get();
143             hashExecutor.submit(() -> {
144                 allocator.calculateHash();
145                 logger.info(String.format("Created allocator with hash  %s", allocator.getHash()));
146                 int cleanUpTime = rand.nextInt(shortLifetime);
147                 String cleanUpMessage = "Released short lived hash  %s";
148                 double prob = MAX_PROBABILITY * rand.nextDouble();
149                 if (prob >= MAX_PROBABILITY - longProbability) {
150                     cleanUpTime = mediumLifetime + rand.nextInt(longLifetime - mediumLifetime);
151                     cleanUpMessage = "Released long lived hash   %s";
152                 } else if (prob >= MAX_PROBABILITY - (longProbability + mediumProbability)) {
153                     cleanUpTime = shortLifetime + rand.nextInt(mediumLifetime - shortLifetime);
154                     cleanUpMessage = "Released medium lived hash %s";
155                 }
156                 final String format = cleanUpMessage;
157                 cleanUpScheduler.schedule(() -> {
158                     logger.info(String.format(format, allocator.getHash()));
159                 }, cleanUpTime, TimeUnit.MILLISECONDS);
160                 return null;
161             });
162         }
163     }
164 
165     /**
166      * Parses the command line arguments into a {@code Configuration} object.
167      *
168      * @param args the command line arguments
169      * @return the parsed {@code Configuration} or {@code null} if help was printed.
170      */
171     public PossibleConfiguration parseCommandLine(String... args) {
172         Options options = new Options();
173 
174         options.addOption(Option
175                 .builder("h")
176                 .longOpt("help")
177                 .optionalArg(true)
178                 .desc("Print this help")
179                 .build());
180 
181         options.addOption(Option
182                 .builder("b")
183                 .longOpt("block-size")
184                 .optionalArg(true)
185                 .hasArg(true)
186                 .argName("size")
187                 .desc(String.format("The max block size of memory to reserve (default: %d)", DEFAULT_BLOCK_SIZE))
188                 .build());
189 
190         options.addOption(Option
191                 .builder("c")
192                 .longOpt("create")
193                 .optionalArg(true)
194                 .hasArg(true)
195                 .argName("interval")
196                 .desc(String.format("The max interval (in ms) between object creation (default: %d)",
197                         DEFAULT_CREATE_INTERVAL))
198                 .build());
199 
200         options.addOption(Option
201                 .builder("s")
202                 .longOpt("short-time")
203                 .optionalArg(true)
204                 .hasArg(true)
205                 .argName("duration")
206                 .desc(String.format("The max lifetime (in ms) of a short lived object (default: %d)",
207                         DEFAULT_SHORT_LIFETIME))
208                 .build());
209 
210         options.addOption(Option
211                 .builder("m")
212                 .longOpt("medium-time")
213                 .optionalArg(true)
214                 .hasArg(true)
215                 .argName("duration")
216                 .desc(String.format("The max lifetime (in ms) of a medium lived object (default: %d)",
217                         DEFAULT_MEDIUM_LIFETIME))
218                 .build());
219 
220         options.addOption(Option
221                 .builder("l")
222                 .longOpt("long-time")
223                 .optionalArg(true)
224                 .hasArg(true)
225                 .argName("duration")
226                 .desc(String.format("The max lifetime (in ms) of a long lived object (default: %d)",
227                         DEFAULT_LONG_LIFETIME))
228                 .build());
229 
230         options.addOption(Option
231                 .builder()
232                 .longOpt("med-prob")
233                 .optionalArg(true)
234                 .hasArg(true)
235                 .argName("probability")
236                 .desc(String.format("The probability (in %%) to create a medium lived object (default: %3.1f)",
237                         DEFAULT_MEDIUM_PROBABILITY))
238                 .build());
239 
240         options.addOption(Option
241                 .builder()
242                 .longOpt("long-prob")
243                 .optionalArg(true)
244                 .hasArg(true)
245                 .argName("probability")
246                 .desc(String.format("The probability (in %%) to create a long lived object (default: %3.1f)",
247                         DEFAULT_LONG_PROBABILITY))
248                 .build());
249 
250         CommandLineParser clParser = new DefaultParser();
251         CommandLine cl = new CommandLine.Builder().build();
252         try {
253             cl = clParser.parse(options, args, true);
254         } catch (ParseException pex) {
255             logger.log(Level.WARNING, "Problems while parsing the command line", pex);
256         }
257 
258         int block = DEFAULT_BLOCK_SIZE;
259         if (cl.hasOption("b")) {
260             try {
261                 block = Integer.parseInt(cl.getOptionValue("b"));
262             } catch (NumberFormatException nfex) {
263                 logger.fine("Cannot parse block size. Using default.");
264             }
265         }
266 
267         int create = DEFAULT_CREATE_INTERVAL;
268         if (cl.hasOption("c")) {
269             try {
270                 create = Integer.parseInt(cl.getOptionValue("c"));
271             } catch (NumberFormatException nfex) {
272                 logger.fine("Cannot parse creation interval. Using default.");
273             }
274         }
275 
276         int shortLife = DEFAULT_SHORT_LIFETIME;
277         if (cl.hasOption("s")) {
278             try {
279                 shortLife = Integer.parseInt(cl.getOptionValue("s"));
280             } catch (NumberFormatException nfex) {
281                 logger.fine("Cannot parse short lifetime. Using default.");
282             }
283         }
284 
285         int medLife = DEFAULT_MEDIUM_LIFETIME;
286         if (cl.hasOption("m")) {
287             try {
288                 medLife = Integer.parseInt(cl.getOptionValue("m"));
289             } catch (NumberFormatException nfex) {
290                 logger.fine("Cannot parse medium lifetime. Using default.");
291             }
292         }
293 
294         int longLife = DEFAULT_LONG_LIFETIME;
295         if (cl.hasOption("l")) {
296             try {
297                 longLife = Integer.parseInt(cl.getOptionValue("l"));
298             } catch (NumberFormatException nfex) {
299                 logger.fine("Cannot parse long lifetime. Using default.");
300             }
301         }
302 
303         double medProb = DEFAULT_MEDIUM_PROBABILITY;
304         if (cl.hasOption("med-prob")) {
305             try {
306                 medProb = Double.parseDouble(cl.getOptionValue("med-prob"));
307             } catch (NumberFormatException nfex) {
308                 logger.fine("Cannot parse medium lived probability. Using default");
309             }
310         }
311 
312         double longProb = DEFAULT_LONG_PROBABILITY;
313         if (cl.hasOption("long-prob")) {
314             try {
315                 longProb = Double.parseDouble(cl.getOptionValue("long-prob"));
316             } catch (NumberFormatException nfex) {
317                 logger.fine("Cannot parse long lived probability. Using default");
318             }
319         }
320 
321         PossibleConfiguration config;
322         if (cl.hasOption("help")) {
323             StringWriter sw = new StringWriter();
324             PrintWriter pw = new PrintWriter(sw);
325             HelpFormatter helper = new HelpFormatter();
326             helper.printHelp(pw, 80, "HeapSpace",
327                     "A small application that will (most likely) create an OutOfMemoryError " +
328                             "with message \"Java heap space\"",
329                     options, 1, 2, "", true);
330             config = new PossibleConfiguration(sw.toString());
331         } else {
332             StringBuilder sb = new StringBuilder();
333             if (block < 1) {
334                 appendNewLineIfNeeded(sb).append("The block size (-b) must be greater than 0.");
335             }
336             if (create < 1) {
337                 appendNewLineIfNeeded(sb).append("The creation interval (-c) must be greater than 0.");
338             }
339             if (shortLife < 1) {
340                 appendNewLineIfNeeded(sb).append("The short life time (-s) must be greater than 0.");
341             }
342             if (medLife < 1) {
343                 appendNewLineIfNeeded(sb).append("The medium life time (-m) must be greater than 0.");
344             }
345             if (longLife < 1) {
346                 appendNewLineIfNeeded(sb).append("The long life time (-l) must be greater than 0.");
347             }
348             if (medProb < 0.0) {
349                 appendNewLineIfNeeded(sb).append("The medium probability (--med-prob) must be greater than 0.0.");
350             }
351             if (longProb < 0.0) {
352                 appendNewLineIfNeeded(sb).append("The long probability (--long-prob) must be greater than 0.0.");
353             }
354             if (shortLife > medLife) {
355                 appendNewLineIfNeeded(sb).append("The short life time must be lower than the medium life time.");
356             }
357             if (medLife > longLife) {
358                 appendNewLineIfNeeded(sb).append("The medium life time must be lower than the long life time.");
359             }
360             if (medProb + longProb > MAX_PROBABILITY) {
361                 appendNewLineIfNeeded(sb).append(String.format("The sum of medium probability and long probability " +
362                         "may not be larger than %4.1f.", MAX_PROBABILITY));
363             }
364             if (sb.length() > 0) {
365                 config = new PossibleConfiguration(sb.toString());
366             } else {
367                 config = new PossibleConfiguration(block, create, shortLife, medLife, longLife, medProb, longProb);
368             }
369         }
370 
371         return config;
372     }
373 
374     private StringBuilder appendNewLineIfNeeded(StringBuilder sb) {
375         if (sb.length() > 0) {
376             sb.append("\n");
377         }
378         return sb;
379     }
380 
381     /**
382      * Stops the creation of new objects.
383      */
384     @Override
385     public void stop() {
386         stopped = true;
387     }
388 
389     @Override
390     public int getBlockSize() {
391         return blockSize;
392     }
393 
394     @Override
395     public void setBlockSize(int blockSize) {
396         this.blockSize = blockSize;
397     }
398 
399     @Override
400     public void setCreateInterval(int millis) {
401         createInterval = millis;
402     }
403 
404     @Override
405     public int getCreateInterval() {
406         return createInterval;
407     }
408 
409     @Override
410     public int getShortLifetime() {
411         return shortLifetime;
412     }
413 
414     @Override
415     public void setShortLifetime(int shortLifetime) {
416         this.shortLifetime = shortLifetime;
417     }
418 
419     @Override
420     public int getMediumLifetime() {
421         return mediumLifetime;
422     }
423 
424     @Override
425     public void setMediumLifetime(int mediumLifetime) {
426         this.mediumLifetime = mediumLifetime;
427     }
428 
429     @Override
430     public int getLongLifetime() {
431         return longLifetime;
432     }
433 
434     @Override
435     public void setLongLifetime(int longLifetime) {
436         this.longLifetime = longLifetime;
437     }
438 
439     @Override
440     public void setMediumProbability(double prob) {
441         mediumProbability = prob;
442     }
443 
444     @Override
445     public double getMediumProbability() {
446         return mediumProbability;
447     }
448 
449     @Override
450     public void setLongProbability(double prob) {
451         longProbability = prob;
452     }
453 
454     @Override
455     public double getLongProbability() {
456         return longProbability;
457     }
458 
459 }