Warm-up Quick Fire: Retention Check (from 4.2 & 4.5 Enterprise Streaming)
สถานการณ์จำลอง (Snowball Scenario): ในบทที่ 4.2 เราป้องกันปัญหา OutOfMemoryError ในการส่งออก Excel ด้วย Apache POI SXSSF (`use.temporary.file=true`) และในบทที่ 4.5 เราตัดปัญหา Memory Buffering ด้วย Direct Socket Streaming แต่หากรายงานฉบับนั้นเป็น Multi-Page Master-Detail Financial Statement หนา 300,000 หน้า ในฝั่งของ JasperReports Fill Engine (`JRVerticalFiller`) จะเกิดอะไรขึ้นบน JVM Heap ก่อนที่ข้อมูลจะถูกส่งต่อไปยัง Exporter?
5.1 Contextual Anchoring: The 1,000,000-Page Heap Exhaustion Crisis
Why High-Volume Production Reports Destroy Microservice Containers Without Paged Virtual Memory
ในขณะที่ `JRVerticalFiller` ทำการวนลูปประมวลผลข้อมูล ทุกๆ หน้ากระดาษที่สร้างเสร็จจะถูกเก็บสะสมเป็นอ็อบเจกต์ JRPrintPage บรรจุในลิสต์ของ JasperPrint หากเอกสารมีขนาด 200,000 หน้า และแต่ละหน้ามี 50 องค์ประกอบ (Text, Line, Image) JVM Heap จะต้องแบกรับ Java Objects กว่า 10,000,000 วัตถุ ดันให้ Old Gen พุ่งเต็ม 100% เกิด Stop-the-World GC นานหลายสิบวินาที และ Pod โดน Kubernetes OOMKilled ในที่สุด
นักพัฒนาหลายคนแก้ปัญหาด้วยการใช้ JRFileVirtualizer แบบดั้งเดิม ซึ่งจะสร้างไฟล์ชั่วคราว 1 ไฟล์แยกต่างหากสำหรับทุกๆ 1 หน้ากระดาษ เมื่อสร้างรายงาน 100,000 หน้า ระบบไฟล์ของ OS จะถูกยัดเยียดด้วยไฟล์ขยะนับแสนไฟล์ ส่งผลให้ Linux Disk Inodes หมด (No space left on device) และเกิด File Handle Lockout จนทั้งเซิร์ฟเวอร์ค้าง
สถาปัตยกรรมระดับ Mission-Critical บังคับใช้ JRSwapFileVirtualizer ร่วมกับ JRSwapFile ซึ่งจำลอง Virtual Memory แบบ Paged Swap File ขนาดใหญ่เพียงไฟล์เดียว ทำงานร่วมกับ In-Memory Page Cache (เช่น ถือไว้บน RAM เพียง 100 หน้า) เมื่อหน้าใหม่ถูกสร้าง หน้าเก่าจะถูก Serialize สลับลง Block Storage ทันที ทำให้ Heap Memory มีขนาดคงที่ระดับ $O(1)$ ตลอดการเรนเดอร์นับล้านหน้า
🏛️ Dynamic Core Pillars (4 เสาหลักการจัดการหน่วยความจำแบบ Virtual Memory)
Paged Virtual Cache Buffer
การกำหนดขนาด maxSize ใน Virtualizer เป็นการจำกัดจำนวนหน้าสูงสุดที่จะคงค้างไว้ใน JVM Heap (เช่น 50 - 200 หน้า) หน้าที่เกินโควตาจะถูก Evict ออกจากหน่วยความจำทันทีตามอัลกอริทึม LRU (Least Recently Used)
Block Storage Fragmentation Control
การตั้งค่า blockSize และ minGrowCount ของ JRSwapFile เพื่อจัดสรรพื้นที่ดิสก์เป็นบล็อกขนาดเท่ากัน (Fixed-size Disk Blocks) ป้องกันปัญหา Fragmentation และนำบล็อกว่างกลับมาใช้ใหม่ (Block Reusability)
Thread-Safe Swap Sharing vs Isolation
การออกแบบ Singleton Shared Swap File ข้ามเธรดในระบบ Microservices เพื่อลด System Calls ในการเปิด/ปิดไฟล์ดิสก์ พร้อมกำหนดตัวจัดการ JRSwapFileVirtualizer ที่ปลอดภัยและคืนบล็อกดิสก์เมื่อคำร้องขอเสร็จสิ้น
Strict Ephemeral Disposal Lifecycle
การผูกคำสั่ง virtualizer.cleanup() หรือ virtualizer.dispose() ภายในบล็อก finally หรือ Spring Interceptor เพื่อกำจัดไฟล์ Swap ชั่วคราวบนดิสก์ ป้องกันปัญหา Disk Space Exhaustion รั่วไหลข้ามวัน
Engine Internals: How JRVerticalFiller & Virtualizer Interact
เมื่อรายงานขนาดใหญ่ถูกเติมข้อมูลโดยส่งผ่านพารามิเตอร์ JRParameter.REPORT_VIRTUALIZER ตัว JRVerticalFiller และ JRVirtualPrintPage จะทำงานประสานกันดังนี้:
- Virtual Page Wrapping: ในทุกๆ หน้ากระดาษที่ถูกสร้างขึ้น Engine จะไม่สร้างอ็อบเจกต์หน้ากระดาษธรรมดา แต่จะห่อหุ้มด้วย
JRVirtualPrintPageที่มีตัวชี้ (Virtual Identity UID) ผูกเข้ากับ Virtualizer - LRU Eviction Trigger: เมื่อจำนวนหน้ากระดาษที่สะสมในหน่วยความจำเกินค่า
maxSizeตัว Virtualizer จะดึงหน้ากระดาษที่เก่าที่สุด (Least Recently Used) มาแปลงเป็น Byte Stream ผ่าน Fast Java Serialization - Continuous Block Allocation: ไบต์ของหน้ากระดาษจะถูกนำไปเขียนลงในบล็อกพื้นที่ว่างของไฟล์
.swapโดยJRSwapFileบน SSD/NVMe และคืนพื้นที่หน่วยความจำ Heap ให้ Garbage Collector ทันที - Transparent Page Fault (Paging In): เมื่อตัว Exporter (เช่น JRPdfExporter หรือ JRXlsxExporter) วนลูปอ่านข้อมูลของหน้าที่ถูกสลับลงดิสก์ไปแล้ว ตัว Virtualizer จะดักจับคำสั่ง
page.getElements()และทำการโหลดไบต์จากดิสก์กลับคืนสู่ RAM แบบโปร่งใส (Page Fault Mechanism) - Lifecycle Disposal: เมื่อกระบวนการ Export เสร็จสิ้น การเรียกใช้
virtualizer.cleanup()จะคืนบล็อกว่างทั้งหมดใน Swap File หรือลบไฟล์ชั่วคราวทิ้งทันที
| Production Memory Hazard | Underlying Root Cause | Runtime Symptom / Exception | Architectural Defense |
|---|---|---|---|
| Disk Inode Exhaustion | ใช้ JRFileVirtualizer แบบเก่า ซึ่งสร้าง 1 ไฟล์แยกต่อ 1 หน้ากระดาษ เมื่อสร้างรายงาน 100,000 หน้า OS Inodes จะหมด |
java.io.IOException: No space left on device (Disk Inodes เต็ม) | เปลี่ยนมาใช้ JRSwapFileVirtualizer ร่วมกับ JRSwapFile ซึ่งใช้เพียง 1 ไฟล์รวมสำหรับทุกหน้ากระดาษ |
| Phantom Swap Leak | เกิดข้อยกเว้นกลางคันระหว่าง Fill หรือ Export แล้วไม่มีการเรียก virtualizer.cleanup() ในบล็อก finally |
Disk Storage Leak: โฟลเดอร์ /tmp ถูกถมเต็มด้วยไฟล์ .swap จน Pod ตาย | ใช้รูปแบบ Try-With-Resources หรือผูก Spring Lifecycle Bean / DisposableBean เข้ากับกระบวนการ |
| Serialization Bottleneck | กำหนด maxSize ต่ำเกินไป (เช่น 2 หรือ 5 หน้า) ทำให้เกิด Page In / Page Out ตลอดเวลา |
CPU Thrashing & High Latency: ประมวลผลช้าลง 20 เท่าจากการทำ Serialization ต่อเนื่อง | ปรับจูน maxSize ให้อยู่ในช่วงที่สมดุลกับ Heap (เช่น 50 ถึง 200 หน้า) และใช้ Fast NVMe Storage |
| Premature Virtualizer Disinfection | เรียก virtualizer.cleanup() ทันทีหลังคำสั่ง fillReport() ก่อนที่ Exporter จะเริ่มทำงาน |
JRRuntimeException: The page data cannot be retrieved from the virtualizer (File closed) | ต้องรักษา Virtualizer ให้เปิดใช้งานอยู่จนกว่ากระบวนการ Exporter จะทำการ Flush ไบต์ลง Network Socket สำเร็จ |
Strict 7.0.x Architecture Implementation
สถาปัตยกรรมบริการ Java 21 / Spring Boot 3.x ด้านล่างแสดงการจัดการ **Thread-Safe Shared Swap File Architecture** เพื่อรองรับการสร้างรายงานขนาด 500,000 หน้า โดยคงขนาด JVM Heap ไว้คงที่ไม่เกิน 512MB:
package com.enterprise.report.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.fill.JRSwapFileVirtualizer;
import net.sf.jasperreports.engine.util.JRSwapFile;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.OutputStream;
import java.util.Map;
@Service
public class EnterpriseVirtualizerService {
@Value("${report.virtualizer.dir:/tmp/jasper_swap}")
private String swapDir;
@Value("${report.virtualizer.cache-size:100}")
private int cacheSize;
// บล็อกข้อมูล 4KB และเติบโตครั้งละ 1024 บล็อก (4MB)
private static final int BLOCK_SIZE = 4096;
private static final int MIN_GROW_COUNT = 1024;
private JRSwapFile sharedSwapFile;
@PostConstruct
public void init() {
File dir = new File(swapDir);
if (!dir.exists()) {
dir.mkdirs();
}
// สร้าง Single Shared Swap File สำหรับรองรับ High-Throughput Worker Threads
this.sharedSwapFile = new JRSwapFile(swapDir, BLOCK_SIZE, MIN_GROW_COUNT);
}
public void executeMassiveReport(JasperReport compiledReport, Map parameters, JRDataSource dataSource, OutputStream targetStream) throws Exception {
// สร้าง Virtualizer ประจำเธรด โดยค้างหน้ากระดาษไว้ใน RAM ไม่เกิน cacheSize หน้า
JRSwapFileVirtualizer virtualizer = new JRSwapFileVirtualizer(cacheSize, sharedSwapFile, false);
try {
// ฉีด Virtualizer เข้าสู่ Parameter Map ของ Engine
parameters.put(JRParameter.REPORT_VIRTUALIZER, virtualizer);
// Fill phase: แม้รายงานจะมี 500,000 หน้า Heap ก็จะไม่ระเบิด OOM
JasperPrint jasperPrint = JasperFillManager.fillReport(compiledReport, parameters, dataSource);
// Export phase: สตรีมตรงสู่ Socket Output Stream
JRPdfExporter exporter = new JRPdfExporter();
exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(targetStream));
exporter.exportReport();
} finally {
// สำคัญที่สุด: ต้องล้างสถานะและคืนบล็อกดิสก์ในบล็อก finally เสมอ
virtualizer.cleanup();
}
}
@PreDestroy
public void destroy() {
if (sharedSwapFile != null) {
sharedSwapFile.dispose();
}
}
}
High-Volume Settlement Ledger Template (100% Unified Element Model 7.0.x)
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
name="MassiveSettlementLedger"
pageWidth="595" pageHeight="842"
columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20"
uuid="f1a2b3c4-d5e6-7890-abcd-ef0123456789">
<parameter name="REPORT_CYCLE" class="java.lang.String"/>
<field name="accountNumber" class="java.lang.String"/>
<field name="counterparty" class="java.lang.String"/>
<field name="settlementAmount" class="java.math.BigDecimal"/>
<field name="clearingStatus" class="java.lang.String"/>
<title height="50">
<element kind="staticText" x="0" y="0" width="555" height="25">
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="Sarabun" size="16" isBold="true"/>
</textElement>
<text><![CDATA[รายงานสรุปการชำระราคาและส่งมอบหลักทรัพย์ปริมาณสูง (High-Volume Clearing Ledger)]]></text>
</element>
<element kind="textField" x="0" y="25" width="555" height="20">
<textElement textAlignment="Center">
<font fontName="Sarabun" size="10"/>
</textElement>
<expression><![CDATA["รอบการประมวลผลระบบสากล: " + $P{REPORT_CYCLE} + " [ระบบจัดการหน่วยความจำเสมือน Off-Heap Active]"]]></expression>
</element>
</title>
<columnHeader height="20">
<element kind="staticText" x="0" y="0" width="120" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[ACCOUNT_NO]]></text>
</element>
<element kind="staticText" x="120" y="0" width="180" height="20">
<textElement><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[COUNTERPARTY]]></text>
</element>
<element kind="staticText" x="300" y="0" width="135" height="20">
<textElement textAlignment="Right"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[AMOUNT (USD)]]></text>
</element>
<element kind="staticText" x="435" y="0" width="120" height="20">
<textElement textAlignment="Center"><font fontName="Sarabun" isBold="true"/></textElement>
<text><![CDATA[STATUS]]></text>
</element>
</columnHeader>
<detail>
<band height="20">
<element kind="textField" x="0" y="0" width="120" height="20">
<expression><![CDATA[$F{accountNumber}]]></expression>
</element>
<element kind="textField" x="120" y="0" width="180" height="20">
<expression><![CDATA[$F{counterparty}]]></expression>
</element>
<element kind="textField" x="300" y="0" width="135" height="20" pattern="#,##0.00">
<textElement textAlignment="Right"/>
<expression><![CDATA[$F{settlementAmount}]]></expression>
</element>
<element kind="textField" x="435" y="0" width="120" height="20">
<textElement textAlignment="Center"/>
<expression><![CDATA[$F{clearingStatus}]]></expression>
</element>
</band>
</detail>
<pageFooter height="20">
<element kind="textField" x="0" y="0" width="555" height="20">
<textElement textAlignment="Center"><font size="8"/></textElement>
<expression><![CDATA["หน้า " + $V{PAGE_NUMBER} + " (จัดเก็บผ่าน Paged Swap Storage)"]]></expression>
</element>
</pageFooter>
</jasperReport>
Interactive Architecture Workspace (Split-View Studio)
Lab 5.1.1: Shared Swap File Virtualizer Configuration
Target: Off-Heap Paged Swapโจทย์สถาปัตยกรรม: ป้องกันไม่ให้ JVM Heap ระเบิด (OOM) เมื่อต้องสร้างเอกสารบัญชีแยกประเภทระดับ 500,000 หน้า โดยจัดสรรพื้นที่ดิสก์แบบคงที่และล้างทรัพยากรอย่างปลอดภัย:
- ต้องเลือกใช้
JRSwapFileVirtualizerร่วมกับJRSwapFile(ห้ามใช้JRFileVirtualizerเด็ดขาด) - ต้องฉีดพารามิเตอร์
JRParameter.REPORT_VIRTUALIZERเข้าสู่คำสั่ง Fill - ทุก Visual Element ใน Template ต้องใช้แท็ก Unified Element Model (
<element kind="...">) 100% - ต้องมีกลไก
virtualizer.cleanup()ในบล็อกfinallyเสมอ
| ACCOUNT NO | COUNTERPARTY | SETTLED AMOUNT (USD) | STATUS |
|---|---|---|---|
| ACT-99120412 | Barclays Capital Int. | $18,450,000.00 | SETTLED |
| ACT-99120413 | Credit Suisse AG | $4,120,500.00 | SETTLED |
ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production เมื่อระบบสร้างรายงานขนาดมหาศาลโดยไม่มี Virtualizer หรือจัดการบล็อกดิสก์ผิดพลาด:
Production Image Specification Prompts (Visual Acceptance Standard)
"2D Vector architectural flowchart, modern dark slate background #0b0f19. JasperReports 7.0.3+ Off-Heap Memory Virtualization Pipeline: JRVerticalFiller streaming large volumes of data records, instantiating JRVirtualPrintPage objects, pushing into an In-Memory LRU Cache of 100 pages, evicting overflow pages through Fast Java Serialization into a single shared JRSwapFile on NVMe block storage with fixed 4096-byte blocks, JRPdfExporter triggering transparent page-fault reads without increasing JVM heap, neon cyan (#06b6d4) and emerald green (#10b981) data streams, crisp typography, 8k resolution."
"Realistic high-resolution commercial PDF A4 portrait financial statement representing an ultra-high-volume clearing ledger. Pristine white paper background, crisp corporate header 'GLOBAL SETTLEMENT CLEARING HOUSE', pagination metadata 'Page 124,500 of 500,000', clean tabular grid with columns ACCOUNT_NO, COUNTERPARTY, SETTLED_AMOUNT, and STATUS. Exact right-aligned monetary values such as '$18,450,000.00', elegant typography, subtle green verified badges, perfectly straight layout, zero blueprint outlines, zero technical bounding boxes, enterprise banking production standard."
"Commercial financial statement detail view focusing on the execution telemetry and ledger balance. Bottom footer showing 'Off-Heap Paged Swap Storage Active • Constant JVM Heap Occupancy: 198MB • Processed Pages: 500,000 • Virtualizer: JRSwapFileVirtualizer'. Includes an official security seal and crisp monochrome DataMatrix barcode for transaction authenticity verification. Razor-sharp typography, flawless contrast, authentic bond paper texture, photorealistic document lighting."
15 Architectural Mastery Quizzes
เจาะลึก JRSwapFileVirtualizer, LRU Page Cache, Disk Block Allocation, OOM Prevention, และ Lifecycle Disposal