Summary
When resolving a resource path Spring creates a class path resource for the URI path. In use cases where the application is hit with a variety of request paths that are not mapped in a controller, this results in memory being leaked over time as these resources are not collected.
Steps to reproduce
Create a new spring boot application with a single controller endpoint. Use a script to generate HTTP requests to hit your spring boot app. The request paths must be randomly generated and miss your controller, resulting in a 404 response from Spring. With request rates of 1000 req/s it took me only a few minutes to see massive amounts of leaked memory.
Likely Cause
If you look at PathResourceResolver::getResource
on the line that reads Resource resource = location.createRelative(resourcePath);
, this creates a class path resource for every incoming request. After a while, this builds up resulting in lots of leaked resource paths.
My setup
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>playground</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>playground</name>
<description>playground</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
PlaygroundApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import reactor.core.publisher.Mono;
@SpringBootApplication
@Controller("/")
public class PlaygroundApplication {
@GetMapping("test")
public Mono<String> doTest() {
return Mono.just("test");
}
public static void main(String[] args) {
SpringApplication.run(PlaygroundApplication.class, args);
}
}
Class I wrote to generate requests
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class QuickTester {
private static final Random RND = new Random();
private static final ScheduledExecutorService timerExecutor = Executors.newScheduledThreadPool(40);
public static void main(String[] args) {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
timerExecutor.scheduleAtFixedRate(() -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(String.format("http://localhost:8080/%d",
RND.nextInt(1000000000))))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
try {
client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception ignored) {}
}, 0, 1, TimeUnit.MILLISECONDS);
}
}
Comment From: bclozel
What do you mean by "they are not collected"? Are those never cleaned by the garbage collector? Do you know what's keeping a reference to those? Did you connect the application to a profiler? If you gathered information there, could you join them to this issue?
Comment From: LoganEarl
Yea, I have a heap dump handy. Here
It looks like I might have barked up the wrong tree, I see reactor netty keeping references.
Comment From: bclozel
@LoganEarl that's strange, this shouldn't be keeping references. Are those weak references maybe? Do they go away if you trigger the GC manually in your profiler?