001package io.prometheus.metrics.instrumentation.jvm;
002
003import com.sun.management.GarbageCollectionNotificationInfo;
004import com.sun.management.GcInfo;
005import io.prometheus.metrics.config.PrometheusProperties;
006import io.prometheus.metrics.core.metrics.Counter;
007import io.prometheus.metrics.model.registry.PrometheusRegistry;
008
009import javax.management.Notification;
010import javax.management.NotificationEmitter;
011import javax.management.NotificationListener;
012import javax.management.openmbean.CompositeData;
013import java.lang.management.GarbageCollectorMXBean;
014import java.lang.management.ManagementFactory;
015import java.lang.management.MemoryUsage;
016import java.util.HashMap;
017import java.util.List;
018import java.util.Map;
019
020/**
021 * JVM memory allocation metrics. The {@link JvmMemoryPoolAllocationMetrics} are registered as part of the {@link JvmMetrics} like this:
022 * <pre>{@code
023 *   JvmMetrics.builder().register();
024 * }</pre>
025 * However, if you want only the {@link JvmMemoryPoolAllocationMetrics} you can also register them directly:
026 * <pre>{@code
027 *   JvmMemoryAllocationMetrics.builder().register();
028 * }</pre>
029 * Example metrics being exported:
030 * <pre>
031 * # HELP jvm_memory_pool_allocated_bytes_total Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.
032 * # TYPE jvm_memory_pool_allocated_bytes_total counter
033 * jvm_memory_pool_allocated_bytes_total{pool="Code Cache"} 4336448.0
034 * jvm_memory_pool_allocated_bytes_total{pool="Compressed Class Space"} 875016.0
035 * jvm_memory_pool_allocated_bytes_total{pool="Metaspace"} 7480456.0
036 * jvm_memory_pool_allocated_bytes_total{pool="PS Eden Space"} 1.79232824E8
037 * jvm_memory_pool_allocated_bytes_total{pool="PS Old Gen"} 1428888.0
038 * jvm_memory_pool_allocated_bytes_total{pool="PS Survivor Space"} 4115280.0
039 * </pre>
040 */
041public class JvmMemoryPoolAllocationMetrics {
042
043    private static final String JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL = "jvm_memory_pool_allocated_bytes_total";
044
045    private final PrometheusProperties config;
046    private final List<GarbageCollectorMXBean> garbageCollectorBeans;
047
048    private JvmMemoryPoolAllocationMetrics(List<GarbageCollectorMXBean> garbageCollectorBeans, PrometheusProperties config) {
049        this.garbageCollectorBeans = garbageCollectorBeans;
050        this.config = config;
051    }
052
053    private void register(PrometheusRegistry registry) {
054
055        Counter allocatedCounter = Counter.builder()
056                .name(JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL)
057                .help("Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.")
058                .labelNames("pool")
059                .register(registry);
060
061        AllocationCountingNotificationListener listener = new AllocationCountingNotificationListener(allocatedCounter);
062        for (GarbageCollectorMXBean garbageCollectorMXBean : garbageCollectorBeans) {
063            if (garbageCollectorMXBean instanceof NotificationEmitter) {
064                ((NotificationEmitter) garbageCollectorMXBean).addNotificationListener(listener, null, null);
065            }
066        }
067    }
068
069    static class AllocationCountingNotificationListener implements NotificationListener {
070
071        private final Map<String, Long> lastMemoryUsage = new HashMap<String, Long>();
072        private final Counter counter;
073
074        AllocationCountingNotificationListener(Counter counter) {
075            this.counter = counter;
076        }
077
078        @Override
079        public synchronized void handleNotification(Notification notification, Object handback) {
080            GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
081            GcInfo gcInfo = info.getGcInfo();
082            Map<String, MemoryUsage> memoryUsageBeforeGc = gcInfo.getMemoryUsageBeforeGc();
083            Map<String, MemoryUsage> memoryUsageAfterGc = gcInfo.getMemoryUsageAfterGc();
084            for (Map.Entry<String, MemoryUsage> entry : memoryUsageBeforeGc.entrySet()) {
085                String memoryPool = entry.getKey();
086                long before = entry.getValue().getUsed();
087                long after = memoryUsageAfterGc.get(memoryPool).getUsed();
088                handleMemoryPool(memoryPool, before, after);
089            }
090        }
091
092        // Visible for testing
093        void handleMemoryPool(String memoryPool, long before, long after) {
094            /*
095             * Calculate increase in the memory pool by comparing memory used
096             * after last GC, before this GC, and after this GC.
097             * See ascii illustration below.
098             * Make sure to count only increases and ignore decreases.
099             * (Typically a pool will only increase between GCs or during GCs, not both.
100             * E.g. eden pools between GCs. Survivor and old generation pools during GCs.)
101             *
102             *                         |<-- diff1 -->|<-- diff2 -->|
103             * Timeline: |-- last GC --|             |---- GC -----|
104             *                      ___^__        ___^____      ___^___
105             * Mem. usage vars:    / last \      / before \    / after \
106             */
107
108            // Get last memory usage after GC and remember memory used after for next time
109            long last = getAndSet(lastMemoryUsage, memoryPool, after);
110            // Difference since last GC
111            long diff1 = before - last;
112            // Difference during this GC
113            long diff2 = after - before;
114            // Make sure to only count increases
115            if (diff1 < 0) {
116                diff1 = 0;
117            }
118            if (diff2 < 0) {
119                diff2 = 0;
120            }
121            long increase = diff1 + diff2;
122            if (increase > 0) {
123                counter.labelValues(memoryPool).inc(increase);
124            }
125        }
126
127        private static long getAndSet(Map<String, Long> map, String key, long value) {
128            Long last = map.put(key, value);
129            return last == null ? 0 : last;
130        }
131    }
132
133    public static Builder builder() {
134        return new Builder(PrometheusProperties.get());
135    }
136
137    public static Builder builder(PrometheusProperties config) {
138        return new Builder(config);
139    }
140
141    public static class Builder {
142
143        private final PrometheusProperties config;
144        private List<GarbageCollectorMXBean> garbageCollectorBeans;
145
146        private Builder(PrometheusProperties config) {
147            this.config = config;
148        }
149
150        /**
151         * Package private. For testing only.
152         */
153        Builder withGarbageCollectorBeans(List<GarbageCollectorMXBean> garbageCollectorBeans) {
154            this.garbageCollectorBeans = garbageCollectorBeans;
155            return this;
156        }
157
158        public void register() {
159            register(PrometheusRegistry.defaultRegistry);
160        }
161
162        public void register(PrometheusRegistry registry) {
163            List<GarbageCollectorMXBean> garbageCollectorBeans = this.garbageCollectorBeans;
164            if (garbageCollectorBeans == null) {
165                garbageCollectorBeans = ManagementFactory.getGarbageCollectorMXBeans();
166            }
167            new JvmMemoryPoolAllocationMetrics(garbageCollectorBeans, config).register(registry);
168        }
169    }
170}