28
28
import io .micrometer .core .util .internal .logging .InternalLoggerFactory ;
29
29
30
30
import javax .management .ListenerNotFoundException ;
31
+ import javax .management .Notification ;
31
32
import javax .management .NotificationEmitter ;
32
33
import javax .management .NotificationListener ;
33
34
import javax .management .openmbean .CompositeData ;
@@ -73,6 +74,13 @@ public class JvmGcMetrics implements MeterBinder, AutoCloseable {
73
74
74
75
private final List <Runnable > notificationListenerCleanUpRunnables = new CopyOnWriteArrayList <>();
75
76
77
+ private Counter allocatedBytes ;
78
+ @ Nullable
79
+ private Counter promotedBytes ;
80
+ private AtomicLong allocationPoolSizeAfter ;
81
+ private AtomicLong liveDataSize ;
82
+ private AtomicLong maxDataSize ;
83
+
76
84
public JvmGcMetrics () {
77
85
this (emptyList ());
78
86
}
@@ -90,109 +98,115 @@ public JvmGcMetrics(Iterable<Tag> tags) {
90
98
this .tags = tags ;
91
99
}
92
100
101
+ // VisibleForTesting
102
+ GcMetricsNotificationListener gcNotificationListener ;
103
+
93
104
@ Override
94
105
public void bindTo (MeterRegistry registry ) {
95
106
if (!this .managementExtensionsPresent ) {
96
107
return ;
97
108
}
98
109
110
+ gcNotificationListener = new GcMetricsNotificationListener (registry );
111
+
99
112
double maxLongLivedPoolBytes = getLongLivedHeapPools ().mapToDouble (mem -> getUsageValue (mem , MemoryUsage ::getMax )).sum ();
100
113
101
- AtomicLong maxDataSize = new AtomicLong ((long ) maxLongLivedPoolBytes );
114
+ maxDataSize = new AtomicLong ((long ) maxLongLivedPoolBytes );
102
115
Gauge .builder ("jvm.gc.max.data.size" , maxDataSize , AtomicLong ::get )
103
116
.tags (tags )
104
117
.description ("Max size of long-lived heap memory pool" )
105
118
.baseUnit (BaseUnits .BYTES )
106
119
.register (registry );
107
120
108
- AtomicLong liveDataSize = new AtomicLong ();
121
+ liveDataSize = new AtomicLong ();
109
122
110
123
Gauge .builder ("jvm.gc.live.data.size" , liveDataSize , AtomicLong ::get )
111
124
.tags (tags )
112
125
.description ("Size of long-lived heap memory pool after reclamation" )
113
126
.baseUnit (BaseUnits .BYTES )
114
127
.register (registry );
115
128
116
- Counter allocatedBytes = Counter .builder ("jvm.gc.memory.allocated" ).tags (tags )
129
+ allocatedBytes = Counter .builder ("jvm.gc.memory.allocated" ).tags (tags )
117
130
.baseUnit (BaseUnits .BYTES )
118
131
.description ("Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next" )
119
132
.register (registry );
120
133
121
- Counter promotedBytes = (isGenerationalGc ) ? Counter .builder ("jvm.gc.memory.promoted" ).tags (tags )
134
+ promotedBytes = (isGenerationalGc ) ? Counter .builder ("jvm.gc.memory.promoted" ).tags (tags )
122
135
.baseUnit (BaseUnits .BYTES )
123
136
.description ("Count of positive increases in the size of the old generation memory pool before GC to after GC" )
124
137
.register (registry ) : null ;
125
138
126
- final AtomicLong allocationPoolSizeAfter = new AtomicLong (0L );
139
+ allocationPoolSizeAfter = new AtomicLong (0L );
127
140
128
- for (GarbageCollectorMXBean mbean : ManagementFactory .getGarbageCollectorMXBeans ()) {
129
- if (!(mbean instanceof NotificationEmitter )) {
141
+ for (GarbageCollectorMXBean gcBean : ManagementFactory .getGarbageCollectorMXBeans ()) {
142
+ if (!(gcBean instanceof NotificationEmitter )) {
130
143
continue ;
131
144
}
132
- NotificationListener notificationListener = (notification , ref ) -> {
133
- CompositeData cd = (CompositeData ) notification .getUserData ();
134
- GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo .from (cd );
135
-
136
- String gcCause = notificationInfo .getGcCause ();
137
- String gcAction = notificationInfo .getGcAction ();
138
- GcInfo gcInfo = notificationInfo .getGcInfo ();
139
- long duration = gcInfo .getDuration ();
140
- if (isConcurrentPhase (gcCause , notificationInfo .getGcName ())) {
141
- Timer .builder ("jvm.gc.concurrent.phase.time" )
142
- .tags (tags )
143
- .tags ("action" , gcAction , "cause" , gcCause )
144
- .description ("Time spent in concurrent phase" )
145
- .register (registry )
146
- .record (duration , TimeUnit .MILLISECONDS );
147
- } else {
148
- Timer .builder ("jvm.gc.pause" )
149
- .tags (tags )
150
- .tags ("action" , gcAction , "cause" , gcCause )
151
- .description ("Time spent in GC pause" )
152
- .register (registry )
153
- .record (duration , TimeUnit .MILLISECONDS );
154
- }
155
-
156
- final Map <String , MemoryUsage > before = gcInfo .getMemoryUsageBeforeGc ();
157
- final Map <String , MemoryUsage > after = gcInfo .getMemoryUsageAfterGc ();
158
-
159
- countPoolSizeDelta (before , after , allocatedBytes , allocationPoolSizeAfter , allocationPoolName );
160
-
161
- final long longLivedBefore = longLivedPoolNames .stream ().mapToLong (pool -> before .get (pool ).getUsed ()).sum ();
162
- final long longLivedAfter = longLivedPoolNames .stream ().mapToLong (pool -> after .get (pool ).getUsed ()).sum ();
163
- if (isGenerationalGc ) {
164
- final long delta = longLivedAfter - longLivedBefore ;
165
- if (delta > 0L ) {
166
- promotedBytes .increment (delta );
167
- }
168
- }
169
-
170
- // Some GC implementations such as G1 can reduce the old gen size as part of a minor GC. To track the
171
- // live data size we record the value if we see a reduction in the old gen heap size or
172
- // after a major GC.
173
- if (longLivedAfter < longLivedBefore || isMajorGc (notificationInfo .getGcName ())) {
174
- liveDataSize .set (longLivedAfter );
175
- maxDataSize .set (longLivedPoolNames .stream ().mapToLong (pool -> after .get (pool ).getMax ()).sum ());
176
- }
177
- };
178
- NotificationEmitter notificationEmitter = (NotificationEmitter ) mbean ;
179
- notificationEmitter .addNotificationListener (notificationListener , notification -> notification .getType ().equals (GarbageCollectionNotificationInfo .GARBAGE_COLLECTION_NOTIFICATION ), null );
145
+ NotificationEmitter notificationEmitter = (NotificationEmitter ) gcBean ;
146
+ notificationEmitter .addNotificationListener (gcNotificationListener , notification -> notification .getType ().equals (GarbageCollectionNotificationInfo .GARBAGE_COLLECTION_NOTIFICATION ), null );
180
147
notificationListenerCleanUpRunnables .add (() -> {
181
148
try {
182
- notificationEmitter .removeNotificationListener (notificationListener );
149
+ notificationEmitter .removeNotificationListener (gcNotificationListener );
183
150
} catch (ListenerNotFoundException ignore ) {
184
151
}
185
152
});
186
153
}
187
154
}
188
155
189
- private boolean isGenerationalGcConfigured () {
190
- return ManagementFactory .getMemoryPoolMXBeans ().stream ()
191
- .filter (JvmMemory ::isHeap )
192
- .map (MemoryPoolMXBean ::getName )
193
- .filter (name -> !name .contains ("tenured" ))
194
- .count () > 1 ;
195
- }
156
+ class GcMetricsNotificationListener implements NotificationListener {
157
+ private final MeterRegistry registry ;
158
+
159
+ public GcMetricsNotificationListener (MeterRegistry registry ) {
160
+ this .registry = registry ;
161
+ }
162
+
163
+ @ Override
164
+ public void handleNotification (Notification notification , Object ref ) {
165
+ CompositeData cd = (CompositeData ) notification .getUserData ();
166
+ GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo .from (cd );
167
+
168
+ String gcCause = notificationInfo .getGcCause ();
169
+ String gcAction = notificationInfo .getGcAction ();
170
+ GcInfo gcInfo = notificationInfo .getGcInfo ();
171
+ long duration = gcInfo .getDuration ();
172
+ if (isConcurrentPhase (gcCause , notificationInfo .getGcName ())) {
173
+ Timer .builder ("jvm.gc.concurrent.phase.time" )
174
+ .tags (tags )
175
+ .tags ("action" , gcAction , "cause" , gcCause )
176
+ .description ("Time spent in concurrent phase" )
177
+ .register (registry )
178
+ .record (duration , TimeUnit .MILLISECONDS );
179
+ } else {
180
+ Timer .builder ("jvm.gc.pause" )
181
+ .tags (tags )
182
+ .tags ("action" , gcAction , "cause" , gcCause )
183
+ .description ("Time spent in GC pause" )
184
+ .register (registry )
185
+ .record (duration , TimeUnit .MILLISECONDS );
186
+ }
187
+
188
+ final Map <String , MemoryUsage > before = gcInfo .getMemoryUsageBeforeGc ();
189
+ final Map <String , MemoryUsage > after = gcInfo .getMemoryUsageAfterGc ();
190
+
191
+ countPoolSizeDelta (before , after , allocatedBytes , allocationPoolSizeAfter , allocationPoolName );
192
+
193
+ final long longLivedBefore = longLivedPoolNames .stream ().mapToLong (pool -> before .get (pool ).getUsed ()).sum ();
194
+ final long longLivedAfter = longLivedPoolNames .stream ().mapToLong (pool -> after .get (pool ).getUsed ()).sum ();
195
+ if (isGenerationalGc ) {
196
+ final long delta = longLivedAfter - longLivedBefore ;
197
+ if (delta > 0L ) {
198
+ promotedBytes .increment (delta );
199
+ }
200
+ }
201
+
202
+ // Some GC implementations such as G1 can reduce the old gen size as part of a minor GC. To track the
203
+ // live data size we record the value if we see a reduction in the long-lived heap size or
204
+ // after a major/non-generational GC.
205
+ if (longLivedAfter < longLivedBefore || shouldUpdateDataSizeMetrics (notificationInfo .getGcName ())) {
206
+ liveDataSize .set (longLivedAfter );
207
+ maxDataSize .set (longLivedPoolNames .stream ().mapToLong (pool -> after .get (pool ).getMax ()).sum ());
208
+ }
209
+ }
196
210
197
211
private void countPoolSizeDelta (Map <String , MemoryUsage > before , Map <String , MemoryUsage > after , Counter counter ,
198
212
AtomicLong previousPoolSize , @ Nullable String poolName ) {
@@ -208,8 +222,27 @@ private void countPoolSizeDelta(Map<String, MemoryUsage> before, Map<String, Mem
208
222
}
209
223
}
210
224
211
- private boolean isMajorGc (String gcName ) {
212
- return !isGenerationalGc || GcGenerationAge .fromGcName (gcName ) == GcGenerationAge .OLD ;
225
+ private boolean shouldUpdateDataSizeMetrics (String gcName ) {
226
+ return nonGenerationalGcShouldUpdateDataSize (gcName ) || isMajorGenerationalGc (gcName );
227
+ }
228
+
229
+ private boolean isMajorGenerationalGc (String gcName ) {
230
+ return GcGenerationAge .fromGcName (gcName ) == GcGenerationAge .OLD ;
231
+ }
232
+
233
+ private boolean nonGenerationalGcShouldUpdateDataSize (String gcName ) {
234
+ return !isGenerationalGc
235
+ // Skip Shenandoah and ZGC gc notifications with the name Pauses due to missing memory pool size info
236
+ && !gcName .endsWith ("Pauses" );
237
+ }
238
+ }
239
+
240
+ private boolean isGenerationalGcConfigured () {
241
+ return ManagementFactory .getMemoryPoolMXBeans ().stream ()
242
+ .filter (JvmMemory ::isHeap )
243
+ .map (MemoryPoolMXBean ::getName )
244
+ .filter (name -> !name .contains ("tenured" ))
245
+ .count () > 1 ;
213
246
}
214
247
215
248
private static boolean isManagementExtensionsPresent () {
0 commit comments