Manually Creating HeapDumps in Java honoring +XX:HeapDumpPath JVM Option

+XX:HeapDumpPath is a JVM-Option that is honored by the HeapDumpOnOutOfMemory JVM-Option.

With it we can define where HeapDumps are written to the Disk.

Why do we need this?

Well, running multiple servers we may want to have them dumped centrally on a mapped network drive specifically for logfiles or dumps. Also maybe we sized the Partition for the JVM-Process too small so that a dump will not be able to be written into it (ok, disk space is cheap, even with a raid, but often we save money at the wrong end).

But if i trigger an heapdump directly (like with jconsole) i often only write the name of the dump into it or want to have it generated. Normally those dumps will now be written into the working directory of the jvm.

To avoid this, we can use the following Code to have it honored and registered as our own MBean that we will use from now on.

net.sjoker.vm.heap.HeapDumperMXBean


package net.sjoker.vm.heap;

import javax.annotation.Nonnull;

/**
* MXBean for Dumping the Heap on Demand. Honors +XX:HeapDumpPath
*
* @author SJoker
*/
public interface HeapDumperMXBean {
  /**
   * Creates a new Heapdump with a timestamped name
   *
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dump();

  /**
   * Creates a new Heapdump with a timestamped name
   *
   * @param liveOnly
   *            if <code>true</code> dumps only live objects
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dump(boolean liveOnly);

  /**
   * Create a Heapdump with custom name
   *
   * @param path
   *            Path and Name of Heapdump
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dump(@Nonnull String path);

  /**
   * Create a Heapdump with custom name
   *
   * @param path
   *            Path and Name of Heapdump
   * @param liveOnly
   *            if <code>true</code> dumps only live objects
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dump(@Nonnull String path, boolean liveOnly);

  /**
   * Create a Heapdump with a timestamped filename and a Prefix for the
   * filename
   *
   * @param prefix
   *            Prefix used before the generated timestamp
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dumpWithPrefix(@Nonnull String prefix);

  /**
   * Create a Heapdump with a timestamped filename and a Prefix for the
   * filename
   *
   * @param prefix
   *            Prefix used before the generated timestamp
   * @param liveOnly
   *            if <code>true</code> dumps only live objects
   * @return Full file path and name to heap dump file
   */
  @Nonnull
  String dumpWithPrefix(@Nonnull String prefix, boolean liveOnly);
}


package net.sjoker.vm.heap;

import java.lang.management.ManagementFactory;

import javax.annotation.Nonnull;

import net.sjoker.vm.internal.heap.HeapDumper;

/**
* Factory that holds a Singleton {@link HeapDumperMXBean}
*
* @author SJoker
*/
public class HeapDumperMXBeanFactory {

  private static final HeapDumperMXBean INSTANCE = HeapDumper
.create(ManagementFactory.getPlatformMBeanServer());

  private HeapDumperMXBeanFactory() {
    throw new IllegalStateException("static only"); //$NON-NLS-1$
  }

  /**
   * Get Runtime Instance of {@link HeapDumperMXBean}
   */
  @Nonnull
  public static HeapDumperMXBean get() {
    return INSTANCE;
  }

}


package net.sjoker.vm.heap;

import java.lang.management.ManagementFactory;

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 com.google.common.base.Throwables;

/**
* Initialize MBean for Heap Dumping
*
* @author SJoker
*/
public class Initializer {

  private Initializer() {
    throw new IllegalStateException("static only"); //$NON-NLS-1$
  }

  public static void init() {
    HeapDumperMXBean heapDumperMBean = HeapDumperMXBeanFactory.get();
    MBeanServer server = ManagementFactory.getPlatformMBeanServer();
    try {
      server.registerMBean(heapDumperMBean, new ObjectName(
"net.sjoker.vm:type=HeapDumper"));
    } catch (InstanceAlreadyExistsException | MBeanRegistrationException
| NotCompliantMBeanException | MalformedObjectNameException e) {
      throw Throwables.propagate(e);
    }
  }

}

package net.sjoker.vm.internal.heap;

/**
* Constants used for Reflection...
*
* @author SJoker
*/
class Constants {

  /**
   * Format for Dateconverstion of timestamp to be used in a filename
   */
  public static final String DATE_FORMAT_FOR_TIMESTAMPED_FILES = "yyyy_MM_dd_HH_mm_ss_SSS"; //$NON-NLS-1$

  /**
   * Fileextension for Heapdumps (hprof)
   */
  public static final String EXTENSION_HEAPDUMP = ".hprof"; //$NON-NLS-1$

  /**
   * Name of HotSpotDiagnostic MBean
   *
   * @see http://download.oracle.com/javase/6/docs/jre
   *      /api/management/extension
   *      /com/sun/management/HotSpotDiagnosticMXBean.html
   */
  public static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"; //$NON-NLS-1$

  /**
   * Methodname to dump the heap on HotSpotDiagnostic MBean
   */
  public static final String OPERATION_DUMP_HEAP = "dumpHeap"; //$NON-NLS-1$

  /**
   * Methodname to get a VMOption on HotSpotDiagnosticMBean
   */
  public static final String OPERATION_GET_VM_OPTION = "getVMOption"; //$NON-NLS-1$

  /**
   * +XX: Option for setting the HeapDumpPath that is normally only used by
   * the JVM for HeapDumpOnOutOfMemory Dums
   *
   * @see http
   *      ://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.
   *      html
   */
  public static final String OPTION_HEAP_DUMP_PATH = "HeapDumpPath"; //$NON-NLS-1$

  private Constants() {
    throw new IllegalStateException("static only"); //$NON-NLS-1$
  }

}


package net.sjoker.vm.internal.heap;

import static net.sjoker.vm.internal.heap.Constants.HOTSPOT_BEAN_NAME;
import static net.sjoker.vm.internal.heap.Constants.OPERATION_DUMP_HEAP;
import static net.sjoker.vm.internal.heap.Constants.OPERATION_GET_VM_OPTION;
import static net.sjoker.vm.internal.heap.Constants.OPTION_HEAP_DUMP_PATH;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.openmbean.CompositeDataSupport;

import net.sjoker.vm.heap.HeapDumperMXBean;

import com.google.common.base.Throwables;

/**
* Implements {@link HeapDumperMXBean} against HotSpotDiagnostic MBean to
* execute HeapDumps honoring +XX:HeapDumpPath Option
*
* @author SJoker
*/
public class HeapDumper implements HeapDumperMXBean {

  private final ObjectName _hotSpotDiagnostic;

  private final MBeanServer _mBeanServer;

  public static HeapDumperMXBean create(MBeanServer mBeanServer) {
    try {
      ObjectName name = new ObjectName(HOTSPOT_BEAN_NAME);
      mBeanServer.getObjectInstance(name); // Try accessing it if it is
                                           // available!
      return new HeapDumper(name, mBeanServer);
    } catch (InstanceNotFoundException | MalformedObjectNameException e) {
      throw Throwables.propagate(e);
    }
  }

  private HeapDumper(ObjectName hotSpotDiagnostic, MBeanServer mBeanServer) {
    _hotSpotDiagnostic = hotSpotDiagnostic;
    _mBeanServer = mBeanServer;
  }

  @Override
  public String dump() {
    return dump(false);
  }

  @Override
  public String dump(boolean liveOnly) {
    return dump0(timestampedName(), liveOnly);
  }

  private String timestampedName() {
    return timestampedName("");
  }

  private String timestampedName(String prefix) {
    String heapDumpPath = heapDumpPath();
    return HeapDumpNameProvider.create(prefix, heapDumpPath);
  }

  private String heapDumpPath() {
    CompositeDataSupport option = getOption(OPTION_HEAP_DUMP_PATH);
    return option == null ? null : optionValue(option);
  }

  private CompositeDataSupport getOption(String optionName) {
    try {
      return (CompositeDataSupport) _mBeanServer.invoke(
 _hotSpotDiagnostic, OPERATION_GET_VM_OPTION,
new Object[] { optionName },
new String[] { String.class.getCanonicalName() });
    } catch (InstanceNotFoundException | ReflectionException
| MBeanException e) {
      throw Throwables.propagate(e);
    }
  }

  private static String optionValue(CompositeDataSupport option) {
    return (String) option.get("value");
  }

  @Override
  public String dump(String path) {
    return dump(path, false);
  }

  @Override
  public String dump(String path, boolean liveOnly) {
    return dump0(path, liveOnly);
  }

  @Override
  public String dumpWithPrefix(String prefix) {
    return dumpWithPrefix(prefix, false);
  }

  @Override
  public String dumpWithPrefix(String prefix, boolean liveOnly) {
    return dump0(timestampedName(prefix), liveOnly);
  }

  private String dump0(String fileName, boolean liveOnly) {
    try {
      _mBeanServer.invoke(_hotSpotDiagnostic, OPERATION_DUMP_HEAP,
new Object[] { fileName, liveOnly },
new String[] { String.class.getCanonicalName(),
boolean.class.getCanonicalName() });
      return fileName;
    } catch (InstanceNotFoundException | ReflectionException
| MBeanException e) {
      throw Throwables.propagate(e);
    }
  }
}


package net.sjoker.vm.internal.heap;

import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.google.common.base.Strings;

/**
* Builds Timestamped name for a Heapdump
*
* @author SJoker
*/
public class HeapDumpNameProvider {

  private static final DateFormat FORMAT = new SimpleDateFormat(
Constants.DATE_FORMAT_FOR_TIMESTAMPED_FILES);

  private HeapDumpNameProvider() {
    throw new IllegalStateException("static only"); //$NON-NLS-1$
  }

  @Nonnull
  public static String create(@Nonnull String prefix,
@Nullable String heapDumpPath) {
    String timestamp = nextTimestamp();
    String fName;
    if (Strings.isNullOrEmpty(heapDumpPath)) {
      fName = prefix + timestamp + Constants.EXTENSION_HEAPDUMP;
    } else {
      fName = pathOnly(heapDumpPath) + prefix + timestamp
+ Constants.EXTENSION_HEAPDUMP;
    }
    return new File(fName).getAbsolutePath();
  }

  private static String nextTimestamp() {
    synchronized (FORMAT) {
      return FORMAT.format(new Date());
    }
  }

  private static String pathOnly(String heapDumpPath) {
    // Allows format with file name and <pid> for replacement.
    // if ends with .hprof we think it is a File and must strip it away!
    if (heapDumpPath.endsWith(Constants.EXTENSION_HEAPDUMP)) {
      int lastIndexOf = heapDumpPath.lastIndexOf(File.separatorChar);
      if (lastIndexOf != -1) {
         return heapDumpPath.substring(0, lastIndexOf - 1);
      } else {
        return heapDumpPath;
      }
    } else {
      return heapDumpPath;
    }
  }

}