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}