HeapSpace.java
/*
* Copyright (C) 2019 uwe
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.sw4j.sample.memory.heap;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
/**
*
* @author Uwe Plonus <u.plonus@gmail.com>
*/
public class HeapSpace implements HeapSpaceMBean {
private static final Logger logger = Logger.getLogger(HeapSpace.class.getName());
private static final int DEFAULT_BLOCK_SIZE = 4096;
private static final int DEFAULT_CREATE_INTERVAL = 200;
private static final int DEFAULT_SHORT_LIFETIME = 1000;
private static final int DEFAULT_MEDIUM_LIFETIME = 150000;
private static final int DEFAULT_LONG_LIFETIME = 600000;
private static final double DEFAULT_MEDIUM_PROBABILITY = 15.0;
private static final double DEFAULT_LONG_PROBABILITY = 5.0;
private static final double MAX_PROBABILITY = 100.0;
private boolean stopped = false;
private int blockSize;
private int createInterval;
private int shortLifetime;
private int mediumLifetime;
private int longLifetime;
private double mediumProbability;
private double longProbability;
public static void main(String... args) throws Exception {
new HeapSpace().run(args);
}
public void run(String... args) throws InterruptedException, ExecutionException {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try {
ObjectName name = new ObjectName(HeapSpace.class.getPackage().getName(), "type",
HeapSpace.class.getSimpleName());
mbs.registerMBean(this, name);
} catch (InstanceAlreadyExistsException | MBeanRegistrationException |
MalformedObjectNameException | NotCompliantMBeanException exc) {
logger.log(Level.WARNING, "Problems creating MBean. Running without MBean creation.", exc);
}
PossibleConfiguration config = parseCommandLine(args);
if (config.hasValues()) {
run(config);
} else {
System.out.println(config.getMessage());
}
}
/**
*
* @param config must have values.
* @throws InterruptedException
* @throws ExecutionException
*/
public void run(PossibleConfiguration config) throws InterruptedException, ExecutionException {
if (!config.hasValues()) {
throw new IllegalArgumentException("Need a configuration with values");
}
logger.info("Started HeapSpace");
blockSize = config.getBlockSize();
createInterval = config.getCreate();
shortLifetime = config.getShortLifetime();
mediumLifetime = config.getMediumLifetime();
longLifetime = config.getLongLifetime();
mediumProbability = config.getMediumProbability();
longProbability = config.getLongProbability();
Random rand = new Random();
final ScheduledExecutorService creationScheduler = Executors.newSingleThreadScheduledExecutor();
final ExecutorService hashExecutor = Executors.newFixedThreadPool(4);
final ScheduledExecutorService cleanUpScheduler = Executors.newSingleThreadScheduledExecutor();
Thread.setDefaultUncaughtExceptionHandler((Thread arg0, Throwable arg1) -> {
creationScheduler.shutdown();
hashExecutor.shutdown();
cleanUpScheduler.shutdown();
logger.warning("Exiting after unhandled exception in thread");
arg1.printStackTrace(System.err);
});
while (!stopped) {
ScheduledFuture<Allocator> futureAllocator =
creationScheduler.schedule(() -> new Allocator(1 + rand.nextInt(blockSize)),
rand.nextInt(getCreateInterval()), TimeUnit.MILLISECONDS);
final Allocator allocator = futureAllocator.get();
hashExecutor.submit(() -> {
allocator.calculateHash();
logger.info(String.format("Created allocator with hash %s", allocator.getHash()));
int cleanUpTime = rand.nextInt(shortLifetime);
String cleanUpMessage = "Released short lived hash %s";
double prob = MAX_PROBABILITY * rand.nextDouble();
if (prob >= MAX_PROBABILITY - longProbability) {
cleanUpTime = mediumLifetime + rand.nextInt(longLifetime - mediumLifetime);
cleanUpMessage = "Released long lived hash %s";
} else if (prob >= MAX_PROBABILITY - (longProbability + mediumProbability)) {
cleanUpTime = shortLifetime + rand.nextInt(mediumLifetime - shortLifetime);
cleanUpMessage = "Released medium lived hash %s";
}
final String format = cleanUpMessage;
cleanUpScheduler.schedule(() -> {
logger.info(String.format(format, allocator.getHash()));
}, cleanUpTime, TimeUnit.MILLISECONDS);
return null;
});
}
}
/**
* Parses the command line arguments into a {@code Configuration} object.
*
* @param args the command line arguments
* @return the parsed {@code Configuration} or {@code null} if help was printed.
*/
public PossibleConfiguration parseCommandLine(String... args) {
Options options = new Options();
options.addOption(Option
.builder("h")
.longOpt("help")
.optionalArg(true)
.desc("Print this help")
.build());
options.addOption(Option
.builder("b")
.longOpt("block-size")
.optionalArg(true)
.hasArg(true)
.argName("size")
.desc(String.format("The max block size of memory to reserve (default: %d)", DEFAULT_BLOCK_SIZE))
.build());
options.addOption(Option
.builder("c")
.longOpt("create")
.optionalArg(true)
.hasArg(true)
.argName("interval")
.desc(String.format("The max interval (in ms) between object creation (default: %d)",
DEFAULT_CREATE_INTERVAL))
.build());
options.addOption(Option
.builder("s")
.longOpt("short-time")
.optionalArg(true)
.hasArg(true)
.argName("duration")
.desc(String.format("The max lifetime (in ms) of a short lived object (default: %d)",
DEFAULT_SHORT_LIFETIME))
.build());
options.addOption(Option
.builder("m")
.longOpt("medium-time")
.optionalArg(true)
.hasArg(true)
.argName("duration")
.desc(String.format("The max lifetime (in ms) of a medium lived object (default: %d)",
DEFAULT_MEDIUM_LIFETIME))
.build());
options.addOption(Option
.builder("l")
.longOpt("long-time")
.optionalArg(true)
.hasArg(true)
.argName("duration")
.desc(String.format("The max lifetime (in ms) of a long lived object (default: %d)",
DEFAULT_LONG_LIFETIME))
.build());
options.addOption(Option
.builder()
.longOpt("med-prob")
.optionalArg(true)
.hasArg(true)
.argName("probability")
.desc(String.format("The probability (in %%) to create a medium lived object (default: %3.1f)",
DEFAULT_MEDIUM_PROBABILITY))
.build());
options.addOption(Option
.builder()
.longOpt("long-prob")
.optionalArg(true)
.hasArg(true)
.argName("probability")
.desc(String.format("The probability (in %%) to create a long lived object (default: %3.1f)",
DEFAULT_LONG_PROBABILITY))
.build());
CommandLineParser clParser = new DefaultParser();
CommandLine cl = new CommandLine.Builder().build();
try {
cl = clParser.parse(options, args, true);
} catch (ParseException pex) {
logger.log(Level.WARNING, "Problems while parsing the command line", pex);
}
int block = DEFAULT_BLOCK_SIZE;
if (cl.hasOption("b")) {
try {
block = Integer.parseInt(cl.getOptionValue("b"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse block size. Using default.");
}
}
int create = DEFAULT_CREATE_INTERVAL;
if (cl.hasOption("c")) {
try {
create = Integer.parseInt(cl.getOptionValue("c"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse creation interval. Using default.");
}
}
int shortLife = DEFAULT_SHORT_LIFETIME;
if (cl.hasOption("s")) {
try {
shortLife = Integer.parseInt(cl.getOptionValue("s"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse short lifetime. Using default.");
}
}
int medLife = DEFAULT_MEDIUM_LIFETIME;
if (cl.hasOption("m")) {
try {
medLife = Integer.parseInt(cl.getOptionValue("m"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse medium lifetime. Using default.");
}
}
int longLife = DEFAULT_LONG_LIFETIME;
if (cl.hasOption("l")) {
try {
longLife = Integer.parseInt(cl.getOptionValue("l"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse long lifetime. Using default.");
}
}
double medProb = DEFAULT_MEDIUM_PROBABILITY;
if (cl.hasOption("med-prob")) {
try {
medProb = Double.parseDouble(cl.getOptionValue("med-prob"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse medium lived probability. Using default");
}
}
double longProb = DEFAULT_LONG_PROBABILITY;
if (cl.hasOption("long-prob")) {
try {
longProb = Double.parseDouble(cl.getOptionValue("long-prob"));
} catch (NumberFormatException nfex) {
logger.fine("Cannot parse long lived probability. Using default");
}
}
PossibleConfiguration config;
if (cl.hasOption("help")) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
HelpFormatter helper = new HelpFormatter();
helper.printHelp(pw, 80, "HeapSpace",
"A small application that will (most likely) create an OutOfMemoryError " +
"with message \"Java heap space\"",
options, 1, 2, "", true);
config = new PossibleConfiguration(sw.toString());
} else {
StringBuilder sb = new StringBuilder();
if (block < 1) {
appendNewLineIfNeeded(sb).append("The block size (-b) must be greater than 0.");
}
if (create < 1) {
appendNewLineIfNeeded(sb).append("The creation interval (-c) must be greater than 0.");
}
if (shortLife < 1) {
appendNewLineIfNeeded(sb).append("The short life time (-s) must be greater than 0.");
}
if (medLife < 1) {
appendNewLineIfNeeded(sb).append("The medium life time (-m) must be greater than 0.");
}
if (longLife < 1) {
appendNewLineIfNeeded(sb).append("The long life time (-l) must be greater than 0.");
}
if (medProb < 0.0) {
appendNewLineIfNeeded(sb).append("The medium probability (--med-prob) must be greater than 0.0.");
}
if (longProb < 0.0) {
appendNewLineIfNeeded(sb).append("The long probability (--long-prob) must be greater than 0.0.");
}
if (shortLife > medLife) {
appendNewLineIfNeeded(sb).append("The short life time must be lower than the medium life time.");
}
if (medLife > longLife) {
appendNewLineIfNeeded(sb).append("The medium life time must be lower than the long life time.");
}
if (medProb + longProb > MAX_PROBABILITY) {
appendNewLineIfNeeded(sb).append(String.format("The sum of medium probability and long probability " +
"may not be larger than %4.1f.", MAX_PROBABILITY));
}
if (sb.length() > 0) {
config = new PossibleConfiguration(sb.toString());
} else {
config = new PossibleConfiguration(block, create, shortLife, medLife, longLife, medProb, longProb);
}
}
return config;
}
private StringBuilder appendNewLineIfNeeded(StringBuilder sb) {
if (sb.length() > 0) {
sb.append("\n");
}
return sb;
}
/**
* Stops the creation of new objects.
*/
@Override
public void stop() {
stopped = true;
}
@Override
public int getBlockSize() {
return blockSize;
}
@Override
public void setBlockSize(int blockSize) {
this.blockSize = blockSize;
}
@Override
public void setCreateInterval(int millis) {
createInterval = millis;
}
@Override
public int getCreateInterval() {
return createInterval;
}
@Override
public int getShortLifetime() {
return shortLifetime;
}
@Override
public void setShortLifetime(int shortLifetime) {
this.shortLifetime = shortLifetime;
}
@Override
public int getMediumLifetime() {
return mediumLifetime;
}
@Override
public void setMediumLifetime(int mediumLifetime) {
this.mediumLifetime = mediumLifetime;
}
@Override
public int getLongLifetime() {
return longLifetime;
}
@Override
public void setLongLifetime(int longLifetime) {
this.longLifetime = longLifetime;
}
@Override
public void setMediumProbability(double prob) {
mediumProbability = prob;
}
@Override
public double getMediumProbability() {
return mediumProbability;
}
@Override
public void setLongProbability(double prob) {
longProbability = prob;
}
@Override
public double getLongProbability() {
return longProbability;
}
}