I am using SpringBoot version 2.3.4.RELEASE. With a custom ClassLoader, I load external Jar files' classes into memory, and inject them into the Spring container for management. Upon examining the source code, I discovered that Spring combines properties of a GenericBeanDefinition object into a new RootBeanDefinition object while constructing the bean.

Later, I unregister the bean from the Spring container, but Spring does not remove the RootBeanDefinition object. Since the beanClass attribute of the RootBeanDefinition object points to the loaded Class, the Class cannot be garbage collected, which in turn prevents the custom ClassLoader object from being collected, causing a memory leak.

The code is provided below:

package org.microboot.test.web.controller;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.http.HttpServletRequest;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

/**
 * @author hp
 */
@Controller
@RequestMapping(value = "/web5")
public class TestController {

    private final String mappingClassJar = "your.jar";

    private final String mappingClassName = "YourTestFunction";

    private final Map<String, Function<String, String>> funcMap = new ConcurrentHashMap<>();

    @RequestMapping("test1.html")
    @ResponseBody
    public String test1(HttpServletRequest request) throws Exception {
        JarLoadUtils.getInstance().loadJarByCustom(mappingClassJar);

        ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());

        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;

        return test1BySpring(configurableApplicationContext);

        // return test1();
    }

    @RequestMapping("test2.html")
    public void test2(HttpServletRequest request) throws Exception {
        JarLoadUtils.getInstance().unloadJarByCustom(mappingClassJar);

        ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());

        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;

        test2BySpring(configurableApplicationContext);

        // test2();
    }

    private String test1BySpring(ConfigurableApplicationContext configurableApplicationContext) throws Exception {
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

        String beanName = getBeanName(mappingClassJar, mappingClassName);

        boolean bool = defaultListableBeanFactory.containsBeanDefinition(beanName);

        if (!bool) {
            Class<Function<String, String>> funcClazz = JarLoadUtils.getInstance().loadClassByCustom(mappingClassJar, mappingClassName);

            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(funcClazz);

            defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());

            return "success";
        }
        return "bean exists";
    }

    private void test2BySpring(ConfigurableApplicationContext configurableApplicationContext) throws Exception {
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

        String beanName = getBeanName(mappingClassJar, mappingClassName);

        System.out.println("打印:" + configurableApplicationContext.getBean(beanName, Function.class).apply("your param").toString());

        if (defaultListableBeanFactory.containsBeanDefinition(beanName)) {
            defaultListableBeanFactory.removeBeanDefinition(beanName);
        }
    }

    private String test1() throws Exception {
        String beanName = getBeanName(mappingClassJar, mappingClassName);

        boolean bool = funcMap.containsKey(beanName);

        if (!bool) {
            Class<Function<String, String>> funcClazz = JarLoadUtils.getInstance().loadClassByCustom(mappingClassJar, mappingClassName);

            Function<String, String> funcInstance = funcClazz.newInstance();

            funcMap.computeIfAbsent(beanName, bn -> funcInstance);

            return "success";
        }
        return "bean exists";
    }

    private void test2() throws Exception {
        String beanName = getBeanName(mappingClassJar, mappingClassName);

        System.out.println("打印:" + funcMap.get(beanName).apply("your param"));

        if (funcMap.containsKey(beanName)) {
            funcMap.remove(beanName);
        }
    }

    private static String getBeanName(String jar, String className) {
        return jar + "$" + className;
    }

    private static class JarLoadUtils {

        private static final JarLoadUtils instance = new JarLoadUtils();

        private final JarLoadUtils.InnerURLClassLoaderManager manager = new JarLoadUtils.InnerURLClassLoaderManager();

        private JarLoadUtils() {
        }

        public static JarLoadUtils getInstance() {
            return instance;
        }

        public void loadJarByCustom(String jar) throws Exception {
            this.manager.loadJar(jar);
        }

        public void unloadJarByCustom(String jar) {
            this.manager.unloadJar(jar);
        }

        @SuppressWarnings("rawtypes")
        public Class loadClassByCustom(String jar, String className) throws Exception {
            return this.manager.loadClass(jar, className);
        }

        private final class InnerURLClassLoaderManager {

            private final ConcurrentHashMap<String, JarLoadUtils.InnerURLClassLoader> LOADER_CACHE = new ConcurrentHashMap<>();

            private InnerURLClassLoaderManager() {
            }

            private void loadJar(String jar) throws MalformedURLException {
                JarLoadUtils.InnerURLClassLoader urlClassLoader = LOADER_CACHE.get(jar);

                if (urlClassLoader != null) {
                    return;
                }

                JarLoadUtils.InnerURLClassLoader innerURLClassLoader = new JarLoadUtils.InnerURLClassLoader();

                urlClassLoader = LOADER_CACHE.computeIfAbsent(jar, j -> innerURLClassLoader);

                if (urlClassLoader != innerURLClassLoader) {
                    return;
                }

                URL jarUrl = new URL("jar:file:/" + jar + "!/");

                urlClassLoader.loadJar(jarUrl);
            }

            private void unloadJar(String jar) {
                JarLoadUtils.InnerURLClassLoader urlClassLoader = LOADER_CACHE.get(jar);

                if (urlClassLoader == null) {
                    return;
                }

                urlClassLoader.unloadJar();

                LOADER_CACHE.remove(jar);
            }

            @SuppressWarnings("rawtypes")
            private Class loadClass(String jar, String className) throws ClassNotFoundException {
                JarLoadUtils.InnerURLClassLoader urlClassLoader = LOADER_CACHE.get(jar);

                if (urlClassLoader == null) {
                    return null;
                }

                return urlClassLoader.loadClass(className);
            }
        }

        private final class InnerURLClassLoader extends URLClassLoader {

            private InnerURLClassLoader() {
                super(new URL[]{}, Thread.currentThread().getContextClassLoader());
            }

            private void loadJar(URL url) {
                this.addURL(url);
            }

            private void unloadJar() {
                try {
                    this.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Build a normal SpringBoot web project, and use the above Controller. test1.html injects the bean, test2.html logs out the bean.

Use the jvisualvm tool to monitor the recycling status of the InnerURLClassLoader object. test1BySpring and test2BySpring are tests when the Spring container is used. test1 and test2 are tests when the Spring container is not used.

When the Spring container manages the bean, the InnerURLClassLoader object cannot be garbage collected. When the Spring container does not manage the bean, the InnerURLClassLoader object can be garbage collected.

Comment From: bclozel

Hello,

First you should know that Spring Boot 2.3.x is out of support and has several known patched CVEs. Please upgrade as soon as possible.

Now back to the issue: we do not consider this a valid use case. Applications are not meant to add/remove bean definitions once the application context has been refreshed. At this point, bean instances have been created and injected in many places. Even if we "cleaned up" all possible definition entries, bean instances would be still referenced by the application itself.

You can look at the Javadoc of the bean definition registry methods: it highlights the fact that they only make sense when the application context is not fully configured.

Comment From: hp525350557

Hello,

First you should know that Spring Boot 2.3.x is out of support and has several known patched CVEs. Please upgrade as soon as possible.

Now back to the issue: we do not consider this a valid use case. Applications are not meant to add/remove bean definitions once the application context has been refreshed. At this point, bean instances have been created and injected in many places. Even if we "cleaned up" all possible definition entries, bean instances would be still referenced by the application itself.

You can look at the Javadoc of the bean definition registry methods: it highlights the fact that they only make sense when the application context is not fully configured.

Thanks

We have a function similar to patching or installing plugins, which is that after the program is running, there may be external Jars to provide some additional functions, but these functions may also be offline after use. Therefore, if all the functions corresponding to the classes in the Jar are offline, the ClassLoader that loads them should also be recycled

In fact, if we create objects directly through reflection, after all the objects are cleared, ClassLoader can be recycled, and we are currently doing the same. Just before, we used Spring to manage this class and create beans. When a bean is logged out of Spring and recycled by GC, its corresponding ClassLoader object cannot be recycled, causing a memory leak