Spring Boot version
- Reproduced on 4.1.0
- Behaves differently on 4.0.6 (no OOM — see below)
- JVM: Eclipse Temurin 25.0.3+9 (LTS) · OS: macOS 26.5.1 (arm64)
- (the binder logic is plain Java, so it is unlikely to be JVM/OS-specific, but this is the configuration I verified)
Describe the bug
When a @ConfigurationProperties object has a List<SomeJavaBean> property, and an indexed element
(prefix.list[i]) resolves to a non-null but empty value, binding does not terminate: the application
fails to start with a deterministic java.lang.OutOfMemoryError: Java heap space during context
initialization. The heap is exhausted allocating ConfigurationPropertyName instances inside
IndexedElementsBinder.bindIndexed.
On 4.0.6 the exact same setup does not OOM — binding stops quickly with a clear, bounded error:
Failed to bind properties under 'app.routes[0]' to com.example.demo.DemoApplication$RouteConfig
So a previously bounded failure has become an unbounded allocation / OOM.
I hit this while upgrading a real application from 4.0.6 to 4.1.0 (the app's context OOMed on startup binding a
List<...> @ConfigurationProperties). The snippet below is a reduced, standalone reproduction.
Stack trace (4.1.0)
Caused by: java.lang.OutOfMemoryError: Java heap space
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:117)
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:92)
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:76)
at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:50)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:127)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:115)
at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:71)
How to reproduce
Full runnable project (just spring-boot-starter, single Java file, run.sh with a small heap):
https://github.com/Lumenol/spring-boot-4.1-binder-oom — flip the parent version between 4.1.0 and
4.0.6 in pom.xml.
The two relevant files are inlined below.
DemoApplication.java:
package com.example.demo;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.PropertySource;
@SpringBootApplication
@EnableConfigurationProperties(DemoApplication.AppProperties.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(DemoApplication.class);
// A high-priority, NON-iterable property source (registered before the application config) that
// returns a non-null EMPTY value for every app.routes[i]. This is the shape of a non-iterable
// source that answers a whole prefix (similar in spirit to RandomValuePropertySource for random.*).
app.addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) e ->
e.getEnvironment().getPropertySources().addFirst(new PropertySource<Void>("misbehaving") {
private boolean isRoutesIndex(String n) { return n.matches("app\\.routes\\[\\d+\\]"); }
@Override public boolean containsProperty(String n) { return isRoutesIndex(n); }
@Override public Object getProperty(String n) { return isRoutesIndex(n) ? "" : null; }
}));
app.run(args);
}
@ConfigurationProperties("app")
public static class AppProperties {
private List<RouteConfig> routes = new ArrayList<>();
public List<RouteConfig> getRoutes() { return routes; }
public void setRoutes(List<RouteConfig> routes) { this.routes = routes; }
}
public static class RouteConfig {
private String id;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
}
}
src/main/resources/application.yaml:
spring:
main:
web-application-type: none
app:
routes:
- id: r0
Result:
- 4.1.0 →
Terminating due to java.lang.OutOfMemoryError: Java heap space (stack above).
- 4.0.6 (only change: parent version) → no OOM;
Failed to bind properties under 'app.routes[0]'.
Expected behavior
Binding should terminate (as on 4.0.6). At minimum, an empty / non-convertible indexed element should not
cause an unbounded loop that allocates up to Integer.MAX_VALUE and OOMs — turning a bounded, diagnosable
failure into a heap exhaustion is a regression.
🤖 Optional: AI-assisted root-cause analysis — please feel free to ignore
Disclaimer. Everything in this collapsed section was put together with AI assistance. I'm sharing it
only in case it saves you time; I personally don't have the expertise to vouch for it, so please treat it as
a hint, not a claim, and skip it entirely if it's noise. The reproduction above stands on its own.
Suspected mechanism. IndexedElementsBinder.bindIndexed stops only when an element binds to null:
for (int i = 0; i < Integer.MAX_VALUE; i++) {
ConfigurationPropertyName name = appendIndex(root, i);
Object value = elementBinder.bind(name, Bindable.of(elementType), source);
if (value == null) { break; }
collection.get().add(value);
}
Comparing v4.0.6...v4.1.0, the binder change appears to be the new fallbackToDefaultValue path
(Binder / JavaBeanBinder / ValueObjectBinder / DataObjectBinder). In Binder.bindObject:
catch (ConverterNotFoundException ex) {
boolean fallbackToDefaultValue = ObjectUtils.isEmpty(property.getValue());
Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding, fallbackToDefaultValue);
...
}
In ValueObjectBinder.bind, when the target is a plain JavaBean (valueObject == null) and
fallbackToDefaultValue is true, it now returns a non-null default instance instead of null:
if (fallbackToDefaultValue) {
return getNewDefaultValueInstanceIfPossible(context, target.getType());
}
// getNewDefaultValueInstanceIfPossible(...) for a JavaBean ends at:
return (resolved != null) ? BeanUtils.instantiateClass(resolved) : null; // non-null
So an empty-valued routes[i] now yields a non-null empty bean, the loop never reaches null, and it
allocates a ConfigurationPropertyName per index up to Integer.MAX_VALUE → OOM. On 4.0.6 the same element
surfaced the ConverterNotFoundException as a bounded BindException.
Possibly related issues/PRs (again, AI-surfaced, may be wrong):
Spring Boot version
Describe the bug
When a
@ConfigurationPropertiesobject has aList<SomeJavaBean>property, and an indexed element(
prefix.list[i]) resolves to a non-null but empty value, binding does not terminate: the applicationfails to start with a deterministic
java.lang.OutOfMemoryError: Java heap spaceduring contextinitialization. The heap is exhausted allocating
ConfigurationPropertyNameinstances insideIndexedElementsBinder.bindIndexed.On 4.0.6 the exact same setup does not OOM — binding stops quickly with a clear, bounded error:
So a previously bounded failure has become an unbounded allocation / OOM.
I hit this while upgrading a real application from 4.0.6 to 4.1.0 (the app's context OOMed on startup binding a
List<...>@ConfigurationProperties). The snippet below is a reduced, standalone reproduction.Stack trace (4.1.0)
How to reproduce
Full runnable project (just
spring-boot-starter, single Java file,run.shwith a small heap):https://github.com/Lumenol/spring-boot-4.1-binder-oom — flip the parent version between
4.1.0and4.0.6inpom.xml.The two relevant files are inlined below.
DemoApplication.java:src/main/resources/application.yaml:Result:
Terminating due to java.lang.OutOfMemoryError: Java heap space(stack above).Failed to bind properties under 'app.routes[0]'.Expected behavior
Binding should terminate (as on 4.0.6). At minimum, an empty / non-convertible indexed element should not
cause an unbounded loop that allocates up to
Integer.MAX_VALUEand OOMs — turning a bounded, diagnosablefailure into a heap exhaustion is a regression.
🤖 Optional: AI-assisted root-cause analysis — please feel free to ignore
Suspected mechanism.
IndexedElementsBinder.bindIndexedstops only when an element binds tonull:Comparing
v4.0.6...v4.1.0, the binder change appears to be the newfallbackToDefaultValuepath(
Binder/JavaBeanBinder/ValueObjectBinder/DataObjectBinder). InBinder.bindObject:In
ValueObjectBinder.bind, when the target is a plain JavaBean (valueObject == null) andfallbackToDefaultValueistrue, it now returns a non-null default instance instead ofnull:So an empty-valued
routes[i]now yields a non-null empty bean, the loop never reachesnull, and itallocates a
ConfigurationPropertyNameper index up toInteger.MAX_VALUE→ OOM. On 4.0.6 the same elementsurfaced the
ConverterNotFoundExceptionas a boundedBindException.Possibly related issues/PRs (again, AI-surfaced, may be wrong):
fallbackToDefaultValuebehavior (empty arrays/collections/maps).at an indexed name in real apps).
IndexedElementsBinderfilter optimization removed in 3.5.4 (the index-binding area is noted asregression-prone).
@ConfigurationPropertiesbinding regression ('validationType' must not be null).