How to Prevent OutOfMemoryError When You Use @Async
This article will help you learn how to prevent ''OutOfMemoryError: unable to create new native thread error'' when using Async.
Join the DZone community and get the full member experience.
Join For FreeDo 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.
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.
@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. -Source.
Now, let's examine how Spring's asynchronous execution is carried out under the hood in order to understand where that programming error is.
@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. -Source.
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
.
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.
Conclusion
Be aware what
@EnableAsync
actually does – Spring configuresSimpleAsyncTaskExecutor
and that doesn’t reuse threads and the number of threads used at any given time aren’t limited by default- A unique
TaskExecutor
bean is automatically recognized –AsyncExecutionAspectSupport
picks is up from the application context as long as it’s unique - Using explicit configuration is always easier to read – use
AsyncConfigurer
for defining anExecutor
andAsyncUncaughtExceptionHandler
dedicated to asynchronous execution
Published at DZone with permission of Laszlo Csontos, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments