Description
Christian Schreder opened SPR-10785 and commented
Overview
Recently I recognized strange behavior in a project with around 700 tests running via Maven in one JVM. Some of the tests have different application contexts defined. As I run the tests I get an OutOfMemoryException
. I managed to break it down to one place where a bean with method injection is defined. If I comment it out I have constant memory usage. With it, memory consumption is increasing. Reproduced on Windows 7 using JRockit 1.6.
For further investigation I wrote a reproducer. It is a simple maven project defining two beans where one bean has a method injection. I have 4 test classes with 1000 test methods per class and after each method the application context is rebuilt (via the @DirtiesContext
annotation). Also I included a log file with activated gc-logging. I was also able to reproduce this behavior on Mac OS 10.6.8 with Sun's JDK 1.6.
Assumptions
ApplicationContext
with method injection.- Several tests with different application contexts or tests which dirty the application context => what matters is that application contexts get built often.
Steps to Reproduce
Run all tests in one JVM (e.g. with maven).
Result
Memory consumption constantly increases until an OutOfMemoryException
is thrown.
Analysis
The following table displays execution time, the number of classes loaded, and memory consumption when the test suite in the attached ZIP file are executed against various Java and Spring versions.
Spring 3.2.x
|| Spring Version || Java Version || Time || Classes || Memory |
| 3.2.7.RELEASE
| JDK 1.6 | 1:54 | 6700 | 54 / 86 / 86 |
| 3.2.7.RELEASE
| JDK 1.7 | 1:51 | 6629 | 45 / 82 / 86 |
| 3.2.7.RELEASE
| JDK 1.8 | 1:44 | 6670 | 43 / 44 / 1082 |
| 3.2.8.BUILD-SNAPSHOT
| JDK 1.6 | 1:42 | 2716 | 21 / 22 / 86 |
| 3.2.8.BUILD-SNAPSHOT
| JDK 1.7 | 1:31 | 2629 | 17 / 22 / 86 |
| 3.2.8.BUILD-SNAPSHOT
| JDK 1.8 | 1:24 | 2683 | 17 / 18 / 1082 |
Spring 4.0.x
|| Spring Version || Java Version || Time || Classes || Memory |
| 4.0.1.RELEASE
| JDK 1.6 | 2:27 | 6761 | 54 / 83 / 86 |
| 4.0.1.RELEASE
| JDK 1.7 | 2:22 | 6681 | 46 / 78 / 86 |
| 4.0.1.RELEASE
| JDK 1.8 | 2:09 | 6739 | 43 / 44 / 1082 |
| 4.0.2.BUILD-SNAPSHOT
| JDK 1.6 | 1:38 | 2758 | 22 / 22 / 86 |
| 4.0.2.BUILD-SNAPSHOT
| JDK 1.7 | 1:56 | 2675 | 17 / 22 / 86 |
| 4.0.2.BUILD-SNAPSHOT
| JDK 1.8 | 1:39 | 2741 | 18 / 19 / 1082 |
Key
- OS: Mac OS X 10.9.1
- Java Versions:
JDK Version 1.6 1.6.0_65-b14-462-11M4609 1.7 1.7.0_51-b13 1.8 1.8.0-b129 - Time: in minutes
- Classes: represent the number of classes loaded.
- Memory: values are rounded and represent Used/Size/Max memory * 1,000,000 Bytes. For JDK 1.6 and 1.7, memory represents PermGen usage. For JDK 1.8, memory represents Metaspace usage.
Conclusion
As can be seen in the Analysis section, the fixes in Spring Framework 3.2.8.BUILD-SNAPSHOT
and 4.0.2.BUILD-SNAPSHOT
result in constant memory consumption and zero unnecessary CGLIB class generation (i.e., no longer one CGLIB class per method injected bean instance).
The following screenshots provide a visual comparison for Java 6.
Before Fix (Spring 3.2.7)
!memory usage - JDK 1.6 - Spring 3.2.7.png|thumbnail!
After Fix (Spring 4.0.2)
!memory usage - JDK 1.6 - Spring 4.0.2.png|thumbnail!
Affects: 3.0 GA
Attachments:
- memory usage - JDK 1.6 - Spring 3.2.7.png (73.16 kB)
- memory usage - JDK 1.6 - Spring 4.0.2.png (61.34 kB)
- memtest.zip (92.08 kB)
Issue Links:
- Inclusion of
overloaded
inequals()
andhashCode()
forMethodOverride
breaksequals()
inAbstractBeanDefinition
[SPR-11420] #16047 Inclusion of 'overloaded' in equals() and hashCode() for MethodOverride breaks equals() in AbstractBeanDefinition ("depends on") - MemoryLeak in Cglib2AopProxy.ProxyCallbackFilter [SPR-8008] #12663 MemoryLeak in Cglib2AopProxy.ProxyCallbackFilter
- Inclusion of
overloaded
inequals()
andhashCode()
forMethodOverride
breaksequals()
inAbstractBeanDefinition
[SPR-11420] #16047 Inclusion of 'overloaded' in equals() and hashCode() for MethodOverride breaks equals() in AbstractBeanDefinition - @Async with cglib based proxy causes memory leak in heap [SPR-11275] #15899
@Async
with cglib based proxy causes memory leak in heap - Add ability to create proxy around classes that has no default constructor [SPR-10594] #15223 Add ability to create proxy around classes that has no default constructor
Referenced from: commits 8028eae, 1ae3eba, f2a4537, bc87910
Backported to: 3.2.8
Activity
spring-projects-issues commentedon Feb 8, 2014
Sam Brannen commented
Hi Christian,
Thanks for putting together the project for reproducing this behavior.
On a Mac, I was not able to reproduce the problem with JDK 7 or JDK 8 running against Spring Framework 4.0.2 (snapshot); however, I was able to reproduce it when running against JDK 6 on the same machine.
Thus it appears to chiefly be an issue with garbage collection in the JVM for Java 6.
However, the core of the issue likely lies in the fact that CGLIB is used to dynamically create subclasses of the bean class for which the method is injected. Specifically, the two variants of
instantiateWithMethodInjection()
inCglibSubclassingInstantiationStrategy
do not cache the CGLIBEnhancer
. As a consequence, a new dynamic subclass is created for every creation of the bean in question (i.e., every time theApplicationContext
is started), and these generated subclasses remain in the class loader and do not get garbage collected. This might not be avoidable since the underlyingBeanFactory
is in fact a different instance each time, but there still might be some room for improvement here.We will continue to investigate the issue.
Regards,
Sam
spring-projects-issues commentedon Feb 8, 2014
Sam Brannen commented
This potentially relates to #15899 in terms of recreation of identical CGLIB proxy classes.
spring-projects-issues commentedon Feb 11, 2014
Sam Brannen commented
In case you're interested, I have pushed some work I've been doing on this issue to the following branch:
https://github.com/sbrannen/spring-framework/commits/SPR-10785
There are a few things to fine tune, but in general this approach seems to take care of the memory leaks associated with method injection.
spring-projects-issues commentedon Feb 12, 2014
Sam Brannen commented
Resolving this issue as Fixed.
The fix was actually performed as a positive side-effect of #16047; however, I am leaving this issue assigned to both 3.2.8 and 4.0.2 since the issue summary is more explanatory than that for #16047.
spring-projects-issues commentedon Feb 13, 2014
Sam Brannen commented
Although this issue has been fixed (see the Analysis and Conclusion sections above), we would like to point out that the Spring Team no longer recommends heavy usage of method injection.
As a modern alternative to method injection, please consider using the
javax.inject.Provider<T>
API instead. With Spring you can inject aProvider
into a component and then callprovider.get()
to get an instance of the typeT
of object provided by that provider.spring-projects-issues commentedon Oct 16, 2015
Piotr Findeisen commented
Hi,
I'm migrating the application from Spring 3 to 4. It appears, changes here are not backwards compatible.
The difference manifest when bean has
lookup-method
and calls it in its constructor. Previously this worked, asCglibSubclassCreator
set callbacks before instantiating. Now, during instantiation the stack looks like (oldest frame on top):AbstractMethodError
Of course, for every found case of this problem, the fix is as simple as moving the ctor code to
@PostConstruct
- unless this is up in the inheritance chain and subclasses' ctors rely on parent ctor having init'ed the data. Nonetheless, this is backwards incompatible change, and perhaps should be mentioned on the migration documentation page (I don't think it was mentioned there)spring-projects-issues commentedon Oct 16, 2015
Sam Brannen commented
Piotr Findeisen,
Since this is a closed issue, would you please create a new JIRA issue describing your findings?
Please also mention this issue in the new issue's description, and feel free to assign the issue to me.
Thanks!
Sam
spring-projects-issues commentedon Oct 17, 2015
Piotr Findeisen commented
Sam Brannen,
I guess this change of behavior is simply the cost of fixing the mem leak (#15411). I'm not very inclined to report bug that cannot be fixed. Not sure it's worth it.
spring-projects-issues commentedon Oct 17, 2015
Sam Brannen commented
Makes sense. Thanks for the feedback!
overloaded
inequals()
andhashCode()
forMethodOverride
breaksequals()
inAbstractBeanDefinition
[SPR-11420] #16047