1010import io .prometheus .metrics .model .snapshots .Unit ;
1111import java .lang .management .GarbageCollectorMXBean ;
1212import java .lang .management .ManagementFactory ;
13+ import java .util .ArrayList ;
1314import java .util .List ;
15+ import java .util .logging .Level ;
16+ import java .util .logging .Logger ;
1417import javax .annotation .Nullable ;
18+ import javax .management .ListenerNotFoundException ;
1519import javax .management .NotificationEmitter ;
20+ import javax .management .NotificationListener ;
1621import javax .management .openmbean .CompositeData ;
1722
1823/**
4045 * jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0
4146 * jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0
4247 * </pre>
48+ *
49+ * <p><b>Note on resource cleanup:</b> When using OpenTelemetry semantic conventions (via {@code
50+ * use_otel_semconv} configuration), this class registers JMX notification listeners that should be
51+ * cleaned up when the metrics are no longer needed. To ensure proper cleanup, keep a reference to
52+ * the {@link JvmGarbageCollectorMetrics} instance and call {@link #close()} when done:
53+ *
54+ * <pre>{@code
55+ * JvmGarbageCollectorMetrics gcMetrics = new JvmGarbageCollectorMetrics(...);
56+ * gcMetrics.register(registry);
57+ * // ... later, when shutting down:
58+ * gcMetrics.close();
59+ * }</pre>
4360 */
44- public class JvmGarbageCollectorMetrics {
61+ public class JvmGarbageCollectorMetrics implements AutoCloseable {
62+ private static final Logger logger = Logger .getLogger (JvmGarbageCollectorMetrics .class .getName ());
4563
4664 private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds" ;
4765 private static final String JVM_GC_DURATION = "jvm.gc.duration" ;
4866
4967 private final PrometheusProperties config ;
5068 private final List <GarbageCollectorMXBean > garbageCollectorBeans ;
5169 private final Labels constLabels ;
70+ private final List <ListenerRegistration > listenerRegistrations = new ArrayList <>();
5271
5372 private JvmGarbageCollectorMetrics (
5473 List <GarbageCollectorMXBean > garbageCollectorBeans ,
@@ -109,22 +128,33 @@ private void registerNotificationListener(Histogram gcDurationHistogram) {
109128 continue ;
110129 }
111130
112- ((NotificationEmitter ) gcBean )
113- .addNotificationListener (
114- (notification , handback ) -> {
115- if (!GarbageCollectionNotificationInfo .GARBAGE_COLLECTION_NOTIFICATION .equals (
116- notification .getType ())) {
117- return ;
118- }
119-
120- GarbageCollectionNotificationInfo info =
121- GarbageCollectionNotificationInfo .from (
122- (CompositeData ) notification .getUserData ());
123-
124- observe (gcDurationHistogram , info );
125- },
126- null ,
127- null );
131+ NotificationEmitter notificationEmitter = (NotificationEmitter ) gcBean ;
132+
133+ // Create a named listener instance so we can remove it later
134+ NotificationListener listener =
135+ (notification , handback ) -> {
136+ try {
137+ if (!GarbageCollectionNotificationInfo .GARBAGE_COLLECTION_NOTIFICATION .equals (
138+ notification .getType ())) {
139+ return ;
140+ }
141+
142+ GarbageCollectionNotificationInfo info =
143+ GarbageCollectionNotificationInfo .from (
144+ (CompositeData ) notification .getUserData ());
145+
146+ observe (gcDurationHistogram , info );
147+ } catch (Exception e ) {
148+ // Must not propagate exceptions - would cause JVM to remove listener permanently
149+ logger .log (
150+ Level .WARNING , "Exception while processing garbage collection notification" , e );
151+ }
152+ };
153+
154+ notificationEmitter .addNotificationListener (listener , null , null );
155+
156+ // Store registration info for cleanup
157+ listenerRegistrations .add (new ListenerRegistration (notificationEmitter , listener ));
128158 }
129159 }
130160
@@ -135,6 +165,47 @@ private void observe(Histogram gcDurationHistogram, GarbageCollectionNotificatio
135165 .observe (observedDuration );
136166 }
137167
168+ /**
169+ * Removes all JMX notification listeners registered by this instance.
170+ *
171+ * <p>This method should be called when the metrics are no longer needed to prevent memory leaks.
172+ * It is safe to call this method multiple times.
173+ *
174+ * <p><b>Note:</b> This only affects metrics registered with OpenTelemetry semantic conventions
175+ * (when {@code use_otel_semconv} is enabled). The callback-based Prometheus metrics do not
176+ * require cleanup.
177+ */
178+ @ Override
179+ public void close () {
180+ for (ListenerRegistration registration : listenerRegistrations ) {
181+ try {
182+ registration .notificationEmitter .removeNotificationListener (registration .listener );
183+ } catch (ListenerNotFoundException e ) {
184+ // Listener was already removed or never added - ignore
185+ logger .log (Level .FINE , "Listener not found during cleanup" , e );
186+ } catch (Exception e ) {
187+ // Log but continue removing other listeners
188+ logger .log (Level .WARNING , "Error removing GC notification listener" , e );
189+ }
190+ }
191+ listenerRegistrations .clear ();
192+ }
193+
194+ /**
195+ * Holds registration information for a notification listener so it can be removed later.
196+ *
197+ * <p>Package-private for testing.
198+ */
199+ static class ListenerRegistration {
200+ final NotificationEmitter notificationEmitter ;
201+ final NotificationListener listener ;
202+
203+ ListenerRegistration (NotificationEmitter notificationEmitter , NotificationListener listener ) {
204+ this .notificationEmitter = notificationEmitter ;
205+ this .listener = listener ;
206+ }
207+ }
208+
138209 public static Builder builder () {
139210 return new Builder (PrometheusProperties .get ());
140211 }
@@ -164,16 +235,46 @@ Builder garbageCollectorBeans(List<GarbageCollectorMXBean> garbageCollectorBeans
164235 return this ;
165236 }
166237
167- public void register () {
168- register (PrometheusRegistry .defaultRegistry );
238+ /**
239+ * Register GC metrics with the default registry.
240+ *
241+ * <p><b>Important:</b> When using OpenTelemetry semantic conventions, this method returns a
242+ * {@link JvmGarbageCollectorMetrics} instance that implements {@link AutoCloseable}. Keep a
243+ * reference and call {@link JvmGarbageCollectorMetrics#close()} when shutting down to prevent
244+ * memory leaks:
245+ *
246+ * <pre>{@code
247+ * JvmGarbageCollectorMetrics gcMetrics = JvmGarbageCollectorMetrics.builder().register();
248+ * // ... later during shutdown:
249+ * gcMetrics.close();
250+ * }</pre>
251+ *
252+ * @return the registered metrics instance, which should be closed when no longer needed
253+ */
254+ public JvmGarbageCollectorMetrics register () {
255+ return register (PrometheusRegistry .defaultRegistry );
169256 }
170257
171- public void register (PrometheusRegistry registry ) {
258+ /**
259+ * Register GC metrics with the specified registry.
260+ *
261+ * <p><b>Important:</b> When using OpenTelemetry semantic conventions, this method returns a
262+ * {@link JvmGarbageCollectorMetrics} instance that implements {@link AutoCloseable}. Keep a
263+ * reference and call {@link JvmGarbageCollectorMetrics#close()} when shutting down to prevent
264+ * memory leaks.
265+ *
266+ * @param registry the registry to register metrics with
267+ * @return the registered metrics instance, which should be closed when no longer needed
268+ */
269+ public JvmGarbageCollectorMetrics register (PrometheusRegistry registry ) {
172270 List <GarbageCollectorMXBean > garbageCollectorBeans = this .garbageCollectorBeans ;
173271 if (garbageCollectorBeans == null ) {
174272 garbageCollectorBeans = ManagementFactory .getGarbageCollectorMXBeans ();
175273 }
176- new JvmGarbageCollectorMetrics (garbageCollectorBeans , config , constLabels ).register (registry );
274+ JvmGarbageCollectorMetrics metrics =
275+ new JvmGarbageCollectorMetrics (garbageCollectorBeans , config , constLabels );
276+ metrics .register (registry );
277+ return metrics ;
177278 }
178279 }
179280}
0 commit comments