Skip to content

OutOfMemoryError on TomcatEmbeddedContext when trying to precompile JSP files #17927

@jagobagascon

Description

@jagobagascon

Affects: Spring Boot 2.1.0

We are precompiling our JSP files on application startup and we are getting an OutOfMemoryError when migrating our application from Spring Boot 2.0.4 to 2.1.0.

java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at java.util.ArrayList.<init>(ArrayList.java:153) ~[na:1.8.0_202]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext$$Lambda$349/790229674.apply(Unknown Source) ~[na:na]
	at java.util.Map.computeIfAbsent(Map.java:957) ~[na:1.8.0_202]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext.getLoadOnStartupWrappers(TomcatEmbeddedContext.java:75) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext.lambda$deferredLoadOnStartup$0(TomcatEmbeddedContext.java:65) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext$$Lambda$348/215632153.run(Unknown Source) ~[na:na]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext.doWithThreadContextClassLoader(TomcatEmbeddedContext.java:109) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext.deferredLoadOnStartup(TomcatEmbeddedContext.java:64) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.performDeferredLoadOnStartup(TomcatWebServer.java:282) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:200) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:300) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) [spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) [spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.0.RELEASE.jar:2.1.0.RELEASE]
	at com.example.demo.DemoApplication.main(DemoApplication.java:19) [classes/:na]

We do the precompilation by creating a servlet for each of our JSP files like this:

@Bean
public ServletContextInitializer preCompileJspsAtStartup() {
    return servletContext -> {
        foreachJSP(jspPath -> {
            ServletRegistration.Dynamic reg = servletContext.addServlet(jspPath, Constants.JSP_SERVLET_CLASS);
            reg.addMapping(jspPath);
        });
    };
}

Here servletContext.addServlet creates a new StandardWrapper for each of the files. This Wrapper sets the loadOnStartup value to -1 by default but returns Integer.MAX_VALUE for JSP servlets.

@Override
public int getLoadOnStartup() {
    if (isJspServlet && loadOnStartup < 0) {
        /*
         * JspServlet must always be preloaded, because its instance is
         * used during registerJMX (when registering the JSP
         * monitoring mbean)
         */
         return Integer.MAX_VALUE;
    } else {
        return this.loadOnStartup;
    }
}

The error is happening on TomcatEmbeddedContext because it is trying to create an ArrayList with the size of order (which in case of a JSP is Integer.MAX_VALUE).

73: int order = wrapper.getLoadOnStartup();
74: if (order >= 0) {
        // next line is calling new ArrayList<>(order) instead of new ArrayList<>()
75:     grouped.computeIfAbsent(order, ArrayList::new);
76:     grouped.get(order).add(wrapper);
77: }

You can avoid the error by simple explicitly setting the loadOnStartup value to a positive integer.

@Bean
public ServletContextInitializer preCompileJspsAtStartup() {
    return servletContext -> {
        foreachJSP(jspPath -> {
            ServletRegistration.Dynamic reg = servletContext.addServlet(jspPath, Constants.JSP_SERVLET_CLASS);
            reg.addMapping(jspPath);
            reg.setLoadOnStartup(99); // manually set to avoid problems
        });
    };
}

You can replicate the error by running this simple application: https://github.com/jagobagascon/Spring-Boot-OutOfMemoryError-Bug

Metadata

Metadata

Assignees

Labels

type: regressionA regression from a previous release

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions