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