Skip to content

Commit

Permalink
Add heap-dump to MemoryLeakVerifier functionality
Browse files Browse the repository at this point in the history
Adjust handling of InterruptedException to return quickly
  • Loading branch information
centic9 committed Apr 29, 2016
1 parent adc3809 commit 556075f
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 12 deletions.
47 changes: 47 additions & 0 deletions src/main/java/org/dstadler/commons/testing/HeapDump.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.dstadler.commons.testing;

import com.sun.management.HotSpotDiagnosticMXBean;

import java.io.IOException;
import java.lang.management.ManagementFactory;

public class HeapDump {
// This is the name of the HotSpot Diagnostic MBean
private static final String HOTSPOT_BEAN_NAME =
"com.sun.management:type=HotSpotDiagnostic";

// field to store the hotspot diagnostic MBean
private static volatile HotSpotDiagnosticMXBean hotspotMBean;

/**
* Call this method from your application whenever you
* want to dump the heap snapshot into a file.
*
* @param fileName name of the heap dump file
* @param live flag that tells whether to dump
* only the live objects
*/
public static void dumpHeap(String fileName, boolean live) throws IOException {
// initialize hotspot diagnostic MBean
initHotspotMBean();
hotspotMBean.dumpHeap(fileName, live);
}

// initialize the hotspot diagnostic MBean field
private static void initHotspotMBean() throws IOException {
if (hotspotMBean == null) {
synchronized (HeapDump.class) {
if (hotspotMBean == null) {
hotspotMBean = getHotspotMBean();
}
}
}
}

// get the hotspot diagnostic MBean from the
// platform MBean server
private static HotSpotDiagnosticMXBean getHotspotMBean() throws IOException {
return ManagementFactory.newPlatformMXBeanProxy(ManagementFactory.getPlatformMBeanServer(),
HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean.class);
}
}
42 changes: 30 additions & 12 deletions src/main/java/org/dstadler/commons/testing/MemoryLeakVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.Assert.assertNull;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -28,17 +29,27 @@ public void someTest() {
* This will verify at the end of the test if the object is actually removed by the
* garbage collector or if it lingers in memory for some reason.
*
* By default, a heap dump will be written to the file 'MemoryLeakVerifier.hprof' in the
* current directory. This can be disable via setHeapDump(false)
*
* Idea taken from http://stackoverflow.com/a/7410460/411846
*/
public class MemoryLeakVerifier {
private static final int MAX_GC_ITERATIONS = 50;
private static final int GC_SLEEP_TIME = 100;

protected static final String HEAP_DUMP_FILE_NAME = "MemoryLeakVerifier.hprof";

private final List<WeakReference<Object>> references = new ArrayList<>();
private boolean dumpHeap = true;

public MemoryLeakVerifier() {
}

public void setHeapDump(boolean dumpHeap) {
this.dumpHeap = dumpHeap;
}

public void addObject(Object object) {
references.add(new WeakReference<>(object));
}
Expand All @@ -58,16 +69,19 @@ public void assertGarbageCollected() {

/**
* Used only for testing the class itself where we would like to fail faster than 5 seconds
* @param name
* @param maxIterations
* @param maxIterations The number of times a GC will be invoked until a possible memory leak is reported
*/
void assertGarbageCollected(int maxIterations) {
for(WeakReference<Object> ref : references) {
assertGarbageCollected(ref, maxIterations);
try {
for(WeakReference<Object> ref : references) {
assertGarbageCollected(ref, maxIterations, dumpHeap);
}
} catch (InterruptedException e) {
// just ensure that we quickly return when the thread is interrupted
}
}

private static void assertGarbageCollected(WeakReference<Object> ref, int maxIterations) {
private static void assertGarbageCollected(WeakReference<Object> ref, int maxIterations, boolean dumpHeap) throws InterruptedException {
Runtime runtime = Runtime.getRuntime();
for (int i = 0; i < maxIterations; i++) {
runtime.runFinalization();
Expand All @@ -76,15 +90,19 @@ private static void assertGarbageCollected(WeakReference<Object> ref, int maxIte
break;

// Pause for a while and then go back around the loop to try again...
try {
//EventQueue.invokeAndWait(Procedure.NoOp); // Wait for the AWT event queue to have completed processing
Thread.sleep(GC_SLEEP_TIME);
} catch (@SuppressWarnings("unused") InterruptedException e) {
// Ignore any interrupts and just try again...
}
//EventQueue.invokeAndWait(Procedure.NoOp); // Wait for the AWT event queue to have completed processing
Thread.sleep(GC_SLEEP_TIME);
}

assertNull("Object should not exist after " + MAX_GC_ITERATIONS + " collections, but still had: " + ref.get(),
if(dumpHeap && ref.get() != null) {
// dumping heap
try {
HeapDump.dumpHeap(HEAP_DUMP_FILE_NAME, true);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
assertNull("Object should not exist after " + MAX_GC_ITERATIONS + " collections, but still had: " + ref.get() + (dumpHeap ? ", a heapdump was written to " + HEAP_DUMP_FILE_NAME : ""),
ref.get());
}
}
50 changes: 50 additions & 0 deletions src/test/java/org/dstadler/commons/testing/HeapDumpTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.dstadler.commons.testing;

import org.junit.Test;

import java.io.File;
import java.io.IOException;

import static org.junit.Assert.*;

public class HeapDumpTest {
@Test
public void testDumpHeap() throws IOException {
File file = File.createTempFile("HeapDumpTest", ".hprof");
assertTrue(file.delete());
assertFalse(file.exists());

try {
HeapDump.dumpHeap(file.getAbsolutePath(), true);
} finally {
assertTrue(file.exists());
assertTrue(file.delete());
}

// a second time to cover singleton
try {
HeapDump.dumpHeap(file.getAbsolutePath(), true);
} finally {
assertTrue(file.exists());
assertTrue(file.delete());
}
}

@Test
public void testDumpHeapFailsOnWrongFilename() throws IOException {
File file = File.createTempFile("HeapDumpTest", ".hprof");
assertTrue(file.delete());
assertFalse(file.exists());
assertTrue(file.mkdirs());

try {
HeapDump.dumpHeap(file.getAbsolutePath(), true);
fail("Should fail to write the dump in this case, tried for " + file.getAbsolutePath());
} catch (IOException e) {
// expected in this case
} finally {
assertTrue(file.exists());
assertTrue(file.delete());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

import org.junit.Test;

import java.io.File;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class MemoryLeakVerifierTest {
@Test
public void testNoMemoryLeak() {
Expand All @@ -17,13 +22,37 @@ public void testWithMemoryLeak() {
Object obj = new Object();

MemoryLeakVerifier verifier = new MemoryLeakVerifier();
verifier.setHeapDump(false);

verifier.addObject(obj);

try {
verifier.assertGarbageCollected(3);
fail("Should report a memory leak here");
} catch (AssertionError e) {
TestHelpers.assertContains(e, "Object should not exist");
}
}

@Test
public void testWithMemoryLeakAndHeapDump() {
Object obj = new Object();

MemoryLeakVerifier verifier = new MemoryLeakVerifier();
verifier.setHeapDump(true);

verifier.addObject(obj);

try {
verifier.assertGarbageCollected(3);
fail("Should report a memory leak here");
} catch (AssertionError e) {
TestHelpers.assertContains(e, "Object should not exist");

final File heapDumpFile = new File(MemoryLeakVerifier.HEAP_DUMP_FILE_NAME);

assertTrue("HeapDumpFile was not found at " + heapDumpFile.getAbsolutePath(), heapDumpFile.exists());
assertTrue("HeapDumpFile at " + heapDumpFile.getAbsolutePath() + " could not be deleted", heapDumpFile.delete());
}
}
}

0 comments on commit 556075f

Please sign in to comment.