Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Method injection causes memory leak [SPR-10785] #15411

Closed
spring-projects-issues opened this issue Jul 26, 2013 · 9 comments
Closed

Method injection causes memory leak [SPR-10785] #15411

spring-projects-issues opened this issue Jul 26, 2013 · 9 comments

Comments

@spring-projects-issues
Copy link
Collaborator

@spring-projects-issues spring-projects-issues commented Jul 26, 2013

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:

Issue Links:

  • #16047 Inclusion of 'overloaded' in equals() and hashCode() for MethodOverride breaks equals() in AbstractBeanDefinition ("depends on")
  • #12663 MemoryLeak in Cglib2AopProxy.ProxyCallbackFilter
  • #16047 Inclusion of 'overloaded' in equals() and hashCode() for MethodOverride breaks equals() in AbstractBeanDefinition
  • #15899 @Async with cglib based proxy causes memory leak in heap
  • #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

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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() in CglibSubclassingInstantiationStrategy do not cache the CGLIB Enhancer. As a consequence, a new dynamic subclass is created for every creation of the bean in question (i.e., every time the ApplicationContext is started), and these generated subclasses remain in the class loader and do not get garbage collected. This might not be avoidable since the underlying BeanFactory 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Feb 8, 2014

Sam Brannen commented

This potentially relates to #15899 in terms of recreation of identical CGLIB proxy classes.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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 a Provider into a component and then call provider.get() to get an instance of the type T of object provided by that provider.

@spring-projects-issues
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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, as CglibSubclassCreator set callbacks before instantiating. Now, during instantiation the stack looks like (oldest frame on top):

  • cglib-generated subclass ctor
  • user class ctor
  • cglib-generated override of the abstract method -- but the interceptor is not (yet) registered, so it forwards the call to super
  • user class's abstract method call cases 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented 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
Copy link
Collaborator Author

@spring-projects-issues spring-projects-issues commented Oct 17, 2015

Sam Brannen commented

Makes sense. Thanks for the feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants