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