Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ public void bindTo(MeterRegistry registry) {
.description("The maximum amount of memory in bytes that can be used for memory management")
.baseUnit(BaseUnits.BYTES)
.register(registry);

JvmMemoryMeterConventions.JvmMemoryUsedAfterLastGcConvention memoryUsedAfterLastGcConvention = conventions
.getMemoryUsedAfterLastGcConvention();
if (memoryPoolBean.getCollectionUsage() != null) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did have a version of this code that registered pools even if this returns null, but they always have the value NaN which didn't seem worthwhile registering.

Gauge.builder(memoryUsedAfterLastGcConvention.getName(), memoryPoolBean, pool -> {
MemoryUsage collectionUsage = pool.getCollectionUsage();
return collectionUsage != null ? collectionUsage.getUsed() : Double.NaN;
})
.tags(memoryUsedAfterLastGcConvention.getTags(memoryPoolBean))
.description(
"Measure of memory used, as measured after the most recent garbage collection event on this pool.")
.baseUnit(BaseUnits.BYTES)
.register(registry);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micrometer.core.instrument.binder.jvm.convention;

import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.MeterConvention;

import java.lang.management.MemoryPoolMXBean;
Expand All @@ -33,4 +34,33 @@ public interface JvmMemoryMeterConventions {

MeterConvention<MemoryPoolMXBean> getMemoryMaxConvention();

/**
* Convention for JVM memory used after last GC.
* @return convention for JVM memory used after last GC
* @since 1.17.0
*/
default JvmMemoryUsedAfterLastGcConvention getMemoryUsedAfterLastGcConvention() {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While other methods here have return type MeterConvention<MemoryPoolMXBean>, that was probably a mistake that I tried to not repeat here. In a framework's configuration model, it would be reasonable for there to be a way for a user to provide a custom convention for a specific convention, and that's made harder by using generic types that do not uniquely identify a single convention.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we create an issue for 2.x?

return new JvmMemoryUsedAfterLastGcConvention() {
};
}

/**
* {@link MeterConvention} for JVM memory used after last GC.
*
* @since 1.17.0
*/
interface JvmMemoryUsedAfterLastGcConvention extends MeterConvention<MemoryPoolMXBean> {
Copy link
Copy Markdown
Member Author

@shakuzen shakuzen Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it would be better to make this a top level type - it can be a bit annoying to reference it as JvmMemoryMeterConventions.JvmMemoryUsedAfterLastGcConvention, but maybe it also logically groups what would be the individual convention types (if we had them for the other conventions here, that is).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grouping makes sense to me. Not sure how well-known it is but if people annoyed by using JvmMemoryMeterConventions.JvmMemoryUsedAfterLastGcConvention, they can static import it:

import static io.micrometer.core.instrument.binder.jvm.convention.JvmMemoryMeterConventions.JvmMemoryUsedAfterLastGcConvention;

and use only JvmMemoryUsedAfterLastGcConvention like it would be a top-level type.


@Override
default String getName() {
return "jvm.memory.used_after_last_gc";
}

@Override
default Tags getTags(MemoryPoolMXBean memoryPoolBean) {
return Tags.empty();
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default implementation is not useful, but it was needed since we can't add a new non-default method to the JvmMemoryMeterConventions interface without breaking types that extend/implement it.

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,15 @@ public MeterConvention<MemoryPoolMXBean> getMemoryMaxConvention() {
return new SimpleMeterConvention<>("jvm.memory.max", this::getCommonTags);
}

@Override
public JvmMemoryUsedAfterLastGcConvention getMemoryUsedAfterLastGcConvention() {
return new JvmMemoryUsedAfterLastGcConvention() {

@Override
public Tags getTags(MemoryPoolMXBean memoryPoolBean) {
return getCommonTags(memoryPoolBean);
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
*
* @author Michael Weirauch
*/
@GcTest
class JvmMemoryMetricsTest {

MeterRegistry registry = new SimpleMeterRegistry();
Expand Down Expand Up @@ -81,6 +82,23 @@ void otelMemoryMetricsWithExtraTags() {
assertJvmMemoryMetrics("non_heap", extraTags, true);
}

@Test
void memoryUsedAfterLastGc() {
new JvmMemoryMetrics().bindTo(registry);

assertThat(registry.get("jvm.memory.used_after_last_gc").gauges()).isNotEmpty()
.allSatisfy(gauge -> assertThat(gauge.value()).isGreaterThanOrEqualTo(0));

System.gc();

assertThat(registry.get("jvm.memory.used_after_last_gc").gauges()).isNotEmpty()
.allSatisfy(gauge -> assertThat(gauge.value()).isGreaterThanOrEqualTo(0));

registry.get("jvm.memory.used_after_last_gc").gauges().forEach(gauge -> {
assertThat(gauge.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES);
});
}

private void assertJvmMemoryMetrics(String area) {
assertJvmMemoryMetrics(area, Tags.empty());
}
Expand Down
Loading