Java G1: мониторинг утечек памяти в производстве

В течение многих лет мы запускали службы Java со скромными размерами кучи, используя +UseParallelOldGC. Теперь мы начинаем развертывать новую услугу, используя большую кучу и сборщик G1. Все идет хорошо.

Для наших служб, которые используют +UseParallelOldGC, мы отслеживаем утечки памяти, просматривая размер старого поколения после сбора и оповещения на пороге. Это работает очень хорошо, и на самом деле спас наш бекон всего две недели назад.

В частности, для +UseParallelOldGC мы делаем следующее:

  • ManagementFactory.getMemoryPoolMXBeans()
  • Найдите результат MemoryPoolMXBean с именем, заканчивающимся на "Old Gen"
  • Сравните getCollectionUsage().getUsed() (если доступно) с помощью getMax()

К сожалению, похоже, что G1 больше не имеет понятия getCollectionUsage().

В принципе, мы хотели бы отслеживать размер кучи G1 после последней смешанной коллекции, которую он хочет делать в смешанном цикле или что-то подобное.

Например, вне VM я был бы доволен awk script, который просто обнаружил, что последний '(mixed)' был следующим, а затем '(young)' и посмотрел, какой был последний размер кучи (например, '1540.0M' 'Heap: 3694.5M(9216.0M)->1540.0M(9216.0M)')

Есть ли способ сделать это внутри виртуальной машины Java?

Ответы

Ответ 1

Да, JVM предоставляет вам достаточно инструментов для получения такой информации для G1. Например, вы можете использовать что-то вроде этого класса, который печатает все сведения о сборках мусора (просто вызовите MemoryUtil.startGCMonitor()):

public class MemoryUtil {

    private static final Set<String> heapRegions;

    static {
        heapRegions = ManagementFactory.getMemoryPoolMXBeans().stream()
                .filter(b -> b.getType() == MemoryType.HEAP)
                .map(MemoryPoolMXBean::getName)
                .collect(Collectors.toSet());
    }

    private static NotificationListener gcHandler = (notification, handback) -> {
        if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
            GarbageCollectionNotificationInfo gcInfo = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
            Map<String, MemoryUsage> memBefore = gcInfo.getGcInfo().getMemoryUsageBeforeGc();
            Map<String, MemoryUsage> memAfter = gcInfo.getGcInfo().getMemoryUsageAfterGc();
            StringBuilder sb = new StringBuilder(250);
            sb.append("[").append(gcInfo.getGcAction()).append(" / ").append(gcInfo.getGcCause())
                    .append(" / ").append(gcInfo.getGcName()).append(" / (");
            appendMemUsage(sb, memBefore);
            sb.append(") -> (");
            appendMemUsage(sb, memAfter);
            sb.append("), ").append(gcInfo.getGcInfo().getDuration()).append(" ms]");
            System.out.println(sb.toString());
        }
    };

    public static void startGCMonitor() {
        for(GarbageCollectorMXBean mBean: ManagementFactory.getGarbageCollectorMXBeans()) {
            ((NotificationEmitter) mBean).addNotificationListener(gcHandler, null, null);
        }
    }

    public static void stopGCMonitor() {
        for(GarbageCollectorMXBean mBean: ManagementFactory.getGarbageCollectorMXBeans()) {
            try {
                ((NotificationEmitter) mBean).removeNotificationListener(gcHandler);
            } catch(ListenerNotFoundException e) {
                // Do nothing
            }
        }
    }

    private static void appendMemUsage(StringBuilder sb, Map<String, MemoryUsage> memUsage) {
        memUsage.entrySet().forEach((entry) -> {
            if (heapRegions.contains(entry.getKey())) {
                sb.append(entry.getKey()).append(" used=").append(entry.getValue().getUsed() >> 10).append("K; ");
            }
        });
    }
}

В этом коде gcInfo.getGcAction() вы получите достаточно информации, чтобы отделить небольшие коллекции от основных/смешанных.

Но есть важное предостережение от использования вашего подхода (с порогом) до G1. Единственная смешанная коллекция в G1 обычно затрагивает только несколько старых регионов генерации - достаточно много, чтобы освободить достаточный объем памяти, но не слишком много, чтобы поддерживать минимальную паузу GC. Итак, после смешанной коллекции в G1 вы не можете быть уверены, что весь ваш мусор ушел. В результате вам нужно найти более сложную стратегию для обнаружения утечек памяти (возможно, на основе частоты сбора, сбора статистики из нескольких коллекций и т.д.).