← Level 5 LEVEL 5.1

Memory Virtualizers & OOM Prevention Architecture (JRSwapFileVirtualizer)

Exam Progress: 0 / 15
Mastery: 0%
⚡

Warm-up Quick Fire: Retention Check (from 4.2 & 4.5 Enterprise Streaming)

Anti-Forgetting Protocol

สถานการณ์จำลอง (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

💥 The JasperPrint Object Accumulation Cliff

ในขณะที่ `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 ในที่สุด

⚠️ Disk I/O Thrashing & Inode Exhaustion

นักพัฒนาหลายคนแก้ปัญหาด้วยการใช้ JRFileVirtualizer แบบดั้งเดิม ซึ่งจะสร้างไฟล์ชั่วคราว 1 ไฟล์แยกต่างหากสำหรับทุกๆ 1 หน้ากระดาษ เมื่อสร้างรายงาน 100,000 หน้า ระบบไฟล์ของ OS จะถูกยัดเยียดด้วยไฟล์ขยะนับแสนไฟล์ ส่งผลให้ Linux Disk Inodes หมด (No space left on device) และเกิด File Handle Lockout จนทั้งเซิร์ฟเวอร์ค้าง

🛡️ The Enterprise Solution: JRSwapFileVirtualizer

สถาปัตยกรรมระดับ 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)

Pillar 1

Paged Virtual Cache Buffer

การกำหนดขนาด maxSize ใน Virtualizer เป็นการจำกัดจำนวนหน้าสูงสุดที่จะคงค้างไว้ใน JVM Heap (เช่น 50 - 200 หน้า) หน้าที่เกินโควตาจะถูก Evict ออกจากหน่วยความจำทันทีตามอัลกอริทึม LRU (Least Recently Used)

Analogy: Linux Kernel Page Swapping
Pillar 2

Block Storage Fragmentation Control

การตั้งค่า blockSize และ minGrowCount ของ JRSwapFile เพื่อจัดสรรพื้นที่ดิสก์เป็นบล็อกขนาดเท่ากัน (Fixed-size Disk Blocks) ป้องกันปัญหา Fragmentation และนำบล็อกว่างกลับมาใช้ใหม่ (Block Reusability)

Analogy: Database Tablespace Extents
Pillar 3

Thread-Safe Swap Sharing vs Isolation

การออกแบบ Singleton Shared Swap File ข้ามเธรดในระบบ Microservices เพื่อลด System Calls ในการเปิด/ปิดไฟล์ดิสก์ พร้อมกำหนดตัวจัดการ JRSwapFileVirtualizer ที่ปลอดภัยและคืนบล็อกดิสก์เมื่อคำร้องขอเสร็จสิ้น

Analogy: Shared ThreadPool Executor
Pillar 4

Strict Ephemeral Disposal Lifecycle

การผูกคำสั่ง virtualizer.cleanup() หรือ virtualizer.dispose() ภายในบล็อก finally หรือ Spring Interceptor เพื่อกำจัดไฟล์ Swap ชั่วคราวบนดิสก์ ป้องกันปัญหา Disk Space Exhaustion รั่วไหลข้ามวัน

Analogy: Try-With-Resources Leak Guard
STEP 1

Engine Internals: How JRVerticalFiller & Virtualizer Interact

เมื่อรายงานขนาดใหญ่ถูกเติมข้อมูลโดยส่งผ่านพารามิเตอร์ JRParameter.REPORT_VIRTUALIZER ตัว JRVerticalFiller และ JRVirtualPrintPage จะทำงานประสานกันดังนี้:

  1. Virtual Page Wrapping: ในทุกๆ หน้ากระดาษที่ถูกสร้างขึ้น Engine จะไม่สร้างอ็อบเจกต์หน้ากระดาษธรรมดา แต่จะห่อหุ้มด้วย JRVirtualPrintPage ที่มีตัวชี้ (Virtual Identity UID) ผูกเข้ากับ Virtualizer
  2. LRU Eviction Trigger: เมื่อจำนวนหน้ากระดาษที่สะสมในหน่วยความจำเกินค่า maxSize ตัว Virtualizer จะดึงหน้ากระดาษที่เก่าที่สุด (Least Recently Used) มาแปลงเป็น Byte Stream ผ่าน Fast Java Serialization
  3. Continuous Block Allocation: ไบต์ของหน้ากระดาษจะถูกนำไปเขียนลงในบล็อกพื้นที่ว่างของไฟล์ .swap โดย JRSwapFile บน SSD/NVMe และคืนพื้นที่หน่วยความจำ Heap ให้ Garbage Collector ทันที
  4. Transparent Page Fault (Paging In): เมื่อตัว Exporter (เช่น JRPdfExporter หรือ JRXlsxExporter) วนลูปอ่านข้อมูลของหน้าที่ถูกสลับลงดิสก์ไปแล้ว ตัว Virtualizer จะดักจับคำสั่ง page.getElements() และทำการโหลดไบต์จากดิสก์กลับคืนสู่ RAM แบบโปร่งใส (Page Fault Mechanism)
  5. 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 สำเร็จ
STEP 2

Strict 7.0.x Architecture Implementation

100% Unified Element Model + Paged Swap File

สถาปัตยกรรมบริการ Java 21 / Spring Boot 3.x ด้านล่างแสดงการจัดการ **Thread-Safe Shared Swap File Architecture** เพื่อรองรับการสร้างรายงานขนาด 500,000 หน้า โดยคงขนาด JVM Heap ไว้คงที่ไม่เกิน 512MB:

EnterpriseVirtualizerService.java (Spring Boot 3.x / Java 21)
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)

massive_settlement_ledger.jrxml (JasperReports 7.0.x Schema)
<?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>
STEP 3

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 เสมอ
Expected High-Volume Production Artifact Spec
GLOBAL SETTLEMENT CLEARING HOUSE (PAGE 124,500 OF 500,000) HEAP: 210MB CONSTANT
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
Swap Engine: JRSwapFile (/tmp/jasper_swap/swap_0.dat) In-Memory Window: 100 Pages | Pages on Disk: 499,900
⚠️ Crash Simulator (High-Volume Memory Hazards)

ทดสอบจำลองข้อผิดพลาดที่มักเกิดบน Production เมื่อระบบสร้างรายงานขนาดมหาศาลโดยไม่มี Virtualizer หรือจัดการบล็อกดิสก์ผิดพลาด:

Virtualizer Pipeline AST & Linter
Awaiting Validation
Click "Verify Architecture" to scan AST and Virtualizer compliance rules.
ARTIFACT PROMPTS

Production Image Specification Prompts (Visual Acceptance Standard)

High-Volume Virtualization Standard
1. Architectural Flowchart (Vector)

"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."

2. Lab 5.1.1 Output (Massive Volume Audit Ledger)

"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."

3. Lab 5.1.2 Output (Telemetry & Heap Proof Document)

"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."

EXAM ARENA

15 Architectural Mastery Quizzes

เจาะลึก JRSwapFileVirtualizer, LRU Page Cache, Disk Block Allocation, OOM Prevention, และ Lifecycle Disposal