001/** 002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.jboot.support.metric; 017 018import com.codahale.metrics.*; 019import com.jfinal.log.Log; 020import com.sun.management.GarbageCollectionNotificationInfo; 021import com.sun.management.GcInfo; 022 023import javax.management.ListenerNotFoundException; 024import javax.management.NotificationEmitter; 025import javax.management.NotificationListener; 026import javax.management.openmbean.CompositeData; 027import java.lang.management.GarbageCollectorMXBean; 028import java.lang.management.ManagementFactory; 029import java.lang.management.MemoryPoolMXBean; 030import java.lang.management.MemoryUsage; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.concurrent.CopyOnWriteArrayList; 035import java.util.concurrent.TimeUnit; 036import java.util.concurrent.atomic.AtomicLong; 037 038import static io.jboot.support.metric.JvmGcUtil.*; 039 040/** 041 * Record metrics that report a number of statistics related to garbage 042 * collection emanating from the MXBean and also adds information about GC causes. 043 * <p> 044 * This provides metrics for OpenJDK garbage collectors: serial, parallel, G1, Shenandoah, ZGC. 045 * 046 * @author Jon Schneider 047 * @author Tommy Ludwig 048 * @see GarbageCollectorMXBean 049 */ 050public class JvmGcMetrics implements MetricSet, AutoCloseable { 051 052 private final static Log log = Log.getLog(JvmGcMetrics.class); 053 054 private final boolean managementExtensionsPresent = isManagementExtensionsPresent(); 055 056private MetricRegistry metricRegistry; 057 058 private String youngGenPoolName; 059 060 private String oldGenPoolName; 061 062 private String nonGenerationalMemoryPool; 063 064 private final List<Runnable> notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>(); 065 066 067 public JvmGcMetrics(MetricRegistry metricRegistry) { 068 this.metricRegistry = metricRegistry; 069 070 for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { 071 String name = mbean.getName(); 072 if (isYoungGenPool(name)) { 073 youngGenPoolName = name; 074 } else if (isOldGenPool(name)) { 075 oldGenPoolName = name; 076 } else if (isNonGenerationalHeapPool(name)) { 077 nonGenerationalMemoryPool = name; 078 } 079 } 080 } 081 082 @Override 083 public Map<String, Metric> getMetrics() { 084 final Map<String, Metric> gauges = new HashMap<>(); 085 if (!this.managementExtensionsPresent) { 086 return gauges; 087 } 088 089 double maxLongLivedPoolBytes = getLongLivedHeapPool().map(mem -> getUsageValue(mem, MemoryUsage::getMax)).orElse(0.0); 090 091 AtomicLong maxDataSize = new AtomicLong((long) maxLongLivedPoolBytes); 092 gauges.put("jvm.gc.max.data.size", (Gauge<Long>) () -> maxDataSize.get()); 093 094 095 AtomicLong liveDataSize = new AtomicLong(); 096 gauges.put("jvm.gc.live.data.size", (Gauge<Long>) () -> liveDataSize.get()); 097 098 Counter allocatedBytes =metricRegistry.counter("jvm.gc.memory.allocated"); 099 Counter promotedBytes =(oldGenPoolName == null) ? null : metricRegistry.counter("jvm.gc.memory.promoted"); 100 101 102 103 // start watching for GC notifications 104 final AtomicLong heapPoolSizeAfterGc = new AtomicLong(); 105 106 for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { 107 if (!(mbean instanceof NotificationEmitter)) { 108 continue; 109 } 110 NotificationListener notificationListener = (notification, ref) -> { 111 CompositeData cd = (CompositeData) notification.getUserData(); 112 GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd); 113 114 String gcCause = notificationInfo.getGcCause(); 115 String gcAction = notificationInfo.getGcAction(); 116 GcInfo gcInfo = notificationInfo.getGcInfo(); 117 long duration = gcInfo.getDuration(); 118 if (isConcurrentPhase(gcCause, notificationInfo.getGcName())) { 119// Timer.builder("jvm.gc.concurrent.phase.time") 120// .tags(tags) 121// .tags("action", gcAction, "cause", gcCause) 122// .description("Time spent in concurrent phase") 123// .register(registry) 124// .record(duration, TimeUnit.MILLISECONDS); 125 metricRegistry.timer("jvm.gc.concurrent.phase.time") 126 .update(duration,TimeUnit.MICROSECONDS); 127 } else { 128// Timer.builder("jvm.gc.pause") 129// .tags(tags) 130// .tags("action", gcAction, "cause", gcCause) 131// .description("Time spent in GC pause") 132// .register(registry) 133// .record(duration, TimeUnit.MILLISECONDS); 134 metricRegistry.timer("jvm.gc.pause") 135 .update(duration,TimeUnit.MICROSECONDS); 136 } 137 138 // Update promotion and allocation counters 139 final Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc(); 140 final Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc(); 141 142 if (nonGenerationalMemoryPool != null) { 143 countPoolSizeDelta(gcInfo.getMemoryUsageBeforeGc(), gcInfo.getMemoryUsageAfterGc(), allocatedBytes, 144 heapPoolSizeAfterGc, nonGenerationalMemoryPool); 145 if (after.get(nonGenerationalMemoryPool).getUsed() < before.get(nonGenerationalMemoryPool).getUsed()) { 146 liveDataSize.set(after.get(nonGenerationalMemoryPool).getUsed()); 147 final long longLivedMaxAfter = after.get(nonGenerationalMemoryPool).getMax(); 148 maxDataSize.set(longLivedMaxAfter); 149 } 150 return; 151 } 152 153 if (oldGenPoolName != null) { 154 final long oldBefore = before.get(oldGenPoolName).getUsed(); 155 final long oldAfter = after.get(oldGenPoolName).getUsed(); 156 final long delta = oldAfter - oldBefore; 157 if (delta > 0L) { 158 promotedBytes.inc(delta); 159 } 160 161 // Some GC implementations such as G1 can reduce the old gen size as part of a minor GC. To track the 162 // live data size we record the value if we see a reduction in the old gen heap size or 163 // after a major GC. 164 if (oldAfter < oldBefore || GcGenerationAge.fromName(notificationInfo.getGcName()) == GcGenerationAge.OLD) { 165 liveDataSize.set(oldAfter); 166 final long oldMaxAfter = after.get(oldGenPoolName).getMax(); 167 maxDataSize.set(oldMaxAfter); 168 } 169 } 170 171 if (youngGenPoolName != null) { 172 countPoolSizeDelta(gcInfo.getMemoryUsageBeforeGc(), gcInfo.getMemoryUsageAfterGc(), allocatedBytes, 173 heapPoolSizeAfterGc, youngGenPoolName); 174 } 175 }; 176 NotificationEmitter notificationEmitter = (NotificationEmitter) mbean; 177 notificationEmitter.addNotificationListener(notificationListener, notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), null); 178 notificationListenerCleanUpRunnables.add(() -> { 179 try { 180 notificationEmitter.removeNotificationListener(notificationListener); 181 } catch (ListenerNotFoundException ignore) { 182 } 183 }); 184 } 185 186 return gauges; 187 } 188 189 private void countPoolSizeDelta(Map<String, MemoryUsage> before, Map<String, MemoryUsage> after, Counter counter, 190 AtomicLong previousPoolSize, String poolName) { 191 final long beforeBytes = before.get(poolName).getUsed(); 192 final long afterBytes = after.get(poolName).getUsed(); 193 final long delta = beforeBytes - previousPoolSize.get(); 194 previousPoolSize.set(afterBytes); 195 if (delta > 0L) { 196 counter.inc(delta); 197 } 198 } 199 200 private static boolean isManagementExtensionsPresent() { 201 if (ManagementFactory.getMemoryPoolMXBeans().isEmpty()) { 202 // Substrate VM, for example, doesn't provide or support these beans (yet) 203 log.warn("GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM"); 204 return false; 205 } 206 207 try { 208 Class.forName("com.sun.management.GarbageCollectionNotificationInfo", false, 209 MemoryPoolMXBean.class.getClassLoader()); 210 return true; 211 } catch (Throwable e) { 212 // We are operating in a JVM without access to this level of detail 213 log.warn("GC notifications will not be available because " + 214 "com.sun.management.GarbageCollectionNotificationInfo is not present"); 215 return false; 216 } 217 } 218 219 @Override 220 public void close() { 221 notificationListenerCleanUpRunnables.forEach(Runnable::run); 222 } 223 224 /** 225 * Generalization of which parts of the heap are considered "young" or "old" for multiple GC implementations 226 */ 227 enum GcGenerationAge { 228 OLD, 229 YOUNG, 230 UNKNOWN; 231 232 private static Map<String, GcGenerationAge> knownCollectors = new HashMap<String, GcGenerationAge>() {{ 233 put("ConcurrentMarkSweep", OLD); 234 put("Copy", YOUNG); 235 put("G1 Old Generation", OLD); 236 put("G1 Young Generation", YOUNG); 237 put("MarkSweepCompact", OLD); 238 put("PS MarkSweep", OLD); 239 put("PS Scavenge", YOUNG); 240 put("ParNew", YOUNG); 241 }}; 242 243 static GcGenerationAge fromName(String name) { 244 return knownCollectors.getOrDefault(name, UNKNOWN); 245 } 246 } 247 248}