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}