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