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