/** * Copyright 2017 Pivotal Software, 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 org.springframework.metrics.instrument.binder; import com.sun.management.GarbageCollectionNotificationInfo; import com.sun.management.GcInfo; import org.springframework.metrics.instrument.Counter; import org.springframework.metrics.instrument.MeterRegistry; import javax.management.NotificationEmitter; 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.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * Record metrics that report a number of statistics related to garbage * collection emanating from the MXBean and also adds information about GC causes. * * @see GarbageCollectorMXBean */ public class JvmGcMetrics implements MeterBinder { private String youngGenPoolName; private String oldGenPoolName; public JvmGcMetrics() { for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { if (isYoungGenPool(mbean.getName())) youngGenPoolName = mbean.getName(); if (isOldGenPool(mbean.getName())) oldGenPoolName = mbean.getName(); } } @Override public void bindTo(MeterRegistry registry) { // Max size of old generation memory pool AtomicLong maxDataSize = registry.gauge("jvm_gc_max_data_size", new AtomicLong(0L)); // Size of old generation memory pool after a full GC AtomicLong liveDataSize = registry.gauge("jvm_gc_live_data_size", new AtomicLong(0L)); // Incremented for any positive increases in the size of the old generation memory pool // before GC to after GC Counter promotionRate = registry.counter("jvm_gc_promotion_rate"); // Incremented for the increase in the size of the young generation memory pool after one GC // to before the next Counter allocationRate = registry.counter("jvm_gc_allocation_rate"); // start watching for GC notifications final AtomicLong youngGenSizeAfter = new AtomicLong(0L); for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { if (mbean instanceof NotificationEmitter) { ((NotificationEmitter) mbean).addNotificationListener((notification, ref) -> { final String type = notification.getType(); if (type.equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) { CompositeData cd = (CompositeData) notification.getUserData(); GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd); registry.timer((isConcurrentPhase(notificationInfo) ? "jvm_gc_concurrent_phase_time" : "jvm_gc_pause"), "action", notificationInfo.getGcAction(), "cause", notificationInfo.getGcCause() ).record(notificationInfo.getGcInfo().getDuration(), TimeUnit.MILLISECONDS); GcInfo gcInfo = notificationInfo.getGcInfo(); // Update promotion and allocation counters final Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc(); final Map<String, MemoryUsage> after = gcInfo.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) { promotionRate.increment(delta); } // Some GC implementations 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 || GcGenerationAge.fromName(notificationInfo.getGcName()) == GcGenerationAge.OLD) { liveDataSize.set(oldAfter); final long oldMaxAfter = after.get(oldGenPoolName).getMax(); maxDataSize.set(oldMaxAfter); } } if (youngGenPoolName != null) { final long youngBefore = before.get(youngGenPoolName).getUsed(); final long youngAfter = after.get(youngGenPoolName).getUsed(); final long delta = youngBefore - youngGenSizeAfter.get(); youngGenSizeAfter.set(youngAfter); if (delta > 0L) { allocationRate.increment(delta); } } } }, null, null); } } } private boolean isConcurrentPhase(GarbageCollectionNotificationInfo info) { return "No GC".equals(info.getGcCause()); } private boolean isOldGenPool(String name) { return name.endsWith("Old Gen") || name.endsWith("Tenured Gen"); } private boolean isYoungGenPool(String name) { return name.endsWith("Eden Space"); } } /** * Generalization of which parts of the heap are considered "young" or "old" for multiple GC implementations */ enum GcGenerationAge { OLD, YOUNG, UNKNOWN; private static Map<String, GcGenerationAge> knownCollectors = new HashMap<String, GcGenerationAge>() {{ put("ConcurrentMarkSweep", OLD); put("Copy", YOUNG); put("G1 Old Generation", OLD); put("G1 Young Generation", YOUNG); put("MarkSweepCompact", OLD); put("PS MarkSweep", OLD); put("PS Scavenge", YOUNG); put("ParNew", YOUNG); }}; static GcGenerationAge fromName(String name) { GcGenerationAge t = knownCollectors.get(name); return (t == null) ? UNKNOWN : t; } }