/** * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.netflix.spectator.gc; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Spectator; import com.netflix.spectator.api.Timer; import com.netflix.spectator.impl.Preconditions; import com.sun.management.GarbageCollectionNotificationInfo; import com.sun.management.GcInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.management.ListenerNotFoundException; import javax.management.Notification; import javax.management.NotificationEmitter; import javax.management.NotificationListener; import javax.management.openmbean.CompositeData; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryUsage; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * Logger to collect GC notifcation events. */ public final class GcLogger { private static final Logger LOGGER = LoggerFactory.getLogger(GcLogger.class); // One major GC per hour would require 168 for a week // One minor GC per minute would require 180 for three hours private static final int BUFFER_SIZE = 256; // Max size of old generation memory pool private static final AtomicLong MAX_DATA_SIZE = Spectator.globalRegistry().gauge("jvm.gc.maxDataSize", new AtomicLong(0L)); // Size of old generation memory pool after a full GC private static final AtomicLong LIVE_DATA_SIZE = Spectator.globalRegistry().gauge("jvm.gc.liveDataSize", new AtomicLong(0L)); // Incremented for any positive increases in the size of the old generation memory pool // before GC to after GC private static final Counter PROMOTION_RATE = Spectator.globalRegistry().counter("jvm.gc.promotionRate"); // Incremented for the increase in the size of the young generation memory pool after one GC // to before the next private static final Counter ALLOCATION_RATE = Spectator.globalRegistry().counter("jvm.gc.allocationRate"); // Pause time due to GC event private static final Id PAUSE_TIME = Spectator.globalRegistry().createId("jvm.gc.pause"); // Time spent in concurrent phases of GC private static final Id CONCURRENT_PHASE_TIME = Spectator.globalRegistry().createId("jvm.gc.concurrentPhaseTime"); private final long jvmStartTime; private final ConcurrentHashMap<String, CircularBuffer<GcEvent>> gcLogs = new ConcurrentHashMap<>(); private long youngGenSizeAfter = 0L; private String youngGenPoolName = null; private String oldGenPoolName = null; private GcNotificationListener notifListener = null; private GcEventListener eventListener = null; /** Create a new instance. */ public GcLogger() { jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime(); for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { CircularBuffer<GcEvent> buffer = new CircularBuffer<>(BUFFER_SIZE); gcLogs.put(mbean.getName(), buffer); } for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { if (HelperFunctions.isYoungGenPool(mbean.getName())) { youngGenPoolName = mbean.getName(); } if (HelperFunctions.isOldGenPool(mbean.getName())) { oldGenPoolName = mbean.getName(); } } } /** * Start collecting data about GC events. * * @param listener * If not null, the listener will be called with the event objects after metrics and the * log buffer is updated. */ public synchronized void start(GcEventListener listener) { // TODO: this class has a bad mix of static fields used from an instance of the class. For now // this has been changed not to throw to make the dependency injection use-cases work. A // more general refactor of the GcLogger class is needed. if (notifListener != null) { LOGGER.warn("logger already started"); return; } eventListener = listener; notifListener = new GcNotificationListener(); for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { if (mbean instanceof NotificationEmitter) { final NotificationEmitter emitter = (NotificationEmitter) mbean; emitter.addNotificationListener(notifListener, null, null); } } } /** Stop collecting GC events. */ public synchronized void stop() { Preconditions.checkState(notifListener != null, "logger has not been started"); for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { if (mbean instanceof NotificationEmitter) { final NotificationEmitter emitter = (NotificationEmitter) mbean; try { emitter.removeNotificationListener(notifListener); } catch (ListenerNotFoundException e) { LOGGER.warn("could not remove gc listener", e); } } } notifListener = null; } /** Return the current set of GC events in the in-memory log. */ public List<GcEvent> getLogs() { final List<GcEvent> logs = new ArrayList<>(); for (CircularBuffer<GcEvent> buffer : gcLogs.values()) { logs.addAll(buffer.toList()); } Collections.sort(logs, GcEvent.REVERSE_TIME_ORDER); return logs; } private void updateMetrics(String name, GcInfo info) { final Map<String, MemoryUsage> before = info.getMemoryUsageBeforeGc(); final Map<String, MemoryUsage> after = info.getMemoryUsageAfterGc(); if (oldGenPoolName != null) { final long oldBefore = before.get(oldGenPoolName).getUsed(); final long oldAfter = after.get(oldGenPoolName).getUsed(); final long delta = oldAfter - oldBefore; if (delta > 0L) { PROMOTION_RATE.increment(delta); } // Some GCs such as G1 can reduce the old gen size as part of a minor GC. To track the // live data size we record the value if we see a reduction in the old gen heap size or // after a major GC. if (oldAfter < oldBefore || HelperFunctions.getGcType(name) == GcType.OLD) { LIVE_DATA_SIZE.set(oldAfter); final long oldMaxAfter = after.get(oldGenPoolName).getMax(); MAX_DATA_SIZE.set(oldMaxAfter); } } if (youngGenPoolName != null) { final long youngBefore = before.get(youngGenPoolName).getUsed(); final long youngAfter = after.get(youngGenPoolName).getUsed(); final long delta = youngBefore - youngGenSizeAfter; youngGenSizeAfter = youngAfter; if (delta > 0L) { ALLOCATION_RATE.increment(delta); } } } private void processGcEvent(GarbageCollectionNotificationInfo info) { GcEvent event = new GcEvent(info, jvmStartTime + info.getGcInfo().getStartTime()); gcLogs.get(info.getGcName()).add(event); if (LOGGER.isDebugEnabled()) { LOGGER.debug(event.toString()); } // Update pause timer for the action and cause... Id eventId = (isConcurrentPhase(info) ? CONCURRENT_PHASE_TIME : PAUSE_TIME) .withTag("action", info.getGcAction()) .withTag("cause", info.getGcCause()); Timer timer = Spectator.globalRegistry().timer(eventId); timer.record(info.getGcInfo().getDuration(), TimeUnit.MILLISECONDS); // Update promotion and allocation counters updateMetrics(info.getGcName(), info.getGcInfo()); // Notify an event listener if registered if (eventListener != null) { try { eventListener.onComplete(event); } catch (Exception e) { LOGGER.warn("exception thrown by event listener", e); } } } private boolean isConcurrentPhase(GarbageCollectionNotificationInfo info) { // So far the only indicator known is that the cause will be reported as "No GC" // when using CMS. return "No GC".equals(info.getGcCause()); } private class GcNotificationListener implements NotificationListener { public void handleNotification(Notification notification, Object ref) { final String type = notification.getType(); if (type.equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { CompositeData cd = (CompositeData) notification.getUserData(); GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from(cd); processGcEvent(info); } } } }