Автоматическое обнаружение утечки памяти в Java

Я думал об автоматическом обнаружении утечки памяти для Java-программы. Основным алгоритмом является создание JUnits, которые содержат следующую логику:

Call System.gc() several times
Determine initial heap memory consumption using either Runtime class or JMX
Loop 
    Do something that exercises program under test
End loop

Call System.gc() several times
Determine final heap memory consumption
Compare initial and final memory numbers

Цикл используется для проверки того, что память ползает с небольшими приращениями.

Необходимо различать ожидаемые и неожиданные увеличения использования памяти.

Это не действительно unit test. Но структура JUnit удобна в использовании.

Считаете ли вы, что этот подход действительно? Считаете ли вы, что этот подход будет успешным при идентификации утечек памяти? Вы когда-нибудь делали что-то подобное?

Ответы

Ответ 1

Я разработал простую структуру unit test для утечек памяти, которая надежно работала для меня. Основная идея заключается в том, чтобы создать слабую ссылку на объект, который должен быть собран в мусор, выполнить тест, выполнить полный GC, а затем проверить, что слабая ссылка была очищена.

Вот довольно типичный регрессионный тест с использованием моей рамки:

public void testDS00032554() throws Exception {
  Project testProject = getTestProject();
  MemoryLeakVerifier verifier = new MemoryLeakVerifier(new RuntimeTestAction(getTestClassMap()));
  testProject.close();
  verifier.assertGarbageCollected("RuntimeTestAction should be garbage collected when project closed");
}

Здесь есть несколько вещей:

  • Очень важно, чтобы объект, который вы хотите собрать, не должен храниться в переменной в unit test, поскольку он будет сохранен в конце вашего теста.
  • Это полезный метод для регрессионных тестов, в которых сообщалось о утечке, и вы знаете, какой объект должен быть удален.
  • Одна из проблем с этим подходом заключается в том, что трудно определить почему тест не прошел. На этом этапе вам понадобится профайлер памяти (я частично отношусь к YourKit). Однако IMO по-прежнему полезно проводить регрессионные тесты, чтобы утечки не могли быть случайно возвращены в будущем.
  • Я столкнулся с некоторыми проблемами с потоками, когда не все ссылки сразу очищаются, поэтому метод теперь пытается выполнить GC несколько раз перед сбоем (как описано в этой статье: подсказка для Java 130: знаете ли вы свой размер данных?)

Здесь полный класс помощника, если вы хотите попробовать:

/**
 * A simple utility class that can verify that an object has been successfully garbage collected.
 */
public class MemoryLeakVerifier {
private static final int MAX_GC_ITERATIONS = 50;
private static final int GC_SLEEP_TIME     = 100;

private final WeakReference reference;

public MemoryLeakVerifier(Object object) {
    this.reference = new WeakReference(object);
}

public Object getObject() {
    return reference.get();
}

/**
 * Attempts to perform a full garbage collection so that all weak references will be removed. Usually only
 * a single GC is required, but there have been situations where some unused memory is not cleared up on the
 * first pass. This method performs a full garbage collection and then validates that the weak reference
 * now has been cleared. If it hasn't then the thread will sleep for 50 milliseconds and then retry up to
 * 10 more times. If after this the object still has not been collected then the assertion will fail.
 *
 * Based upon the method described in: http://www.javaworld.com/javaworld/javatips/jw-javatip130.html
 */
public void assertGarbageCollected(String name) {
    Runtime runtime = Runtime.getRuntime();
    for (int i = 0; i < MAX_GC_ITERATIONS; i++) {
        runtime.runFinalization();
        runtime.gc();
        if (getObject() == null)
            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 (InterruptedException e) {
            // Ignore any interrupts and just try again...
        } catch (InvocationTargetException e) {
            // Ignore any interrupts and just try again...
        }
    }
    PanteroTestCase.assertNull(name + ": object should not exist after " + MAX_GC_ITERATIONS + " collections", getObject());
}

}

Ответ 2

Вы не можете сделать это с помощью java. Сборщик мусора будет работать, когда он определит, что это необходимо. Кроме того, он может "освобождать" память, чтобы ее можно было повторно использовать, но это не значит, что она освободит блок.

Ответ 3

Это не значимый подход в Java. System.gc() не дает вам никаких разумных гарантий и даже если вы убедите себя, что есть проблема, этот подход не поможет вам найти эту проблему.

Вместо этого вы должны использовать профайлер памяти. Если вы не хотите платить за профессиональный, вы можете попробовать jvisualvm, который установлен вместе с JVM.

Ответ 4

По крайней мере, вам нужно сначала выполнить тест перед тем, как выполнить свой тест, чтобы собрать одноразовые объекты, которые должны быть созданы только один раз. Итак:

test()
checkMem()
test()
checkMem()
compareIfMemUsageHasNotIncreased()