Why Adding a New JAR Causes StackOverflowError? Uncovering Spring Bean Recursion

This article walks through a real‑world case where adding a new JAR triggers a java.lang.StackOverflowError during Spring application startup, explains how hidden recursive calls in bean initialization cause the overflow, and offers debugging steps and a design‑level fix to prevent the issue.

Huolala Tech
Huolala Tech
Huolala Tech
Why Adding a New JAR Causes StackOverflowError? Uncovering Spring Bean Recursion

1. Diagnosis

When the application fails to start, three key facts are observed: a java.lang.StackOverflowError is thrown during startup, the only code change is the addition of a new JAR dependency, and the problem reproduces on Intel x86_64 but not on Apple M3 ARM.

These clues suggest the error occurs in the main thread and is likely caused by a recursive call introduced by the new JAR.

The JAR defines only interface declarations for downstream SOA calls, not concrete bean classes, so a circular bean reference is unlikely.

The framework that assembles these SOA client instances probably contains deep recursion.

Adjusting the JVM stack size on Intel machines allows the application to start, confirming that stack consumption differs between CPU architectures.

2. Full Inspection

2.1 Thread‑dump analysis

After the StackOverflowError occurs, a thread dump is captured with jstack and examined. The dump shows a repeating call pattern that starts at AbstractBeanFactory.doGetBean and ends at AbstractBeanFactory.getTypeForFactoryBean, forming a recursive loop.

By locating the line AbstractBeanFactory.java:1643 we confirm that getTypeForFactoryBean calls doGetBean, completing the recursion.

Thread dump illustration
Thread dump illustration

2.2 Code debugging

Two breakpoints are set:

ReferenceBean.doAfterPropertiesSet(ReferenceBean.java:36)
AbstractBeanFactory.getTypeForFactoryBean(AbstractBeanFactory.java:1643)

When the first breakpoint hits, the current bean being created is a VehicleAbilityLogoFacade client proxy. Continuing to the second breakpoint shows that Spring is now creating another bean, e.g., UserOrderListSOAService, confirming that different ReferenceBean instances trigger each other.

Breakpoint on ReferenceBean
Breakpoint on ReferenceBean

Further stepping reveals that the recursion originates from AbstractBeanFactory.getType, which internally calls AbstractAutowireCapableBeanFactory.getTypeForFactoryBean, eventually invoking doGetBean again.

Recursive call stack
Recursive call stack

3. Diagnosis Analysis

3.1 How recursion happens

The recursive chain can be summarized as: AbstractBeanFactory#doGetBean creates a ReferenceBean instance.

The bean’s afterPropertiesSet invokes getBeanNamesForAnnotation to find beans annotated with @Fallback. DefaultListableBeanFactory#getBeanNamesForAnnotation iterates over all bean names and calls getType for each. getType triggers getTypeForFactoryBean, which calls doGetBean again, forming a loop.

Because each iteration creates a new stack frame and the loop is combined with a large for traversal, the stack quickly fills, leading to StackOverflowError.

3.2 Why the stack overflows

Even though the recursion is not infinite—Spring detects already‑creating beans—the depth grows with the number of ReferenceBean candidates. On Intel architectures the per‑call stack cost is higher, exhausting the default stack limit.

4. Treatment and Prevention

The root cause is the use of getBeanNamesForAnnotation inside ReferenceBean.doAfterPropertiesSet. Although the Javadoc claims the method does not create bean instances, the implementation invokes getType, which may instantiate beans and invoke their getObjectType. In this case ReferenceBean.getObjectType returns null, forcing full initialization.

A safer approach is to separate the creation of ReferenceBean and FallbackFactory beans and wire them after all singletons are instantiated, e.g., by implementing SmartInitializingSingleton:

@Component
public class FallbackInjector implements SmartInitializingSingleton {
    private final List<ReferenceBean> referenceBeanList;
    private final List<FallbackFactory> fallbackFactoryList;

    @Autowired
    public FallbackInjector(List<ReferenceBean> referenceBeanList,
                           List<FallbackFactory> fallbackFactoryList) {
        this.referenceBeanList = referenceBeanList;
        this.fallbackFactoryList = fallbackFactoryList;
    }

    @Override
    public void afterSingletonInstantiated() {
        for (ReferenceBean bean : referenceBeanList) {
            FallbackFactory factory = lookupFallbackFactory(bean);
            if (factory == null) {
                factory = getGlobalFallbackFactory();
            }
            bean.setFallbackFactory(factory.create());
        }
    }
}

By postponing the fallback wiring, the recursive bean‑creation path is eliminated.

5. Conclusion

This article is the first part of the “Code Detective” series, demonstrating how a real‑world StackOverflowError can be traced back to hidden recursion in Spring’s bean factory. Understanding the bean lifecycle and avoiding bean creation inside other bean initializers are key to preventing similar issues.

DebuggingBean LifecycleStackOverflowError
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.