When Memory Overflows: Too Many ApplicationContexts in Spring Integration Tests
Spring Boot makes integration testing incredibly convenient: you have the entire application up and running in your test. But sometimes you can see memory usage spikes.
Join the DZone community and get the full member experience.
Join For FreeIn Spring, the ApplicationContext is the central container object that manages all beans (i.e., components, services, repositories, etc.).
Its tasks include reading the configuration (Java Config, XML, annotations), creating and managing bean instances, handling dependency injection, and running the application lifecycle.
When you write an integration test with Spring, for example:
@SpringBootTest
class MyServiceIntegrationTest {
@Autowired
private MyService myService;
@Test
void testSomething() {
assertThat(myService.doWork()).isEqualTo("done");
}
}
Then Spring starts a full ApplicationContext for this test — similar to when the real application starts, but within the test process. Internally, it works like this:
Spring uses the Spring TestContext Framework (org.springframework.test.context).
This framework ensures that for each test class:
- The relevant annotations (e.g.,
@SpringBootTest,@ContextConfiguration,@ActiveProfiles) are read. - A configuration is created from them (which beans, which properties, etc.).
- An
ApplicationContextis initialized.
The crucial point: Spring caches ApplicationContext between tests if the configuration is identical. That means: if two tests use the same configuration, the ApplicationContext will be reused instead of being rebuilt. This saves a lot of startup time.
For example, if two tests use the same context (as long as no different profiles, properties, etc. are used)
@SpringBootTest
class UserServiceIntegrationTest { ... }
@SpringBootTest
class OrderServiceIntegrationTest { ... }
The ApplicationContext is cached and used in both tests. However, if you specify a different profile, for example,
@SpringBootTest(properties = "app.featureX=true")
class FeatureXTest { ... }
Then a new container is created that contains the entire application. Spring does not destroy the context after each test, but instead keeps it in memory (in a static cache map).
In larger Spring test suites, when there are many different ApplicationContext Spring keeps all of them in the TestContext cache; the available memory (heap) may no longer be sufficient.
A typical error message — depending on the situation and JVM version — might look like this:
java.lang.OutOfMemoryError: Java heap space
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:...)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:...)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:...)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:...)
...
Cause:
- Each test loads its own Spring configuration (for example, different
@SpringBootTest(properties = …), profiles, etc.) - Spring caches all of them in the
DefaultContextCache - The cache grows → the heap runs out of memory
Notice, this variant of the error message appears from time to time:
java.lang.OutOfMemoryError: GC overhead limit exceeded
This happens when the garbage collector tries to free up memory, but almost all of it is occupied by the context cache, and no progress can be made.
This article now focuses on what solutions are available when such an error occurs — how to avoid or eliminate the effect when it happens.
How to Detect the Problem Early
Even during the development of integration tests, you can take steps to prevent this error by actively monitoring the number of cached ApplicationContexts.
For example, by analyzing logs, you can detect early on when the test cache is approaching its limits and take countermeasures in time. Spring provides ways to monitor the TestContext cache:
import org.springframework.test.context.cache.ContextCache;
import org.springframework.test.context.cache.DefaultContextCache;
public class CacheMonitor {
public static void printCacheStats() {
ContextCache cache = new DefaultContextCache();
System.out.println("Context cache size: " + cache.size());
}
}
or via Spring Properties:
logging.level.org.springframework.test.context.cache=DEBUG
There are several ways to solve it. These will be discussed in the next section.
Strategies for Solving the Problem
There are several approaches to solve or avoid the heap problem:
- Avoid unnecessary different contexts.
- Combine tests with the same
@SpringBootTestconfigurations - Avoid unnecessary properties or profiles per test class
- Combine tests with the same
- Use
@DirtiesContextsparingly.- Because it invalidates the cache and leads to more context rebuilds
- Use slice tests (
@DataJpaTest,@WebMvcTest, etc.).- These load only partial contexts
- Increase the heap size (e.g.,
-Xmx2g). - Explicitly clear the Spring TestContext cache (e.g., using a custom
TestExecutionListener).
Let’s take a closer look at some of these approaches.
Standardize Configurations
Avoid unnecessary variations in test annotations. For example:
@SpringBootTest(properties = "feature.x=true")
@SpringBootTest(properties = "feature.y=true")
A better approach to avoid duplicate ApplicationContexts is:
@SpringBootTest
@ActiveProfiles("test")
Use Slice Tests
Instead of loading the entire ApplicationContext, use Spring Slice Tests for specific parts of your application:@DataJpaTest for repositories, @WebMvcTest for controllers, @JsonTest for ObjectMapper. These slice tests load only partial contexts, significantly reducing memory usage.
Only Use @DirtiesContext When Necessary
@DirtiesContext invalidates the cached context, forcing a rebuild in subsequent tests. Only use @DirtiesContext if you modify the context during the test (e.g., changing the database state, publishing application events).The @DirtiesContext-Annotation in Spring tests can be very helpful if you want to ensure that it will be reloaded after a test, because the test may have put it into a state that could affect the test run or other tests. However, there are a few important reasons why @DirtiesContext it should be used sparingly.
The biggest disadvantage of using @DirtiesContext is that Spring reloads the entire ApplicationContext after each test that uses this annotation. This not only takes time but also consumes a significant amount of resources, since the entire context — including all beans and their dependencies — has to be rebuilt. Normally, tests should be isolated and independent from each other. When you use @DirtiesContext, the context is reset, which sometimes means that tests no longer share the same context, potentially causing unexpected side effects.
If the context is repeatedly reloaded, it can also make it harder to identify correlations between tests, which in the long run can affect the maintainability and readability of your tests.
@DirtiesContext causes Spring to release all resources used during the test (e.g., database connections, caches, network resources) when the context is reloaded. If you have many tests in use, this can lead to unnecessary overhead, consuming both memory and resources.
This is especially true for tests that heavily interact with external resources (like databases or web servers), as it can slow down test execution.
When tests use @DirtiesContext, it becomes more difficult to run tests in parallel because the context must be reloaded for each test case. In a CI/CD pipeline or in larger test suites where you want to run tests in parallel, @DirtiesContext it often introduces the risk of collisions and reduces the efficiency of test execution.
Often, @DirtiesContext is used simply because it’s more convenient to reload the context than to design the test so that it doesn’t modify the state of the context. While this can be a quick solution, in the long run it’s better to structure the test so that the context state either remains isolated or is automatically cleaned up after each test (e.g., using a @Before or @After hook).
Increase Heap Space
If you have many integration tests in a CI pipeline, sometimes the only solution is to increase available memory:
export MAVEN_OPTS="-Xmx2g"
Or for Gradle:
org.gradle.jvmargs=-Xmx2g
Conclusion
The problem of "too many ApplicationContexts" in Spring Integration Tests is not a bug but rather a side effect of convenience. Spring caches contexts to make tests faster, but when each test class loads its own variant, memory usage can quickly become unsustainable. Key takeaway:
One context per configuration, not per test class.
By being more disciplined with profiles, properties, and annotations, you can make your tests faster and more stable, avoiding the dreaded OutOfMemoryError.
Use @DirtiesContext sparingly and only when it is truly necessary. Frequent use of this annotation can lead to performance issues, hidden dependencies, and reduced parallelism. However, if you do need it, make sure to reload the context only when absolutely required, and consider structuring your tests so that they can run without this annotation.
The key to fast and stable tests lies in avoiding unnecessary context reloads and properly managing state between tests.
Opinions expressed by DZone contributors are their own.
Comments