Summary
When management.metrics.use-global-registry=true (the default), MeterRegistryPostProcessor adds each context's MeterRegistry bean to Micrometer's static Metrics.globalRegistry:
private void addToGlobalRegistryIfNecessary(MeterRegistry meterRegistry) {
if (this.properties.getObject().isUseGlobalRegistry() && !isGlobalRegistry(meterRegistry)) {
Metrics.addRegistry(meterRegistry);
}
}
https://github.com/spring-projects/spring-boot/blob/v4.0.6/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java#L123-L127
On context close, MetricsAutoConfiguration$MeterRegistryCloser calls meterRegistry.close() but never calls Metrics.removeRegistry(meterRegistry) (or equivalently Metrics.globalRegistry.remove(meterRegistry)):
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (this.context.equals(event.getApplicationContext())) {
for (MeterRegistry meterRegistry : this.meterRegistries) {
if (!meterRegistry.isClosed()) {
meterRegistry.close();
}
}
}
}
https://github.com/spring-projects/spring-boot/blob/v4.0.6/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java#L115-L117
This leads to memory leaks if closed MeterRegistrys are not cleaned up from the global registry. Because if the static field, Metrics.globalRegistry, still holds a reference to a closed MeterRegistry, the JVM garbage collector would not be able to cleanup the MeterRegistry instance and its held objects.
Suggested fix
In MeterRegistryCloser, call Metrics.removeRegistry(meterRegistry) or Metrics.globalRegistry.remove(meterRegistry) before closing the meterRegistry:
public void onApplicationEvent(ContextClosedEvent event) {
if (this.context.equals(event.getApplicationContext())) {
for (MeterRegistry meterRegistry : this.meterRegistries) {
Metrics.globalRegistry.remove(meterRegistry);
if (!meterRegistry.isClosed()) {
meterRegistry.close();
}
}
}
}
Note: Unlike when adding them to the globalRegistry, which was conditional on management.metrics.use-global-registry=true, its probably fine to just unconditionally remove the MeterRegistry here since its being closed anyway?
Reproduction
To see the bug:
- Pull down my reproduction project: https://github.com/Nephery/spring-boot-micrometer-global-registry-leak
- Run the test class:
MeterRegistryGlobalRegistryLeakTest.java
- The test will fail because
Metrics.globalRegistry is holding on to a monotonously ever-increasing list of closed MeterRegistry instances.
- This test class will also create 2 heap dump files for the first and last iterations,
leak-iter-0.hprof and leak-iter-19.hprof. Notice that by leak-iter-19.hprof, the number of AnnotationConfigApplicationContext in the heap increases from 1 to 20.
Notice the statically from globalRegistry of io.micrometer.core.instrument.Metrics lines in the images below.
AnnotationConfigApplicationContext instances in leak-iter-0.hprof:
AnnotationConfigApplicationContext instances in leak-iter-19.hprof:
To try out the suggested fix:
- Pull down my branch cleanup-micrometer-globalregistry
- Note: To minimize deviation from an actual release, this is just a branch off the
4.0.6 tag with Nephery@3f7277a applied for the suggested fix.
- Run
gradle module:spring-boot-micrometer-metrics:publishToMavenLocal
- Uncomment this line: https://github.com/Nephery/spring-boot-micrometer-global-registry-leak/blob/main/pom.xml#L36
- Run the test again and notice that it passes now since global registry is now cleaned up.
- Inspecting the
leak-iter-19.hprof heap dump, notice that there's now always ever only one AnnotationConfigApplicationContext in the heap (the last one presumably still in scope due to test reference to it).
AnnotationConfigApplicationContext instances in leak-iter-19.hprof:
There is now NO statically from globalRegistry of io.micrometer.core.instrument.Metrics lines in the heap dump image.
Summary
When
management.metrics.use-global-registry=true(the default),MeterRegistryPostProcessoradds each context'sMeterRegistrybean to Micrometer's staticMetrics.globalRegistry:https://github.com/spring-projects/spring-boot/blob/v4.0.6/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MeterRegistryPostProcessor.java#L123-L127
On context close,
MetricsAutoConfiguration$MeterRegistryClosercallsmeterRegistry.close()but never callsMetrics.removeRegistry(meterRegistry)(or equivalentlyMetrics.globalRegistry.remove(meterRegistry)):https://github.com/spring-projects/spring-boot/blob/v4.0.6/module/spring-boot-micrometer-metrics/src/main/java/org/springframework/boot/micrometer/metrics/autoconfigure/MetricsAutoConfiguration.java#L115-L117
This leads to memory leaks if closed
MeterRegistrys are not cleaned up from the global registry. Because if the static field,Metrics.globalRegistry, still holds a reference to a closedMeterRegistry, the JVM garbage collector would not be able to cleanup theMeterRegistryinstance and its held objects.Suggested fix
In
MeterRegistryCloser, callMetrics.removeRegistry(meterRegistry)orMetrics.globalRegistry.remove(meterRegistry)before closing themeterRegistry:Note: Unlike when adding them to the
globalRegistry, which was conditional onmanagement.metrics.use-global-registry=true, its probably fine to just unconditionally remove theMeterRegistryhere since its being closed anyway?Reproduction
To see the bug:
MeterRegistryGlobalRegistryLeakTest.javaMetrics.globalRegistryis holding on to a monotonously ever-increasing list of closedMeterRegistryinstances.leak-iter-0.hprofandleak-iter-19.hprof. Notice that byleak-iter-19.hprof, the number ofAnnotationConfigApplicationContextin the heap increases from1to20.Notice the
statically from globalRegistry of io.micrometer.core.instrument.Metricslines in the images below.AnnotationConfigApplicationContextinstances inleak-iter-0.hprof:AnnotationConfigApplicationContextinstances inleak-iter-19.hprof:To try out the suggested fix:
4.0.6tag with Nephery@3f7277a applied for the suggested fix.gradle module:spring-boot-micrometer-metrics:publishToMavenLocalleak-iter-19.hprofheap dump, notice that there's now always ever only oneAnnotationConfigApplicationContextin the heap (the last one presumably still in scope due to test reference to it).AnnotationConfigApplicationContextinstances inleak-iter-19.hprof:There is now NO
statically from globalRegistry of io.micrometer.core.instrument.Metricslines in the heap dump image.