io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics Maven / Gradle / Ivy
Show all versions of micrometer-core Show documentation
/**
* Copyright 2020 VMware, 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
*
* https://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 io.micrometer.core.instrument.binder.jvm;
import com.sun.management.GarbageCollectionNotificationInfo;
import com.sun.management.GcInfo;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.BaseUnits;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.distribution.TimeWindowSum;
import io.micrometer.core.lang.NonNull;
import javax.management.ListenerNotFoundException;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;
import java.lang.management.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
/**
* Provides methods to access measurements of low pool memory and heavy GC overhead as described in
* TeamCity's Memory Monitor.
*
* @author Jon Schneider
* @since 1.4.0
*/
public class JvmHeapPressureMetrics implements MeterBinder, AutoCloseable {
private final Iterable tags;
private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>();
private final long startOfMonitoring = System.nanoTime();
private final Duration lookback;
private final TimeWindowSum gcPauseSum;
private final AtomicReference lastLongLivedPoolUsageAfterGc = new AtomicReference<>(0.0);
private final Set longLivedPoolNames;
public JvmHeapPressureMetrics() {
this(emptyList(), Duration.ofMinutes(5), Duration.ofMinutes(1));
}
public JvmHeapPressureMetrics(Iterable tags, Duration lookback, Duration testEvery) {
this.tags = tags;
this.lookback = lookback;
this.gcPauseSum = new TimeWindowSum((int) lookback.dividedBy(testEvery.toMillis()).toMillis(), testEvery);
longLivedPoolNames = JvmMemory.getLongLivedHeapPools().map(MemoryPoolMXBean::getName).collect(Collectors.toSet());
monitor();
}
@Override
public void bindTo(@NonNull MeterRegistry registry) {
if (!longLivedPoolNames.isEmpty()) {
Gauge.builder("jvm.memory.usage.after.gc", lastLongLivedPoolUsageAfterGc, AtomicReference::get)
.tags(tags)
.tag("area", "heap")
.tag("pool", "long-lived")
.description("The percentage of long-lived heap pool used after the last GC event, in the range [0..1]")
.baseUnit(BaseUnits.PERCENT)
.register(registry);
}
Gauge.builder("jvm.gc.overhead", gcPauseSum,
pauseSum -> {
double overIntervalMillis = Math.min(System.nanoTime() - startOfMonitoring, lookback.toNanos()) / 1e6;
return gcPauseSum.poll() / overIntervalMillis;
})
.tags(tags)
.description("An approximation of the percent of CPU time used by GC activities over the last lookback period or since monitoring began, whichever is shorter, in the range [0..1]")
.baseUnit(BaseUnits.PERCENT)
.register(registry);
}
private void monitor() {
for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
if (!(mbean instanceof NotificationEmitter)) {
continue;
}
NotificationListener notificationListener = (notification, ref) -> {
CompositeData cd = (CompositeData) notification.getUserData();
GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd);
String gcCause = notificationInfo.getGcCause();
GcInfo gcInfo = notificationInfo.getGcInfo();
long duration = gcInfo.getDuration();
if (!JvmMemory.isConcurrentPhase(gcCause, notificationInfo.getGcName())) {
gcPauseSum.record(duration);
}
Map after = gcInfo.getMemoryUsageAfterGc();
if (!longLivedPoolNames.isEmpty()) {
final long usedAfter = longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getUsed()).sum();
double maxAfter = longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getMax()).sum();
lastLongLivedPoolUsageAfterGc.set(usedAfter / maxAfter);
}
};
NotificationEmitter notificationEmitter = (NotificationEmitter) mbean;
notificationEmitter.addNotificationListener(notificationListener,
notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION),
null);
notificationListenerCleanUpRunnables.add(() -> {
try {
notificationEmitter.removeNotificationListener(notificationListener);
} catch (ListenerNotFoundException ignore) {
}
});
}
}
@Override
public void close() {
notificationListenerCleanUpRunnables.forEach(Runnable::run);
}
}