Skip to content

Commit

Permalink
Support non-generational garbage collector metrics (#2306)
Browse files Browse the repository at this point in the history
There were assumptions in JvmGcMetrics that the garbage collector was generational, with designated young and old regions. ZGC and Shenandoah are examples of OpenJDK garbage collectors that are not generational. GC metrics should be properly recorded for them with these changes.

Resolves #1861
Resolves #2305
  • Loading branch information
shakuzen authored Oct 28, 2020
1 parent d172f3d commit 6d3fec0
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 52 deletions.
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
executor: circle-jdk-executor
steps:
- gradlew-build
- run: ./gradlew shenandoahTest
- run: ./gradlew zgcTest

docker-tests:
executor: machine-executor
Expand Down
22 changes: 22 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,28 @@ subprojects {
}
}

task shenandoahTest(type: Test) {
// set heap size for the test JVM(s)
maxHeapSize = "1500m"

useJUnitPlatform {
includeTags 'gc'
}

jvmArgs '-XX:+UseShenandoahGC'
}

task zgcTest(type: Test) {
// set heap size for the test JVM(s)
maxHeapSize = "1500m"

useJUnitPlatform {
includeTags 'gc'
}

jvmArgs '-XX:+UseZGC'
}

license {
ext.year = Calendar.getInstance().get(Calendar.YEAR)
skipExistingHeaders = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@
/**
* Record metrics that report a number of statistics related to garbage
* collection emanating from the MXBean and also adds information about GC causes.
* <p>
* This provides metrics for OpenJDK garbage collectors: serial, parallel, G1, Shenandoah, ZGC.
*
* @author Jon Schneider
* @author Tommy Ludwig
* @see GarbageCollectorMXBean
*/
@NonNullApi
Expand All @@ -67,6 +70,9 @@ public class JvmGcMetrics implements MeterBinder, AutoCloseable {
@Nullable
private String oldGenPoolName;

@Nullable
private String nonGenerationalMemoryPool;

private final List<Runnable> notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>();

public JvmGcMetrics() {
Expand All @@ -80,6 +86,8 @@ public JvmGcMetrics(Iterable<Tag> tags) {
youngGenPoolName = name;
} else if (isOldGenPool(name)) {
oldGenPoolName = name;
} else if (isNonGenerationalHeapPool(name)) {
nonGenerationalMemoryPool = name;
}
}
this.tags = tags;
Expand All @@ -91,44 +99,41 @@ public void bindTo(MeterRegistry registry) {
return;
}

double maxOldGen = getOldGen().map(mem -> getUsageValue(mem, MemoryUsage::getMax)).orElse(0.0);
double maxLongLivedPoolBytes = getLongLivedHeapPool().map(mem -> getUsageValue(mem, MemoryUsage::getMax)).orElse(0.0);

AtomicLong maxDataSize = new AtomicLong((long) maxOldGen);
AtomicLong maxDataSize = new AtomicLong((long) maxLongLivedPoolBytes);
Gauge.builder("jvm.gc.max.data.size", maxDataSize, AtomicLong::get)
.tags(tags)
.description("Max size of old generation memory pool")
.description("Max size of long-lived heap memory pool")
.baseUnit(BaseUnits.BYTES)
.register(registry);

AtomicLong liveDataSize = new AtomicLong();

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

Counter promotedBytes = Counter.builder("jvm.gc.memory.promoted").tags(tags)
.baseUnit(BaseUnits.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(BaseUnits.BYTES)
.description("Incremented for an increase in the size of the young generation memory pool after one GC to before the next")
.description("Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next")
.register(registry);

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

// start watching for GC notifications
final AtomicLong youngGenSizeAfter = new AtomicLong();
final AtomicLong heapPoolSizeAfterGc = new AtomicLong();

for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
if (!(mbean instanceof NotificationEmitter)) {
continue;
}
NotificationListener notificationListener = (notification, ref) -> {
if (!notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
return;
}
CompositeData cd = (CompositeData) notification.getUserData();
GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd);

Expand Down Expand Up @@ -156,6 +161,16 @@ public void bindTo(MeterRegistry registry) {
final Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc();
final Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc();

if (nonGenerationalMemoryPool != null) {
countPoolSizeDelta(gcInfo, allocatedBytes, heapPoolSizeAfterGc, nonGenerationalMemoryPool);
if (after.get(nonGenerationalMemoryPool).getUsed() < before.get(nonGenerationalMemoryPool).getUsed()) {
liveDataSize.set(after.get(nonGenerationalMemoryPool).getUsed());
final long longLivedMaxAfter = after.get(nonGenerationalMemoryPool).getMax();
maxDataSize.set(longLivedMaxAfter);
}
return;
}

if (oldGenPoolName != null) {
final long oldBefore = before.get(oldGenPoolName).getUsed();
final long oldAfter = after.get(oldGenPoolName).getUsed();
Expand All @@ -175,17 +190,11 @@ public void bindTo(MeterRegistry registry) {
}

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);
}
countPoolSizeDelta(gcInfo, allocatedBytes, heapPoolSizeAfterGc, youngGenPoolName);
}
};
NotificationEmitter notificationEmitter = (NotificationEmitter) mbean;
notificationEmitter.addNotificationListener(notificationListener, null, null);
notificationEmitter.addNotificationListener(notificationListener, notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), null);
notificationListenerCleanUpRunnables.add(() -> {
try {
notificationEmitter.removeNotificationListener(notificationListener);
Expand All @@ -195,6 +204,18 @@ public void bindTo(MeterRegistry registry) {
}
}

private void countPoolSizeDelta(GcInfo gcInfo, Counter counter, AtomicLong previousPoolSize, String poolName) {
Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc();
Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc();
final long beforeBytes = before.get(poolName).getUsed();
final long afterBytes = after.get(poolName).getUsed();
final long delta = beforeBytes - previousPoolSize.get();
previousPoolSize.set(afterBytes);
if (delta > 0L) {
counter.increment(delta);
}
}

private static boolean isManagementExtensionsPresent() {
if ( ManagementFactory.getMemoryPoolMXBeans().isEmpty() ) {
// Substrate VM, for example, doesn't provide or support these beans (yet)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class JvmHeapPressureMetrics implements MeterBinder, AutoCloseable {
private final TimeWindowSum gcPauseSum;
private final AtomicReference<Double> lastOldGenUsageAfterGc = new AtomicReference<>(0.0);

private String oldGenPoolName;
private final String longLivedPoolName;

public JvmHeapPressureMetrics() {
this(emptyList(), Duration.ofMinutes(5), Duration.ofMinutes(1));
Expand All @@ -66,26 +66,23 @@ public JvmHeapPressureMetrics(Iterable<Tag> tags, Duration lookback, Duration te
this.lookback = lookback;
this.gcPauseSum = new TimeWindowSum((int) lookback.dividedBy(testEvery.toMillis()).toMillis(), testEvery);

for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) {
String name = mbean.getName();
if (JvmMemory.isOldGenPool(name)) {
oldGenPoolName = name;
break;
}
}
longLivedPoolName = JvmMemory.getLongLivedHeapPool().map(MemoryPoolMXBean::getName).orElse(null);

monitor();
}

@Override
public void bindTo(@NonNull MeterRegistry registry) {
Gauge.builder("jvm.memory.usage.after.gc", lastOldGenUsageAfterGc, AtomicReference::get)
Gauge.Builder<AtomicReference<Double>> builder = Gauge.builder("jvm.memory.usage.after.gc", lastOldGenUsageAfterGc, AtomicReference::get)
.tags(tags)
.tag("area", "heap")
.tag("generation", "old")
.description("The percentage of old gen heap used after the last GC event, in the range [0..1]")
.baseUnit(BaseUnits.PERCENT)
.register(registry);
.baseUnit(BaseUnits.PERCENT);
if (JvmMemory.isOldGenPool(longLivedPoolName))
builder.tag("generation", "old");
else
builder.tag("pool", longLivedPoolName);
builder.register(registry);

Gauge.builder("jvm.gc.overhead", gcPauseSum,
pauseSum -> {
Expand All @@ -99,17 +96,11 @@ public void bindTo(@NonNull MeterRegistry registry) {
}

private void monitor() {
double maxOldGen = JvmMemory.getOldGen().map(mem -> JvmMemory.getUsageValue(mem, MemoryUsage::getMax)).orElse(0.0);

for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
if (!(mbean instanceof NotificationEmitter)) {
continue;
}
NotificationListener notificationListener = (notification, ref) -> {
if (!notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
return;
}

CompositeData cd = (CompositeData) notification.getUserData();
GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd);

Expand All @@ -123,13 +114,15 @@ private void monitor() {

Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc();

if (oldGenPoolName != null) {
final long oldAfter = after.get(oldGenPoolName).getUsed();
lastOldGenUsageAfterGc.set(oldAfter / maxOldGen);
if (longLivedPoolName != null) {
final long oldAfter = after.get(longLivedPoolName).getUsed();
lastOldGenUsageAfterGc.set(oldAfter / (double) after.get(longLivedPoolName).getMax());
}
};
NotificationEmitter notificationEmitter = (NotificationEmitter) mbean;
notificationEmitter.addNotificationListener(notificationListener, null, null);
notificationEmitter.addNotificationListener(notificationListener,
notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION),
null);
notificationListenerCleanUpRunnables.add(() -> {
try {
notificationEmitter.removeNotificationListener(notificationListener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ class JvmMemory {
private JvmMemory() {
}

static Optional<MemoryPoolMXBean> getOldGen() {
static Optional<MemoryPoolMXBean> getLongLivedHeapPool() {
return ManagementFactory
.getPlatformMXBeans(MemoryPoolMXBean.class)
.stream()
.filter(JvmMemory::isHeap)
.filter(mem -> isOldGenPool(mem.getName()))
.filter(mem -> isOldGenPool(mem.getName()) || isNonGenerationalHeapPool(mem.getName()))
.findAny();
}

Expand All @@ -50,6 +50,10 @@ static boolean isOldGenPool(String name) {
return name.endsWith("Old Gen") || name.endsWith("Tenured Gen");
}

static boolean isNonGenerationalHeapPool(String name) {
return "Shenandoah".equals(name) || "ZHeap".equals(name);
}

private static boolean isHeap(MemoryPoolMXBean memoryPoolBean) {
return MemoryType.HEAP.equals(memoryPoolBean.getType());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Copyright 2020 VMware, Inc.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.junit.jupiter.api.Tag;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("gc")
public @interface GcTest {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,34 @@
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;

import java.lang.management.MemoryPoolMXBean;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
* Tests for {@link JvmGcMetrics}.
*
* @author Johnny Lim
*/
@GcTest
class JvmGcMetricsTest {

@Test
void metersAreBound() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();
JvmGcMetrics binder = new JvmGcMetrics();
binder.bindTo(registry);
assertThat(registry.find("jvm.gc.max.data.size").gauge()).isNotNull();
assertThat(registry.find("jvm.gc.live.data.size").gauge()).isNotNull();
assertThat(registry.find("jvm.gc.memory.promoted").counter()).isNotNull();
assertThat(registry.find("jvm.gc.memory.allocated").counter()).isNotNull();
assertThat(registry.find("jvm.gc.max.data.size").gauge().value()).isGreaterThan(0);

assumeTrue(isGenerationalGc());
assertThat(registry.find("jvm.gc.memory.promoted").counter()).isNotNull();
}

private boolean isGenerationalGc() {
return JvmMemory.getLongLivedHeapPool().map(MemoryPoolMXBean::getName).filter(JvmMemory::isOldGenPool).isPresent();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@GcTest
class JvmMemoryTest {

@Test
void assertJvmMemoryGetOldGen() {
Optional<MemoryPoolMXBean> oldGen = JvmMemory.getOldGen();
assertThat(oldGen).isNotEmpty();
void assertJvmMemoryGetLongLivedHeapPool() {
Optional<MemoryPoolMXBean> longLivedHeapPool = JvmMemory.getLongLivedHeapPool();
assertThat(longLivedHeapPool).isNotEmpty();
}
}

0 comments on commit 6d3fec0

Please sign in to comment.