How to prevent OutOfMemoryError when you use @Async

January 30, 2018 · 6 min read – Last updated on October 26, 2019

Do you use @Async? You’d better watch out, because you might run into OutOfMemoryError: unable to create new native thread error just as I did. After reading this article you’ll learn how to prevent it from happening.

1. Introduction

This post expands on the 5th mistake regarding multi threading outlined by Toni Kukurin in his article Top 10 Most Common Spring Framework Mistakes and explains a special use case of that.

Well, I like the Spring Framework and especially Spring Boot very much. The latter is so easy to use and really feels like pair programming with Pivotal‘s team and other talented committers. Yet, I feel that making development far too easy might stop developers thinking about the implications of pulling starters in and relying on Spring Boot auto-configuration.

Altought @EnableAsync isn’t Spring Boot specific, it still belongs to Spring Core, care should be taken when you’re enabling asynchronous processing in your code.

2. @EnableAsync and using @Async

Before diving into the details of Spring’s support of asynchronous execution, let’s take a look at an application where this problem occurred.

@EnableAsync
@SpringBootApplication
public class SyncApplication {

  public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication(SyncApplication.class);
    springApplication.run(args);
  }

}

@Component
public class SyncSchedulerImpl implements SyncScheduler {

  private final SyncWorker syncWorker;

  public SyncScheduler(SyncWorker syncWorker) {
    this.syncWorker = syncWorker;
  }

  @Scheduled(cron = "0 0/5 * * * ?")
  @Override
  public void sync() {
    List contactEntries = suiteCRMService.getContactList();
    for (ContactEntry contactEntry : contactEntries) {
      syncWorker.processContact(contactEntry);
    }
  }

}

@Component
public class SyncWorkerImpl implements SyncWorker {

  @Async
  @Override
  public void processContact(ContactEntry contactEntry) {
    ...
  }

}

What happens here is that SyncScheduler takes the list of contacts from a CRM system and then delegates processing those contacts to SyncWorker. As you might expect syncWorker.processContact() wouldn’t block at that time when it’s called, but it’s executed on a separate thread instead. So far so good, for a long time this setup had been working just fine, until the number of contacts in the source system had increased.

Why would have that caused an OutOfMemoryError? One logical explanation could be that perhaps the app had to contain much more ContactEntry instances than before. However, if we look at the second half of the error message, it was complaining about heap space. It said a new native thread couldn’t have been allocated.

I wouldn’t like to repeat that what the guys at Plumbr wrote about the java.lang.OutOfMemoryError: Unable to create new native thread issue, so I just summarize it here.

Occasionally you can bypass the Unable to create new native thread issue by increasing the limits at the OS level. […] More often than not, the limits on new native threads hit by the OutOfMemoryError indicate a programming error. When your application spawns thousands of threads then chances are that something has gone terribly wrong – there are not many applications out there which would benefit from such a vast amount of threads.
https://plumbr.io/outofmemoryerror/unable-to-create-new-native-thread

Now, let’s examine how Spring’s asynchronous execution is carried out under the hood in order to understand where that programming error is.

2.1. @EnableAsync under the hood

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync { ... }

Annotation @EnableAsync provides many parameters we can tune. I omitted them now for brevity and we’ll revisited them later, but what’s relevant for us from the point of view of the thread allocation is AsyncConfigurationSelector.

The infrastructure behind asynchronous execution in Spring has got many moving parts and to make the long story short, AsyncConfigurationSelector selects ProxyAsyncConfiguration by default, which (through quite of few indirection) delegates the actual heavy lifting to AsyncExecutionInterceptor.

package org.springframework.aop.interceptor;

import java.util.concurrent.Executor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport
  implements MethodInterceptor, Ordered {

  public AsyncExecutionInterceptor(Executor defaultExecutor) {
    super(defaultExecutor);
  }

  ...

  @Override
  protected Executor getDefaultExecutor(BeanFactory beanFactory) {
    Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
    return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
  }

  ...

}

The important takeaway is that without having an explicitly configured Executor, SimpleAsyncTaskExecutor will be used, which doesn’t impose a limit upon the number of spawned threads.

Fortunately the official getting started guide for Creating Asynchronous Methods uses an explicitly configured Executor and briefly mentions what the default behavior is if you skip that.

The @EnableAsync annotation switches on Spring’s ability to run @Async methods in a background thread pool. This class also customizes the used Executor. In our case, we want to limit the number of concurrent threads to 2 and limit the size of the queue to 500. There are many more things you can tune. By default, a SimpleAsyncTaskExecutor is used.
https://spring.io/guides/gs/async-method/

Note: When @EnableAsync(mode = ASPECTJ) is used, initialization seems to take a different route and eventually AbstractAsyncExecutionAspect falls back to synchronous execution in the lack of an explicitly configured Executor.

2.2. Customizing threading behind @Async

Let’s continue with those options you can tune.

public @interface EnableAsync {

  Class<? extends Annotation> annotation() default Annotation.class;
  boolean proxyTargetClass() default false;
  AdviceMode mode() default AdviceMode.PROXY;
  int order() default Ordered.LOWEST_PRECEDENCE;

}

@EnableAsync gives us fair amount of customization points.

  • annotation: We can define an arbitrary annotation of our choice if we don’t wish to use @Async.
  • proxyTargetClass: Whether to use CGLIB-based proxies instead of the default Java interface-based proxies
  • mode: It’s also possible to change the way how method calls should be being intercepted in order to apply asynchronous behavior. The default it PROXY mode and AspectJ can also be used
  • order: By default AsyncAnnotationBeanPostProcessor will be applied as the last one, after all other post processors have been completed.

If we want to use our own Executor, there are two ways to define one. It’s suffice is we just register a descendant of TaskExecutor to the application context, AsyncExecutionAspectSupport will find it as long as there’s only a single one. Or alternatively, we can also implement AsyncConfigurer like in the example below.

@Data
@EnableAsync
@Configuration
@ConfigurationProperties(prefix = "async.thread.pool")
public class AsyncConfiguration implements AsyncConfigurer {

  private int coreSize;
  private int maxSize;
  private int queueCapacity;

  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(coreSize);
    executor.setMaxPoolSize(maxSize);
    executor.setQueueCapacity(queueCapacity);
    executor.setThreadNamePrefix("worker-exec-");
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return executor;
  }

  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return (ex, method, params) -> {
      Class<?> targetClass = method.getDeclaringClass();
      Logger logger = LoggerFactory.getLogger(targetClass);
      logger.error(ex.getMessage(), ex);
    };
  }

}

I tend to use explicit configuration wherever possible, because that produces a codebase easier to read in the long run. Relying on automated configuration is very convenient indeed, especially for prototypes, but that might also make debugging more difficult when things do sideways.

3. Conclusion