背景
自己封装了一个Apollo的客户端,在官方支持@Value注解自动刷新的基础上,额外封装了@RefreshScope和@ConfigurationProperties注解的自动刷新,代码如下:
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
/**
* @ClassName: SpringBootApolloRefreshConfig
* @Author: Dylan Li
* @Date: 2026/4/8 10:10
*/
@Component
@Slf4j
@ConditionalOnProperty(name = "config-center.auto-refresh", havingValue = "true", matchIfMissing = false)
public class SpringBootApolloRefresh {
private final ApplicationEventPublisher eventPublisher;
public SpringBootApolloRefresh(
final ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@ApolloConfigChangeListener(value = "${apollo.bootstrap.namespaces}")
public void onChange(ConfigChangeEvent changeEvent) {
if (log.isDebugEnabled()) {
log.debug("ConfigChangeListener refresh ConfigChangeEvent begin, changedKeys={}",
changeEvent.changedKeys());
}
eventPublisher.publishEvent(new RefreshEvent(this, changeEvent, "Refresh Apollo config"));
if (log.isDebugEnabled()) {
log.debug("ConfigChangeListener publish RefreshEvent success.");
}
}
}
原本以为很简单的配置,结果通过查看Java进程的对象数,发现com.ctrip.framework.apollo.spring.property.SpringValue对象数量在配置变更的时候疯狂增加,触发GC也无法被回收,很明显,存在内存泄露。
原因分析
SpringValue 是在 SpringValueProcessor.doRegister() 里无条件 new 出来的。
注册路径是 BeanPostProcessor.postProcessBeforeInitialization —— 也就是每个 bean 实例走一次。而SpringValueRegistry.register() 用的是 LinkedListMultimap,允许同 key 重复,没有任何去重。
所以只要 bean 实例被反复重建,SpringValue就反复堆积。
而官方的回收机制在这个场景下是失效的:
scanAndClean() 每 5 秒跑一次,唯一的删除条件是 isTargetBeanValid(),即 beanRef.get() == null—— 依赖 bean 被 GC 掉、WeakReference 被清空。但:
private final Map<BeanFactory, Multimap<String, SpringValue>> registry = Maps.newConcurrentMap();
外层 map 的 key 是强引用的 BeanFactory,而且这个 entry 永远不会被删除(整个类里没有任何 registry.remove())。BeanFactory 活着 → 它的 singletonObjects 强引用所有单例 bean → 那些 bean 的 WeakReference 永远清不掉 →
isTargetBeanValid() 恒为 true → 老的 SpringValue 一个都删不掉。
scanAndClean 只能回收那些已经从 BeanFactory 缓存里被移除的实例(@RefreshScope 被 destroy 掉的 target、prototype)。
通过jmap -histo:live <pid> | grep SpringValue可以很明显的看到这个增长曲线。
而把config-center.auto-refresh=false关掉,SpringValue的增长停了。
问题已经很明显了。
解决办法
压缩Apollo SpringValueRegistry,回收 rebind 造成的重复SpringValue。
周期性地把「同一目标 Bean 实例 + 同一成员 + 同一 key」的重复 SpringValue 压成一份 (保留最后注册的一份)。
这里以目标 Bean 实例身份而非 beanName 作为去重维度是关键: SpringValueProcessor 对每个 Bean 实例都会登记(无 singleton 判断),因此 prototype / request / session 作用域的多个存活实例会共享同一 beanName 与同一成员。
若按 beanName 去重,会误删仍然存活的实例对应的 SpringValue,使其静默失去自动刷新能力。按实例身份去重后,被折叠的 一定指向同一对象,保留任意一份功能等价,对现有刷新逻辑零影响,只是把注册表占用从「无界」压回「有界」。
代码如下:
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
import com.ctrip.framework.apollo.spring.property.SpringValue;
import com.ctrip.framework.apollo.spring.property.SpringValueRegistry;
import com.ctrip.framework.apollo.spring.util.SpringInjector;
import com.google.common.collect.Multimap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
@ConditionalOnProperty(name = "config-center.auto-refresh", havingValue = "true", matchIfMissing = false)
public class SpringValueRegistryCompactor implements InitializingBean, DisposableBean {
/** 连续失败多少次后升级为 ERROR 日志(避免反射长期失效却只有 WARN、泄漏无声回归)。 */
private static final int FAILURE_ESCALATE_THRESHOLD = 5;
/** 重复项增长缓慢,无需高频扫描 —— 每轮都要持有 Apollo 注册表锁,间隔宁长勿短。 */
private static final long CLEAN_INTERVAL_IN_SECONDS = 60;
/** 独立单线程 daemon 调度器,不依赖 Spring @EnableScheduling、不占用业务调度线程。 */
private ScheduledExecutorService scheduler;
/** 懒加载的 Apollo 注册表单例:构造期解析会让 Guice 初始化失败直接拖垮上下文启动。 */
private volatile SpringValueRegistry springValueRegistry;
/** 反射缓存:SpringValueRegistry 内部的 registry 私有字段。 */
private volatile Field registryField;
/** 反射缓存:SpringValue 内部指向目标 Bean 实例的 beanRef 私有字段。 */
private volatile Field beanRefField;
/** 连续失败计数:一旦成功即清零,持续失败则升级告警。 */
private final AtomicInteger consecutiveFailures = new AtomicInteger();
/**
* 上轮压缩后各 BeanFactory 的注册表条目数,用于「未增长则跳过」短路。
* 弱键:BeanFactory 销毁后自动移除,不额外延长其生命周期。
*/
private final Map<BeanFactory, Integer> lastSizes = Collections.synchronizedMap(new WeakHashMap<>());
@Override
public void afterPropertiesSet() {
this.scheduler = Executors.newSingleThreadScheduledExecutor(
ApolloThreadFactory.create("SpringValueRegistryCompactor", true));
this.scheduler.scheduleAtFixedRate(this::compactQuietly,
CLEAN_INTERVAL_IN_SECONDS, CLEAN_INTERVAL_IN_SECONDS, TimeUnit.SECONDS);
log.info("SpringValueRegistryCompactor started, interval={}s.", CLEAN_INTERVAL_IN_SECONDS);
}
@Override
public void destroy() {
if (scheduler != null) {
scheduler.shutdownNow(); // Bean 销毁时关闭调度器,避免线程/执行器泄漏
}
}
/** 调度入口:吞掉一切异常,防止任务因抛异常被 scheduleAtFixedRate 静默终止。 */
private void compactQuietly() {
try {
compact();
} catch (Throwable ex) {
log.warn("SpringValueRegistryCompactor unexpected error: {}", ex.toString());
}
}
// 锁的是注册表内的共享 multimap 实例(与 Apollo scanAndClean 同一把锁),非普通局部变量,故抑制该误报
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
void compact() {
try {
Map<BeanFactory, Multimap<String, SpringValue>> registry = resolveRegistry();
if (registry == null || registry.isEmpty()) {
consecutiveFailures.set(0);
return;
}
int removed = 0;
for (Map.Entry<BeanFactory, Multimap<String, SpringValue>> registryEntry : registry.entrySet()) {
// springValues 是 Multimaps.synchronizedListMultimap(...) 包装实例:其内部 mutex 即自身
// (构造时未传外部 mutex),因此锁住该实例即与 Guava 每次 put()/get() 的内部锁互斥,
// 也与 Apollo scanAndClean() 的 synchronized(springValues) 一致。此依赖 Guava 内部行为,
// 若未来 Apollo 改用其它 Multimap 包装或显式外部 mutex,此互斥将失效,需同步调整。
Multimap<String, SpringValue> springValues = registryEntry.getValue();
synchronized (springValues) {
// 短路:条目数未超过上轮压缩后的水位,说明没有新增登记,无需全量扫描。
// size() 与压缩同处一把锁内,读到的水位与后续扫描一致。
Integer lastSize = lastSizes.get(registryEntry.getKey());
if (lastSize != null && springValues.size() <= lastSize) {
continue;
}
removed += compactOne(springValues);
lastSizes.put(registryEntry.getKey(), springValues.size());
}
}
consecutiveFailures.set(0);
if (removed > 0 && log.isDebugEnabled()) {
log.debug("SpringValueRegistry compacted, removed {} duplicated SpringValue(s).", removed);
}
} catch (Throwable ex) {
// 反射面属 Apollo 内部结构,单次失败仅跳过本轮、绝不影响业务;
// 但持续失败意味着压缩器已失效、SpringValue 泄漏会无声回归 —— 升级为 ERROR 以便告警。
int failures = consecutiveFailures.incrementAndGet();
if (failures >= FAILURE_ESCALATE_THRESHOLD) {
log.error("SpringValueRegistry compact FAILED {} consecutive times; "
+ "SpringValue leak protection is INACTIVE (Apollo internals may have changed).",
failures, ex);
} else {
log.warn("SpringValueRegistry compact skipped ({} consecutive): {}", failures, ex.toString());
}
}
}
/** 保留每个「目标实例 + 成员」最后注册的一份,删除其余重复项;返回删除数量。 */
private int compactOne(Multimap<String, SpringValue> springValues) throws Exception {
// 第一遍:LinkedListMultimap 保持插入顺序,后写覆盖 => winner 即最新注册的一份。
// 外层按实例身份分桶(IdentityHashMap 用 == 比较,不依赖 identityHashCode,无碰撞风险)
Map<Object, Map<String, SpringValue>> winners = new IdentityHashMap<>();
for (Map.Entry<String, SpringValue> entry : springValues.entries()) {
Object bean = targetBeanOf(entry.getValue());
if (bean == null) {
continue; // 目标已 GC,留给 Apollo scanAndClean
}
winners.computeIfAbsent(bean, k -> new HashMap<>())
.put(memberIdentity(entry.getKey(), entry.getValue()), entry.getValue());
}
// 第二遍:非 winner(指向同一实例的旧重复项)删除
int removed = 0;
Iterator<Map.Entry<String, SpringValue>> it = springValues.entries().iterator();
while (it.hasNext()) {
Map.Entry<String, SpringValue> entry = it.next();
Object bean = targetBeanOf(entry.getValue());
if (bean == null) {
continue;
}
Map<String, SpringValue> perBean = winners.get(bean);
if (perBean == null) {
continue; // 第一遍时目标尚存、此刻已 GC,同样留给 scanAndClean
}
if (perBean.get(memberIdentity(entry.getKey(), entry.getValue())) != entry.getValue()) {
it.remove();
removed++;
}
}
return removed;
}
/** 同一实例内的成员唯一标识:占位符 key + 字段/方法成员。 */
private String memberIdentity(String key, SpringValue springValue) {
String member;
if (springValue.isField()) {
Field field = springValue.getField();
member = "F:" + field;
} else {
MethodParameter mp = springValue.getMethodParameter();
member = "M:" + mp.getMethod() + '#' + mp.getParameterIndex();
}
return key + '|' + member;
}
/** 读取 SpringValue 的目标 Bean 实例;已被 GC 时返回 null。 */
private Object targetBeanOf(SpringValue springValue) throws NoSuchFieldException, IllegalAccessException {
Field field = beanRefField;
if (field == null) {
synchronized (this) {
field = beanRefField;
if (field == null) {
field = SpringValue.class.getDeclaredField("beanRef");
field.setAccessible(true);
beanRefField = field;
}
}
}
WeakReference<?> beanRef = (WeakReference<?>) field.get(springValue);
return beanRef == null ? null : beanRef.get();
}
@SuppressWarnings("unchecked")
private Map<BeanFactory, Multimap<String, SpringValue>> resolveRegistry() throws NoSuchFieldException, IllegalAccessException {
Field field = registryField;
if (field == null) {
synchronized (this) {
field = registryField;
if (field == null) {
field = SpringValueRegistry.class.getDeclaredField("registry");
field.setAccessible(true);
registryField = field;
}
}
}
return (Map<BeanFactory, Multimap<String, SpringValue>>) field.get(resolveSpringValueRegistry());
}
/** 懒解析 Apollo 注册表单例:失败被 compact() 的 catch 兜住,不影响业务与启动。 */
private SpringValueRegistry resolveSpringValueRegistry() {
SpringValueRegistry registry = springValueRegistry;
if (registry == null) {
synchronized (this) {
registry = springValueRegistry;
if (registry == null) {
registry = SpringInjector.getInstance(SpringValueRegistry.class);
springValueRegistry = registry;
}
}
}
return registry;
}
}
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

文章评论
从定位到根治完整闭环,干货满满的后端排障实录
@博客印象
踩坑踩多了