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
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Leveraging Salesforce Using a Client Written In Vue.js
  • Leveraging Salesforce Using a Client Written In Svelte
  • Best Practice for Exception Handling In Spring Boot

Trending

  • How To Introduce a New API Quickly Using Quarkus and ChatGPT
  • Introducing Graph Concepts in Java With Eclipse JNoSQL
  • The Evolution of Scalable and Resilient Container Infrastructure
  • Supervised Fine-Tuning (SFT) on VLMs: From Pre-trained Checkpoints To Tuned Models
  1. DZone
  2. Coding
  3. Frameworks
  4. How to Prevent OutOfMemoryError When You Use @Async

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.

By 
Laszlo Csontos user avatar
Laszlo Csontos
·
Jun. 18, 18 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
32.0K Views

Join the DZone community and get the full member experience.

Join For Free

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.

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 @EnableAsyncactually does – Spring configures SimpleAsyncTaskExecutor 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 an Executor and AsyncUncaughtExceptionHandler dedicated to asynchronous execution
Spring Framework Spring Boot application Execution (computing) IT Annotation Contacts (Apple)

Published at DZone with permission of Laszlo Csontos, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Leveraging Salesforce Using a Client Written In Vue.js
  • Leveraging Salesforce Using a Client Written In Svelte
  • Best Practice for Exception Handling In Spring Boot

Partner Resources

×

Comments
Oops! Something Went Wrong

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

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

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 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!