Java heap spaceiText loads entire PDF documents into JVM heap memory before processing. On large files or under high concurrency, this exhausts available heap and throws OutOfMemoryError. This guide covers JVM tuning, streaming alternatives, and a migration path to a memory-efficient Rust solution.
iText's default document model reads the entire PDF into memory at once. A 100 MB file may require 300–500 MB of heap after object graph expansion, because every page, font, and image stream is parsed and held simultaneously.
When creating or modifying PDFs, iText accumulates all content in-memory until PdfDocument.close() is called. For documents with many pages or large embedded images, this buffer grows unboundedly and can trigger OOM before the file is written.
Default JVM heap is typically 256 MB or 512 MB. Production servers processing concurrent PDF requests can exhaust this quickly. The problem compounds when multiple requests arrive before GC can reclaim memory from completed operations.
Set the maximum heap size high enough for your largest expected document. As a rule of thumb, allocate 5× the size of the largest PDF you process. Add these flags to your JVM startup arguments.
# In your startup script or JAVA_OPTS:
java -Xms512m -Xmx4g -jar your-app.jar
# For Docker, set in your Dockerfile or compose:
ENV JAVA_OPTS="-Xms512m -Xmx4g"For document creation, use PdfWriter with a SmartModeWriter or write pages incrementally and flush them. For reading, use PdfReader with partial loading where possible to avoid pulling the entire document into memory.
// Use try-with-resources and flush pages as you go
try (PdfWriter writer = new PdfWriter(dest);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf)) {
for (int i = 0; i < pageCount; i++) {
document.add(buildPage(i));
// Flush completed pages to disk
pdf.flushCopiedObjects(sourcePdf);
}
}PDFluent is built in Rust and uses a streaming parser that never loads the full document into memory. Pages are processed one at a time and dropped immediately. There is no JVM, no GC pauses, and no heap to tune.
use pdfluent::PdfDocument;
// PDFluent streams pages — no full document in memory
let doc = PdfDocument::open("large.pdf")?;
for page in doc.pages() {
let page = page?;
let text = page.text()?;
println!("{}", text);
// page is dropped here, memory reclaimed immediately
}