Spring Boot version: 2.3.12.RELEASE, 2.7.8
I've tried to apply AOP ( cglib ) on prototype Bean. Since cglib generates new class every time, metaspace hit OOM at the end.
Below is my code for AOP and prototype Bean:
@Aspect("pertarget(this(pack.path.to.Bean))")
public class BeanAspect {
@Pointcut("execution(public void pack.path.to.BeanImpl.hi(..))")
public void hi() {}
@Around("hi()")
public Object before(ProceedingJoinPoint pjp) throws Throwable {
try {
System.out.println("before hi");
return pjp.proceed();
} finally {
System.out.println("after hi");
}
}
}
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public BeanAspect beanAspect() {
return new BeanAspect();
}
}
public interface Bean {
void hi();
}
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class BeanImpl implements Bean {
@Override
public void hi() {
System.out.println("hi~");
}
}
@Component
public class BeanFactory {
@Autowired
private ApplicationContext context;
public Bean create() {
return context.getBean(BeanImpl.class);
}
}
Below is my Runner
@Component
public class Runner {
@Autowired
private BeanFactory factory;
private final ExecutorService executorService = Executors.newFixedThreadPool(1);
@PostConstruct
public void init() {
executorService.execute(() -> {
while (true) {
Bean bean = factory.create();
System.out.println(bean.hashCode() + " " + bean.getClass());
bean.hi();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
I've referred to this PR, but it seems not work:
-
27375
Comment From: vishalsingh2972
@ZhangChee @rstoyanchev @sbrannen could you assign this to me if it's still open?
Comment From: backslideZhang
This case still exists.
I've tried to find out why this happened. Maybe this is the hint. cglib will create new class instance every time when a prototype bean instance is created. So as the system running, more prototype bean instance created, and more class instance created without recycling. Finally metaspace hit OOM. When it comes to singleton bean, no more bean instance is created, so no more cglib class instance created too. That's why this case hits Metasapce OOM but singleton won't happen.
The solution maybe: Every prototype instance shares the same cglib class instance ( Maybe we need relation cache between cglib class instance and corresponding Bean Class with @Component and @Scope=SCOPE_PROTOTYPE ? )
I've no authrization to reassign. Let's waiting... T T
Comment From: vishalsingh2972
This case still exists.
I've tried to find out why this happened. Maybe this is the hint. cglib will create new class instance every time when a prototype bean instance is created. So as the system running, more prototype bean instance created, and more class instance created without recycling. Finally metaspace hit OOM. When it comes to singleton bean, no more bean instance is created, so no more cglib class instance created too. That's why this case hits Metasapce OOM but singleton won't happen.
The solution maybe: Every prototype instance shares the same cglib class instance ( Maybe we need relation cache between cglib class instance and corresponding Bean Class with @component and @scope=SCOPE_PROTOTYPE ? )
I've no authrization to reassign. Let's waiting... T T
@ZhangChee @backslideZhang I see it is tagged as waiting-for-triage, so maybe no use of us contributing some internal error and maybe core spring team will handle this.
Comment From: ArvindAbi
I m facing this issue as well, Any workaround for this till it get fixed ?
@vishalsingh2972 , Can you point me to a workaround if you found one?
Comment From: ArvindAbi
From my observation, the Proxies are not GCed even though the underlying Target Source is GCed, Attaching an example where one of the Proxy Generated is GCed but there are two other proxies still in Heap causing memory leak.
I don't find a way to manually destroy these proxy objects as well. Any help would be helpful.
Comment From: ArvindAbi
@ZhangChee , have you found any workaround/ fix for it temporarily?
Comment From: ZhangChee
@ArvindAbi I give up using aop on prototype bean. Instead, I inject another service with aop to the prototype bean
Comment From: snicoll
Everyone, the creation of a new bean using the prototype scope should not lead to the creation of a new CGLIB class. If that happens, something unexpected happened that we need to debug. In order to do that, please share a small sample that we can run ourselves with instructions to trigger the problem. You can attach a zip to this issue or push the code to a separate GitHub repository. Thank you!
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.
Comment From: dcaillia
We observe the same problem: in spring 5, all worked fine, in spring 6.0.15, we have (after some days uptime) thousands of classes named like this (with 0 instances in heapdump) where the trailing number goes up and up (growing metaspace from 300M to 8G)
com.our.company.SomePrototypeScopedBean$$SpringCGLIB$$43717
We're using
I understand saying "me too" is not helping to reproduce the problem.
It looks like the problem is near this code as this cache keeps on growing.
org.springframework.cglib.core.internal.LoadingCache.get(K)
public V get(K key) {
final KK cacheKey = keyMapper.apply(key);
Object v = map.get(cacheKey);
if (v != null && !(v instanceof FutureTask)) {
return (V) v;
}
return createEntry(key, cacheKey, v);
}
The cacheKey is org.springframework.cglib.proxy.Enhancer.EnhancerKey (which was an interface in spring 5, and a record in spring 6). Maybe this rings a bell somewhere.
While debugging i find many almost-duplicate cache keys of that type in the map, where they only differ in filter.referent.advised (in particular their fields advisors and advisorKey seem to be non-equal while they look equal - advisors[i].methodMatcherKey).
Looking in org.springframework.aop.MethodMatcher i find javadoc:
WARNING: Concrete implementations of this interface must
- provide proper implementations of {@link Object#equals(Object)},
- {@link Object#hashCode()}, and {@link Object#toString()} in order to allow the
- matcher to be used in caching scenarios — for example, in proxies generated
- by CGLIB. As of Spring Framework 6.0.13, the {@code toString()} implementation
- must generate a unique string representation that aligns with the logic used
- to implement {@code equals()}. See concrete implementations of this interface
- within the framework for examples.
In our codebase i found this
private Pointcut pointcut =
new Pointcut() {
@Override
public ClassFilter getClassFilter() {
return anyClass -> true;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MethodMatcher() {
@Override
public boolean matches(Method method, Class<?> aClass) {
...
When i change it to this
final MethodMatcher mm = new MethodMatcher() {
@Override
public boolean matches(Method method, Class<?> aClass) {
...
private Pointcut pointcut =
new Pointcut() {
@Override
public ClassFilter getClassFilter() {
return anyClass -> true;
}
@Override
public MethodMatcher getMethodMatcher() {
return mm;
}
the problem is gone.