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}