DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • CI/CD Integration: Running Playwright on GitHub Actions: The Definitive Automation Blueprint
  • How to Marry MDC With Spring Integration
  • Integrating Selenium With Amazon S3 for Test Artifact Management
  • Integrating Jenkins With Playwright TypeScript: A Complete Guide

Trending

  • Why Pass/Fail CI Pipelines Are Insufficient for Enterprise Release Decisions
  • Securing Everything: Mapping the Right Identity and Access Protocol (OIDC, OAuth2, and SAML) to the Right Identity
  • The Hidden Bottlenecks That Break Microservices in Production
  • Agentic Testing: Moving Quality From Checkpoint to Control Layer
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. When Memory Overflows: Too Many ApplicationContexts in Spring Integration Tests

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.

By 
Constantin Kwiatkowski user avatar
Constantin Kwiatkowski
·
Nov. 13, 25 · Analysis
Likes (7)
Comment
Save
Tweet
Share
4.1K Views

Join the DZone community and get the full member experience.

Join For Free

In 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:

Java
 
@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 ApplicationContext is 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)

Java
 
@SpringBootTest
class UserServiceIntegrationTest { ... }

@SpringBootTest
class OrderServiceIntegrationTest { ... }


The ApplicationContext is cached and used in both tests. However, if you specify a different profile, for example,

Java
 
@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
 
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
 
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:

Java
 
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:

Shell
 
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 @SpringBootTest configurations
    • Avoid unnecessary properties or profiles per test class
  • Use @DirtiesContext sparingly.
    • 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:

Java
 
@SpringBootTest(properties = "feature.x=true")
@SpringBootTest(properties = "feature.y=true")


A better approach to avoid duplicate ApplicationContexts is:

Java
 
@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:

Shell
 
export MAVEN_OPTS="-Xmx2g"


Or for Gradle: 

Shell
 
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.

Spring Integration Memory (storage engine) Testing Integration

Opinions expressed by DZone contributors are their own.

Related

  • CI/CD Integration: Running Playwright on GitHub Actions: The Definitive Automation Blueprint
  • How to Marry MDC With Spring Integration
  • Integrating Selenium With Amazon S3 for Test Artifact Management
  • Integrating Jenkins With Playwright TypeScript: A Complete Guide

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook