Memory growth when repeatedly loading ApplicationContext with @Transactional beans lacking interfaces
0 reputation · 22 Apr 2022, 04:25 UTC
0 reputation · 22 Apr 2022, 04:25 UTC
Goal: determine whether the default CGLIB proxy created for @Transactional methods on beans without interfaces causes observable memory growth when ApplicationContext instances are repeatedly instantiated in a test loop without being closed.
Uncertainty: Spring documentation notes that the proxy-target-class attribute can force JDK dynamic proxies, but the default selection remains unspecified, making it unclear if switching to JDK proxies eliminates the leak or if the leak stems from proxy lifecycle independent of context closure.
Does setting proxy-target-class to false stop the heap increase? Is the memory retention tied to the CGLIB proxy holding a reference to the target bean even after the bean is eligible for GC? What configuration yields deterministic proxy selection while preserving transactional semantics?
34775 reputation · 22 Apr 2022, 10:42 UTC
When a new ApplicationContext is created repeatedly in a test loop without closing it, each context keeps its own CGLIB proxy for every @Transactional bean that has no interface. The proxy holds a strong reference to the target bean, so the entire bean graph remains reachable until the context is destroyed. This is the source of the observable heap increase.
proxy-target-class=true (CGLIB subclassing).proxy-target-class=false forces Spring to use JDK dynamic proxies. Those require at least one interface; otherwise a BeanCreationException is thrown and the context will not start.BeanFactory retains the CGLIB proxy and the target instance, keeping them reachable and preventing GC.try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
// use ctx
}
proxy-target-class=false and an interface is present. The leak is still prevented by closing the context.@Transactional annotation to a separate service that does have an interface.ApplicationContext closed after each loop iteration?proxy-target-class=false is used, does the bean implement at least one interface?Answering these questions will tell you whether the heap increase is due to the proxy strategy or simply the context lifecycle.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 22 Apr 2022, 04:56 UTC
When you spin up a fresh ApplicationContext in a loop without closing it, Spring creates a new CGLIB subclass for every @Transactional bean that has no interface. The classloader that defines that subclass is kept alive by the context, so the entire class hierarchy stays reachable even after the bean instance itself could otherwise be collected.
The proxy object itself is not the problem – it merely holds a reference to the target. If you set proxy-target-class=false and the bean lacks an interface, Spring simply does not create a proxy at all, so the transactional semantics are lost. The memory growth therefore persists because the per‑context CGLIB ClassLoader remains bound to the context until context.close() is called.
Practical fix: close the context after each iteration, or reuse a single context for the whole test run. If you need a long‑lived context, add an interface to the bean and keep proxy-target-class=true – the proxy will be a JDK proxy that still holds the target reference, but the context‑level classloader will be the same and no new classes will be loaded each time.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
for (int i = 0; i < 100; i++) {
// use ctx
}
ctx.close();