Bug 58023

Summary: Memory Leak in WebappClassLoader
Product: Tomcat 7 Reporter: Matheus <mn>
Component: CatalinaAssignee: Tomcat Developers Mailing List <dev>
Status: RESOLVED FIXED    
Severity: normal    
Priority: P2    
Version: 7.0.59   
Target Milestone: ---   
Hardware: PC   
OS: All   

Description Matheus 2015-06-11 12:59:17 UTC
Memory leak occur because a reference of entry.manifest is never removed.

Problem:
1 - ${class}.class.getResource( ${resource.name1} );
2 - JarFile load a manifest.
3- resourceEntries put a new ResourceEntry with a manifest reference.
4 - ${class}.class.getResource( ${resource.name2} );
5 - resourceEntries put a new ResourceEntry with a manifest reference. Manifest contains ~20MB, but, no problem because the JarFile is the same reference. In other words, only one Manifest in the Heap space.
6 - wait for 90000 milliseconds.
7 - ${class}.class.getResource( ${resource.name3} );
8 - WebappClassLoader.closeJARs because time is elapsed 90000 milliseconds and load a new JarFile instances.
9 - JarFile load a new manifest.
10 - resourceEntries put a new ResourceEntry with a new manifest reference.
11 - wait for 90000 milliseconds
...
And this will be memory leak in little time (See resourceEntries.png).
In attachment, a scenario to simulate this problem.

PS.: A jar file need to be signed for accelerate leak, because of Manifest.entries ~5MB (see manifests.png) retained heap (in test case, but, my real app is ~20MB).
I attached the heap dump used in this test case (heap.zip).

Solution:
Always release the manifest reference.

Workaround:
public class WebappClassLoader
    extends 
        org.apache.catalina.loader.WebappClassLoader
{
    @Override
    public InputStream getResourceAsStream( String name )
    {
        InputStream in = super.getResourceAsStream( name ); 
        
        ResourceEntry entry = resourceEntries.get( name );
        
        if ( entry != null )
        {
            // prevent a memory leak
            entry.manifest = null;
            entry.certificates = null;
        }
        
        return in;
    }
    
    @Override
    public URL getResource( String name )
    {
        URL url = super.getResource( name );
        
        ResourceEntry entry = resourceEntries.get( name );
        
        if ( entry != null )
        {
            // prevent a memory leak
            entry.manifest = null;
            entry.certificates = null;
        }
        
        return url;
    }
}
Comment 1 Mark Thomas 2015-06-16 19:25:43 UTC
Thanks for the report. Strictly I would class this as excessive memory usage rather than a leak since a) there is an upper limit to the memory that will be used and b) the memory will be recovered once the application has been undeployed. Regardless of how it is classified, it is a bug.

I have applied an alternative fix (only request and retain the manifest while loading a class and then drop it) that should achieve the same ends for trunk (9.0.x), 8.0.x (for 8.0.24 onwards) and 7.0.x (for 7.0.63 onwards).