stanfordnlp/CoreNLP · warning
Cached annotator will never GC -- this can cause OOM…
Error message
Cached annotator will never GC -- this can cause OOM exceptions!
What it means
AnnotatorPool's CachedAnnotator constructor warns when the wrapped Lazy<Annotator> is not a caching Lazy. A non-caching Lazy holds only a supplier, so every annotation request may rebuild the annotator, and the constructed annotator instance is strongly held without the cache mechanism that allows garbage collection — leading to unbounded memory growth and potential OutOfMemoryError.
Solutions
- Wrap the annotator supplier with Lazy.cache(...) (or use Lazy.of followed by .cache()) so the pool's caching/GC behavior applies.
- If the annotator is stateless and cheap, keep the non-caching lazy but be aware it will be rebuilt per request; better to cache it anyway.
- In long-running services, avoid re-creating AnnotatorPools; reuse a single cached pool and monitor heap usage.
Example fix
// before
AnnotatorPool.AnnotationFactory factory = (signature, props) ->
new AnnotatorPool.CachedAnnotator(signature, Lazy.of(() -> new MyAnnotator(props)));
// after
AnnotatorPool.AnnotationFactory factory = (signature, props) ->
new AnnotatorPool.CachedAnnotator(signature, Lazy.cache(() -> new MyAnnotator(props))); Defensive patterns
Strategy: validation
Validate before calling
// Only construct CachedAnnotator with a caching Lazy
Lazy<Annotator> lazy = Lazy.cache(() -> new MyAnnotator(props));
if (!lazy.isCache()) {
throw new IllegalArgumentException("Annotator lazy must be caching to avoid GC leak");
} Prevention
- Always use Lazy.cache(...) when registering annotators in an AnnotatorPool
- Watch heap usage in long-running annotation services; growth between pools signals this leak
- Reuse a single AnnotatorPool across requests instead of rebuilding it
- Run load tests with many documents to surface OOM before production
When it happens
Trigger: Constructing a CachedAnnotator with Lazy.of(...) (non-caching) instead of Lazy.cache(...), typically when registering a custom annotator into an AnnotatorPool.
Common situations: Custom pipeline code that plugs annotators into Stanford CoreNLP's AnnotatorPool with a hand-rolled Lazy wrapper; long-running servers that annotate many documents and hold the pool for the process lifetime.
Related errors
- after W derivative, index() != x.length()
- An error occurred while testing the tagger.
- ancestor: height cannot be negative
- : Entry doesn't have overwriteable types , but entry type…
- : Entry has multiple types for : . Taking type to be
AI-assisted analysis of stanfordnlp/CoreNLP@1b7edd19c4 (2026-09-10).
Data as JSON: /api/errors/cb20deb52face8ac.
Report an issue: GitHub.
Appendix: source
Thrown at src/edu/stanford/nlp/pipeline/AnnotatorPool.java:42
/** A logger for this class */
private static final Redwood.RedwoodChannels log = Redwood.channels(AnnotatorPool.class);
/**
* A cached annotator, including the signature it should cache on.
*/
private static class CachedAnnotator {
/** The signature of the annotator. */
public final String signature;
/** The cached annotator. */
public final Lazy<Annotator> annotator;
/**
* The straightforward constructor.
*/
private CachedAnnotator(String signature, Lazy<Annotator> annotator) {
if (!annotator.isCache()) {
log.warn("Cached annotator will never GC -- this can cause OOM exceptions!");
}
this.signature = signature;
this.annotator = annotator;
}
/** {@inheritDoc} */
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CachedAnnotator that = (CachedAnnotator) o;
return Objects.equals(signature, that.signature) && (Objects.equals(annotator, that.annotator));
}
/** {@inheritDoc} */
@Override
public int hashCode() {
int result = signature != null ? signature.hashCode() : 0;View on GitHub (pinned to 1b7edd19c4)