We might want to invoke some business logic when there is excessive GC activity in out program. The code snippet below shows one way
of doing that. It defines a GCAnalyzer
class that takes the following key parameters -
trackedCycles
- The number of consecutive GC cycles that we want to track continouslypercentThreshold
- If the time take in GC activity exceeds the wall time by this percentage, call the callbackthresholdExceededCb
- User supplied callback invoked when the threshold is exceeded
This code tracks GC activity, regardless of either the GC algorithm or GC type for the algorithm. It could be easily tweaked to, say, only track time taken by consecutive STW (Stop-The-World) cycles, or consecutive full GC cycles.
It works by keeping track of the GC times in the last trackedCycles
cycles, and the total time spent from the start of the first cycle, to the
end of the last one.
import com.sun.management.GarbageCollectionNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.openmbean.CompositeData;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
/**
* GCAnalyzer monitors garbage collection activities and invokes a callback function
* when the percentage of time spent in garbage collection, over a specified number
* of consecutive cycles exceeds a given threshold.
*/
public class GCAnalyzer {
private List<GarbageCollectorMXBean> gcBeans;
private LinkedList<Long> durations = new LinkedList<>();
private LinkedList<Long> startTimes = new LinkedList<>();
// Number of consecutive GC cycles to keep track of
private int trackedCycles = 0;
// Percentage of time spent in GC over the cycles to trigger the callback function
private double percentThreshold = 0.0;
// Callback function to call when the percentage threshold is exceeded
private Consumer<Double> thresholdExceededCb;
private final class GCNotificationListener implements javax.management.NotificationListener {
/**
* Calculates the percentage of time spent in garbage collection relative to total elapsed time.
*
* @param currentEndTime The end time of the current GC cycle.
* @return The percentage of time spent in GC.
*/
private double calculateGcPercent(long currentEndTime) {
long totalDuration = durations.stream().mapToLong(Long::longValue).sum();
return (double) totalDuration / (currentEndTime - startTimes.getFirst()) * 100.0;
}
@Override
public void handleNotification(Notification notification, Object handback) {
if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
long duration = info.getGcInfo().getDuration();
long startTime = info.getGcInfo().getStartTime();
long endTime = info.getGcInfo().getEndTime();
if (durations.size() == trackedCycles) {
double percent = calculateGcPercent(endTime);
if (percent > percentThreshold) {
thresholdExceededCb.accept(percent);
}
durations.removeFirst();
startTimes.removeFirst();
}
durations.add(duration);
startTimes.add(startTime);
}
}
}
/**
* Registers the GCNotificationListener to all available GarbageCollectorMXBeans.
*/
private void registerGCNotificationListeners() {
for (GarbageCollectorMXBean bean : gcBeans) {
if (bean instanceof NotificationEmitter) {
NotificationEmitter emitter = (NotificationEmitter) bean;
emitter.addNotificationListener(new GCNotificationListener(), null, null);
}
}
}
/**
* Constructs a GCAnalyzer instance.
*
* @param trackedCycles The number of consecutive GC cycles to monitor.
* @param percentThreshold The threshold of GC activity as a percentage of total time.
* @param thresholdExceededCb A callback function to invoke when the threshold is exceeded.
*/
public GCAnalyzer(int trackedCycles, double percentThreshold, Consumer<Double> thresholdExceededCb) {
this.gcBeans = ManagementFactory.getGarbageCollectorMXBeans();
this.trackedCycles = trackedCycles;
this.percentThreshold = percentThreshold;
this.thresholdExceededCb = thresholdExceededCb;
registerGCNotificationListeners();
}
}
It could be called from main like this -
public static void main(String[] args) {
...
new GCAnalyzer(6,
70,
timeSpentInGcInPercent -> System.out.printf("GC Threshold exceeded: %.2f\n", timeSpentInGcInPercent)
);
...
}
In the example above, the registered callback, which prints out the threshold exceeded message, is called when the program spends more than 70% of clock
time in GC activity over the 6 previous GC cycles. Note, the the GCAnalyzer
implementation is agnostic of the GC type. The program could be easily modified
to filter on specific GC type (e.g. only G1 Young Gen
etc.)