Talk about the difference between JvmGcMetrics of springboot1.x and 2.x.

  springboot

Order

This article mainly studies the differences between the JvmGcMetrics of springboot1.x and 2.x.

Jvm gc metrics for springboot1.x

Springboot1.x does not have JvmGcMetrics, which simply collects several indicators of jvm in SystemPublicMetrics
spring-boot-actuator-1.5.9.RELEASE-sources.jar! /org/springframework/boot/actuate/endpoint/SystemPublicMetrics.java

    /**
     * Add JVM heap metrics.
     * @param result the result
     */
    protected void addHeapMetrics(Collection<Metric<?>> result) {
        MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean()
                .getHeapMemoryUsage();
        result.add(newMemoryMetric("heap.committed", memoryUsage.getCommitted()));
        result.add(newMemoryMetric("heap.init", memoryUsage.getInit()));
        result.add(newMemoryMetric("heap.used", memoryUsage.getUsed()));
        result.add(newMemoryMetric("heap", memoryUsage.getMax()));
    }

    /**
     * Add JVM non-heap metrics.
     * @param result the result
     */
    private void addNonHeapMetrics(Collection<Metric<?>> result) {
        MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean()
                .getNonHeapMemoryUsage();
        result.add(newMemoryMetric("nonheap.committed", memoryUsage.getCommitted()));
        result.add(newMemoryMetric("nonheap.init", memoryUsage.getInit()));
        result.add(newMemoryMetric("nonheap.used", memoryUsage.getUsed()));
        result.add(newMemoryMetric("nonheap", memoryUsage.getMax()));
    }

    /**
     * Add garbage collection metrics.
     * @param result the result
     */
    protected void addGarbageCollectionMetrics(Collection<Metric<?>> result) {
        List<GarbageCollectorMXBean> garbageCollectorMxBeans = ManagementFactory
                .getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean garbageCollectorMXBean : garbageCollectorMxBeans) {
            String name = beautifyGcName(garbageCollectorMXBean.getName());
            result.add(new Metric<Long>("gc." + name + ".count",
                    garbageCollectorMXBean.getCollectionCount()));
            result.add(new Metric<Long>("gc." + name + ".time",
                    garbageCollectorMXBean.getCollectionTime()));
        }
    }

Count and time of this gc are cumulative

JvmGcMetrics for springboot2.x

Springboot2.x uses micrometer instead to collect metrics, where gc is related to JvmGcMetrics
micrometer-core-1.0.3-sources.jar! /io/micrometer/core/instrument/binder/jvm/JvmGcMetrics.java

/**
 * 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
 */
@NonNullApi
@NonNullFields
public class JvmGcMetrics implements MeterBinder {

    private static final Logger logger = LoggerFactory.getLogger(JvmGcMetrics.class);

    private boolean managementExtensionsPresent = isManagementExtensionsPresent();

    private Iterable<Tag> tags;

    @Nullable
    private String youngGenPoolName;

    @Nullable
    private String oldGenPoolName;

    public JvmGcMetrics() {
        this(emptyList());
    }

    public JvmGcMetrics(Iterable<Tag> tags) {
        for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) {
            if (isYoungGenPool(mbean.getName()))
                youngGenPoolName = mbean.getName();
            if (isOldGenPool(mbean.getName()))
                oldGenPoolName = mbean.getName();
        }
        this.tags = tags;
    }

    @Override
    public void bindTo(MeterRegistry registry) {
        AtomicLong maxDataSize = new AtomicLong(0L);
        Gauge.builder("jvm.gc.max.data.size", maxDataSize, AtomicLong::get)
            .tags(tags)
            .description("Max size of old generation memory pool")
            .baseUnit("bytes")
            .register(registry);

        AtomicLong liveDataSize = new AtomicLong(0L);

        Gauge.builder("jvm.gc.live.data.size", liveDataSize, AtomicLong::get)
            .tags(tags)
            .description("Size of old generation memory pool after a full GC")
            .baseUnit("bytes")
            .register(registry);

        Counter promotedBytes = Counter.builder("jvm.gc.memory.promoted").tags(tags)
            .baseUnit("bytes")
            .description("Count of positive increases in the size of the old generation memory pool before GC to after GC")
            .register(registry);

        Counter allocatedBytes = Counter.builder("jvm.gc.memory.allocated").tags(tags)
            .baseUnit("bytes")
            .description("Incremented for an increase in the size of the young generation memory pool after one GC to before the next")
            .register(registry);

        if (this.managementExtensionsPresent) {
            // 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);

                            if (isConcurrentPhase(notificationInfo.getGcCause())) {
                                Timer.builder("jvm.gc.concurrent.phase.time")
                                        .tags(tags)
                                        .tags("action", notificationInfo.getGcAction(), "cause", notificationInfo.getGcCause())
                                        .description("Time spent in concurrent phase")
                                        .register(registry)
                                        .record(notificationInfo.getGcInfo().getDuration(), TimeUnit.MILLISECONDS);
                            } else {
                                Timer.builder("jvm.gc.pause")
                                        .tags(tags)
                                        .tags("action", notificationInfo.getGcAction(),
                                                "cause", notificationInfo.getGcCause())
                                        .description("Time spent in GC pause")
                                        .register(registry)
                                        .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) {
                                    promotedBytes.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) {
                                    allocatedBytes.increment(delta);
                                }
                            }
                        }
                    }, null, null);
                }
            }
        }
    }
//......
}

It can be seen that the relevant part of gc is updated with jmx’s NotificationEmitter mechanism instead; Gcause uses Timer type to collect.

Since micrometer supports tag, JvmGcMetrics adds action (For example, end of minor GC) and cause (For example, g1evaluation pause) two tags to support finer-grained gc pause indicator statistics.

Summary

Gc time and count of springboot1.x are cumulative, while springboot2.x uses JvmGcMetrics of micrometer, and its pause index of gc becomes Timer type. Timer type is equivalent to Meter plus Histogram. Meter records instantaneous values, and Histogram carries out distribution statistics on these instantaneous values. In addition, the springboot2.x version also marks gc pause with the tag of action and cause, which supports finer-grained index statistics query.

doc