"การใช้หน่วยความจำ" ของ Windows หมายถึงอะไรจริง ๆ? — อ่าน Working Set, Private Bytes, Commit และเพจไฟล์ให้ถูก
· Go Komura · Windows, การพัฒนา Windows, การจัดการหน่วยความจำ, Working Set, Private Bytes, Commit, เพจไฟล์, การเฝ้าดูประสิทธิภาพ, การแก้ปัญหา, Sysinternals
Task Manager แสดง “Memory” ของโปรเซสเป็น 1.2GB แต่ Process Explorer แสดง Working Set 1.5GB และ Private Bytes 2.4GB และ Size ของ VMMap ใหญ่กว่าอีก เมื่อดูระบบทั้งก้อนจะอ่านว่า “Committed 19.6/31.8GB”
แล้วท้ายที่สุดแอปนี้ใช้หน่วยความจำกี่กิกะไบต์จริง ๆ
คำตอบคือ ตัวเลขที่ควรดูขึ้นกับสิ่งที่คุณอยากรู้จริง ๆ เมตริกต่างกันตามว่าคุณอยากรู้ปริมาณที่อยู่ใน RAM ตอนนี้ ปริมาณที่จัดสรรเฉพาะโปรเซสนั้น ปริมาณที่ระบบสัญญาจะรองรับต่อไป หรือเพียงช่วงที่อยู่เสมือนที่จองไว้
สิ่งที่ทำให้เมตริกหน่วยความจำ Windows สับสนคือทั้งหมดแสดงภายใต้คำเดียวว่า “หน่วยความจำ” ทั้งที่จริงแล้ววัดแกนที่แยกกันดังนี้
- ใช้ พื้นที่ที่อยู่ เท่าใด
- บริโภค commit เท่าใด
- อยู่ใน RAM กายภาพ ตอนนี้หรือไม่
- เพจนั้น เป็นของโปรเซสเอง หรือใช้ร่วมได้
- ระบบทั้งก้อนยังรองรับการจัดสรรได้อีกเท่าใด
บทความนี้สำหรับผู้ที่สอบสวนหน่วยความจำแอปที่โต หรือการขาดหน่วยความจำทั้งระบบบน Windows 10/11 และ Windows Server ปัจจุบัน และเชื่อมความสัมพันธ์ของ Working Set, Private Working Set, Private Bytes, Commit, Virtual Bytes, เพจไฟล์, Available และเพจฟอลต์ในภาพเดียว
ขั้นตอนไล่เหตุที่อ็อบเจกต์ .NET ไม่ถูกเก็บมีรายละเอียดใน “แยกความหน่วง GC ออกจากการรั่วหน่วยความจำใน .NET” และการใช้งานจริงของ VMMap กับ Process Explorer อยู่ใน “Process Explorer / Handle / VMMap ในทางปฏิบัติ” บทความนี้โฟกัสที่ข้อกำหนดเบื้องต้นของทั้งคู่: วิธีอ่านตัวเลขฝั่ง OS ของ Windows
1. สรุปก่อนเลย
- Working Set คือชุดเพจที่อยู่ใน RAM ตอนนี้ รวมไม่เฉพาะเพจของโปรเซสเอง แต่รวมเพจที่ใช้ร่วมกับโปรเซสอื่นได้ เช่นโค้ด DLL และไฟล์แมปหน่วยความจำ1
- Private Working Set คือส่วนของ Working Set ที่เป็นของโปรเซสนั้นเท่านั้นตอนนี้ ใช้เป็นค่าประมาณ “RAM ที่โปรเซสนี้ครองอยู่คนเดียวตอนนี้” ได้ แต่ไม่ใช่ปริมาณทั้งหมดที่แอปจัดสรร2
- Private Bytes คือปริมาณ commit ที่เป็นของโปรเซสนั้น เป็นเมตริกแยกจากว่าหน่วยความจำอยู่ใน RAM ตอนนี้หรือไม่ ฟิลด์
PagefileUsageในโครงสร้าง Win32 API บน Windows ปัจจุบันก็แทน Commit Charge เดียวกันโดยพฤตินัย ไม่ใช่จำนวนไบต์ที่เขียนลงเพจไฟล์จริง2 - “Committed X/Y” ของ Task Manager แสดง X เป็น commit รวมปัจจุบันของระบบ และ Y เป็นเพดาน commit X ไม่ใช่การใช้เพจไฟล์ Y ประมาณจาก RAM บวกเพจไฟล์3
- Reserve กับ Commit เป็นคนละเรื่อง เพียงจองช่วงที่อยู่เสมือนคือกันช่วงนั้นไว้ใช้ภายหลัง ไม่กิน RAM หรือเพดาน commit ในปริมาณเดียวกัน45
- เพจฟอลต์ไม่จำเป็นต้องหมายถึง I/O ดิสก์ มีซอฟต์ฟอลต์ที่แก้ใน RAM ได้ และฮาร์ดฟอลต์ที่อ่านจากเพจไฟล์ ไฟล์ปฏิบัติการ ไฟล์แมปหน่วยความจำ เป็นต้น16
- การรั่วหน่วยความจำตัดสินจากแนวโน้มเมื่อทำโหลดเดิมซ้ำ ไม่ใช่จากค่าครั้งเดียว โดยเฉพาะดูว่า Private Bytes และรายละเอียดยังไต่ขึ้นทีละขั้นหลังประมวลผลจบ โดยไม่กลับสู่สถานะคงที่เดิม
ประโยคเดียว: Working Set คือ “ปริมาณที่อยู่ใน RAM ตอนนี้” Private Bytes คือ “ปริมาณที่สัญญาเฉพาะโปรเซสนี้” และ Commit คือ “ปริมาณที่ระบบทั้งก้อนสัญญา”
flowchart TB
accTitle: เลือกเมตริกหน่วยความจำ Windows ที่ถูก
accDescr: เมตริกที่ดูขึ้นกับว่าอยากรู้การอยู่ใน RAM commit ของโปรเซส commit ทั้งระบบ หรือช่วงที่อยู่เสมือน
question["คุณอยากรู้อะไรเกี่ยวกับการใช้หน่วยความจำ"]
question -->|ปริมาณที่อยู่ใน RAM ตอนนี้| workingSet["Working Set"]
question -->|ปริมาณที่สัญญาเฉพาะโปรเซส| privateBytes["Private Bytes"]
question -->|ปริมาณที่สัญญาระบบทั้งก้อน| systemCommit["System Commit"]
question -->|ช่วงที่อยู่ที่จองไว้| virtualBytes["Virtual Bytes / Reserved"]
workingSet --> resident["การอยู่ใน RAM กายภาพ"]
privateBytes --> privateCommit["Commit ของโปรเซส"]
systemCommit --> commitLimit["เทียบกับ Commit Limit"]
virtualBytes --> addressSpace["พื้นที่ที่อยู่เสมือน"]
ภาพ 1: แยกการสังเกต “หน่วยความจำสูง” เป็นสี่คำถามก่อน
2. แยก “การใช้หน่วยความจำ” เป็นสี่แกน
เริ่มต้นคิดหน่วยความจำ Windows ไม่ใช่ “แท่งเดียว” แต่ตามสี่แกน
flowchart TB
accTitle: สี่แกนอิสระสำหรับจำแนกหนึ่งเพจ
accDescr: ตรวจสถานะที่อยู่เสมือน แบ็กกิงของเพจที่ commit การอยู่ใน RAM กายภาพ และความใช้ร่วมกับโปรเซสอื่นแยกกัน
page["ดูหนึ่งเพจตามสี่แกน"]
page --> address["สถานะที่อยู่"]
address --> addressValues["Free / Reserved / Committed"]
page --> backing["แบ็กกิง"]
backing --> backingValues["Page-file-backed / File-backed"]
page --> residentAxis["การอยู่ใน RAM"]
residentAxis --> residentValues["Resident / Not resident"]
page --> sharing["ความใช้ร่วมได้"]
sharing --> sharingValues["Private / Shareable"]
ภาพ 2: แม้เพจเดียว สถานะที่อยู่ แบ็กกิง การอยู่ และความใช้ร่วมก็กำหนดอิสระ
Mapped ไม่ใช่สถานะที่อยู่คู่กับ Free, Reserved และ Committed — เป็นหมวดของบริเวณ เพจในวิวที่แมปก็ Committed ได้ เช่นกัน Private ไม่ใช่สื่อแบ็กกิง แต่เป็นการจำแนกความใช้ร่วม ดังนั้นอ่านแบ็กกิงเป็น Page-file-backed หรือ File-backed และความใช้ร่วมเป็น Private หรือ Shareable แยกกัน
รวมสี่แกนนี้ได้ความสัมพันธ์ของเมตริกตัวแทนดังนี้
| สถานะเพจ | Working Set | Private Working Set | Private Bytes | ตระกูล Virtual Bytes |
|---|---|---|---|---|
| ของโปรเซส commit อยู่ใน RAM | รวม | รวม | รวม | รวม |
| ของโปรเซส commit ไม่อยู่ใน RAM | ไม่รวม | ไม่รวม | รวม | รวม |
| เพจร่วมของ DLL หรือไฟล์แมป อยู่ใน RAM | รวม | โดยทั่วไปไม่รวม | โดยทั่วไปไม่รวม | รวม |
| จองแล้วแต่ยังไม่ commit | ไม่รวม | ไม่รวม | ไม่รวม | อาจรวม |
| ช่วงที่อยู่ที่ไม่ได้ใช้ | ไม่รวม | ไม่รวม | ไม่รวม | มักไม่รวม |
flowchart TB
accTitle: การจับคู่ชนิดเพจกับเมตริกหน่วยความจำหลัก
accDescr: แสดงว่าเมตริกใดรวมเพจส่วนตัวที่อยู่ใน RAM เพจส่วนตัวที่ไม่อยู่ เพจร่วมที่อยู่ใน RAM และช่วงที่จองอย่างเดียว
privateResident["ส่วนตัว commit อยู่ใน RAM"]
privateNonresident["ส่วนตัว commit ไม่อยู่ใน RAM"]
sharedResident["เพจร่วม อยู่ใน RAM"]
reservedOnly["จองแล้ว ไม่ commit"]
workingSet["Working Set"]
privateWorkingSet["Private Working Set"]
privateBytes["Private Bytes"]
virtualBytes["ตระกูล Virtual Bytes"]
privateResident --> workingSet
privateResident --> privateWorkingSet
privateResident --> privateBytes
privateResident --> virtualBytes
privateNonresident --> privateBytes
privateNonresident --> virtualBytes
sharedResident --> workingSet
sharedResident --> virtualBytes
reservedOnly --> virtualBytes
ภาพ 3: Working Set กับ Private Bytes นับชุดเพจต่างกัน จึงไม่ใช่ความสัมพันธ์การบรรจุแบบง่าย
จุดสำคัญคือ Working Set กับ Private Bytes ไม่ได้อยู่ในความสัมพันธ์การบรรจุแบบง่าย
Private Bytes รวมเพจที่เป็นของโปรเซสแต่ไม่อยู่ใน RAM ตอนนี้ Working Set ในทางกลับกันรวมเพจร่วม เช่นโค้ด DLL และหน่วยความจำร่วม ที่ Private Bytes ไม่นับเลย ดังนั้นตามโปรเซสและช่วงเวลา Working Set อาจใหญ่กว่า Private Bytes หรือกลับกัน
และเพียงรวม Working Set ของหลายโปรเซสอาจนับเพจกายภาพเดียวกัน เช่น DLL ที่ใช้ร่วม มากกว่าหนึ่งครั้ง “ผลรวม Working Set ของแต่ละโปรเซสเท่ากับ RAM ที่ใช้” ไม่จำเป็นต้องเป็นจริง
3. พื้นที่ที่อยู่เสมือน — Reserve กับ Commit เป็นคนละเรื่อง
3.1. ที่อยู่เสมือนไม่ใช่ที่อยู่ RAM กายภาพ
แต่ละโปรเซสมีพื้นที่ที่อยู่เสมือนของตนเอง พอยน์เตอร์ที่แอปใช้ไม่ได้ชี้ตำแหน่งใน RAM กายภาพโดยตรง Windows ใช้ตารางเพจแมปกที่อยู่เสมือนไปยังเพจกายภาพหรือข้อมูลบนไฟล์7
ผลคือแม้ PC มี RAM 64GB พื้นที่ที่อยู่เสมือนที่โปรเซส 32 บิตใช้ได้มักเล็กกว่านั้นมาก ในทางกลับกันโปรเซส 64 บิตมีพื้นที่ที่อยู่เสมือนใหญ่กว่า RAM กายภาพก็เป็นเรื่องปกติ
3.2. Reserved หมายถึงแค่ “กันที่อยู่ไว้”
MEM_RESERVE ของ VirtualAlloc จองช่วงที่อยู่เสมือนต่อเนื่องเพื่อใช้ภายหลัง ในขั้นนี้ยังไม่มีที่เก็บกายภาพผูกกับเพจ และช่วงนั้นอ่านเขียนไม่ได้45
ตัวอย่างเช่นฐานข้อมูลหรือรันไทม์ Reserve ช่วงที่อยู่ 8GB เพื่อโตในอนาคต ก็ไม่กิน RAM 8GB หรือ Private Bytes 8GB เพียงแค่นั้น
3.3. Committed คือสัญญาว่า “จะรองรับเมื่อต้องการ”
MEM_COMMIT คือการทำให้เพจเสมือนอยู่ในสถานะ Committed และให้ Windows สัญญาว่าจะให้แบ็กกิงที่จำเป็น การอ่าน เขียน หรือรันได้รับอนุญาตจริงหรือไม่ตัดสินแยกด้วยการป้องกันเพจ — PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE, PAGE_NOACCESS เป็นต้น — ดังนั้นการเป็น Committed เองไม่หมายถึง “อ่านเขียนได้” ขณะ commit จะนับเข้า Commit Charge ของระบบ แต่เพจกายภาพจริงอาจยังไม่ถูกมอบจนเข้าถึงครั้งแรก เพจที่แตะครั้งแรกถูกตั้งศูนย์ ผ่าน demand-zero fault แล้วเข้า Working Set51
ดังนั้นแม้เรียก “จัดสรร” ทั้งสองแบบ จริงมีสามขั้นต่อไปนี้
flowchart TB
accTitle: สามขั้นจาก Reserve ผ่าน Commit สู่การอยู่ใน RAM
accDescr: แสดงการจองที่อยู่เสมือน การ commit เพจ และการเข้าถึงครั้งแรกที่มอบเพจกายภาพแล้วเข้า Working Set
reserve["MEM_RESERVE - จองช่วงที่อยู่"]
reserve -.-> virtualMetric["สะท้อนในตระกูล Virtual Bytes"]
reserve -->|MEM_COMMIT| committed["Committed - เข้าถึงได้ตามการป้องกันเพจ"]
committed -.-> commitMetric["สะท้อนใน Private Bytes / System Commit"]
committed -->|เข้าถึงครั้งแรก demand-zero fault| resident["มอบเพจกายภาพ อยู่ใน RAM"]
resident -.-> workingSetMetric["สะท้อนใน Working Set"]
committed -.->|ถ้าไม่เคยเข้าถึง| nonresident["commit แล้วแต่ไม่อยู่"]
ภาพ 4: Reserve, Commit และการเข้าถึงครั้งแรกเป็นเหตุการณ์แยก และแต่ละอันขยับเมตริกคนละตัว
สามขั้นนี้ขยับตัวเลขของตระกูล Virtual Bytes, Private Bytes และ Working Set แยกกัน
3.4. ทำไมจึงเกิด OutOfMemory ได้แม้ยังมี RAM ว่าง
ความสำเร็จของการจัดสรรหน่วยความจำไม่ได้ตัดสินจาก RAM ว่างอย่างเดียว
- โปรเซสหมดพื้นที่ที่อยู่เสมือน
- ไม่มีช่วงที่อยู่ว่างขนาดที่ต้องการที่ต่อเนื่อง
- Commit Charge ทั้งระบบถึง Commit Limit
- Job Object คอนเทนเนอร์ รันไทม์ หรือไลบรารีมีขีดจำกัดของตนเอง
- เป็นโปรเซส 32 บิต
- ฮีปเนทีฟแตกเป็นชิ้น
แม้บน Windows 64 บิต พื้นที่ที่อยู่เสมือนโหมดผู้ใช้ของโปรเซส 32 บิตมักเป็น 2GB หากไม่ตั้ง IMAGE_FILE_LARGE_ADDRESS_AWARE แอป 32 บิตที่มีแฟล็กนั้นใช้ได้ถึง 4GB บน Windows 64 บิต8
ดังนั้น “PC มี RAM ว่าง 20GB แต่แอป 32 บิตล้มที่ราว 1.6GB” ไม่ใช่ข้อขัดแย้ง อาจไม่ใช่ปัญหา RAM เลย แต่เป็นการแตกของพื้นที่ที่อยู่หรือชนขีดจำกัดแข็ง
4. Working Set — เพจที่อยู่ใน RAM ตอนนี้
Working Set คือชุดเพจในพื้นที่ที่อยู่เสมือนของโปรเซสที่อยู่ใน RAM กายภาพตอนนี้1
ชุดนี้ผสมสิ่งต่อไปนี้
- ฮีปและสแตกของโปรเซสเอง
- โค้ดและข้อมูลอ่านอย่างเดียวของ EXE และ DLL
- ไฟล์แมปหน่วยความจำ
- หน่วยความจำร่วม
- เพจที่กลายเป็นของโปรเซสนั้นหลัง copy-on-write
- เพจที่รันไทม์และไลบรารีต่าง ๆ แตะ
4.1. Working Set ที่โตไม่จำเป็นต้องหมายถึงจัดสรรเพิ่ม
การเข้าถึงเพจที่ commit แล้วครั้งแรกอาจเพิ่มเฉพาะ Working Set ขณะ Private Bytes คงที่ เช่นกันเมื่อแมปไฟล์ใหญ่แล้วอ่านตามลำดับ เพจที่แบ็กด้วยไฟล์เข้า Working Set ขณะ Private Bytes แทบไม่เพิ่ม
ในทางกลับกันเมื่อ Windows Trim Working Set ตามแรงดันหน่วยความจำ Working Set อย่างเดียวหด ขณะแอปยังถือหน่วยความจำเดิมตามตรรกะ การแตะอีกครั้งภายหลังนำกลับผ่านเพจฟอลต์
ดังนั้น Working Set ที่ลดไม่จำเป็นต้องหมายถึง “แอปปล่อยแล้ว” และการเพิ่มไม่จำเป็นต้องหมายถึง “แอปเพิ่งจัดสรร”
flowchart TB
accTitle: กระแสทั่วไปที่เฉพาะ Working Set ขึ้นลง
accDescr: เพจที่ commit เดียวกันเข้า RAM เมื่อเข้าถึงครั้งแรก กลายเป็นไม่อยู่เมื่อ Trim และกลับเมื่อเข้าถึงอีก ขณะ Private Bytes ยังถูกนับตลอด
committed["เพจที่ commit เดียวกัน"]
committed -->|เข้าถึงครั้งแรก| resident["อยู่ใน RAM"]
resident -->|Trim ภายใต้แรงดันหน่วยความจำ| nonresident["ไม่อยู่"]
nonresident -->|เพจฟอลต์เมื่อเข้าถึงอีก| resident
resident -.-> inWorkingSet["รวมใน Working Set"]
nonresident -.-> outsideWorkingSet["ไม่รวมใน Working Set"]
committed -.-> privateBytes["นับใน Private Bytes ขณะยัง commit"]
ภาพ 5: Working Set ขึ้นลงตามการอยู่ แต่ Private Bytes ไม่ลดตราบที่ commit ของเพจเดียวกันยังอยู่
4.2. Working Set รวมเพจร่วม
หาก 10 โปรเซสใช้เพจโค้ด DLL เดียวกัน เพจนั้นอาจปรากฏใน Working Set ของแต่ละโปรเซส แม้ใน RAM กายภาพมีสำเนาเดียว ผลรวม Working Set เกิน RAM ที่ติดตั้งไม่ใช่สัญญาณปัญหาทันที
หากอยากเข้าใกล้ “RAM ที่โปรเซสนี้ครองอยู่คนเดียวตอนนี้” ให้ดู Private Working Set แม้กระนั้นก็ไม่ใช่ “หน่วยความจำทั้งหมดที่โปรเซสนั้นจัดสรร” — มันคือ เพจส่วนตัวที่อยู่ใน RAM ตอนนี้ อย่างเคร่งครัด
4.3. บังคับ Working Set ให้ลงไม่แก้การรั่ว
คุณใช้ EmptyWorkingSet หรือ SetProcessWorkingSetSize เพื่อไล่เพจออกจาก Working Set ของโปรเซสได้ แต่ไม่ใช่การปล่อย commit หรือปล่อยการอ้างอิงบนฮีป การใช้ RAM ที่เห็นลดลงขณะ Private Bytes คงที่ และการเข้าถึงถัดไปอาจกระตุ้นเพจฟอลต์ระลอก9
หากตัวเลข Task Manager หดเฉพาะทันทีหลังกดปุ่ม “ลดหน่วยความจำ” แล้วพุ่งกลับเมื่อกลับมาทำงาน อาจเป็นเพียง Working Set Trim ไม่ใช่ “การปล่อย” จริง
5. Private Bytes — ปริมาณ commit ที่เป็นของโปรเซส
Private Bytes คือปริมาณหน่วยความจำเสมือนที่ commit เฉพาะโปรเซสนั้น แทน Commit Charge ที่ใช้ร่วมกับโปรเซสอื่นไม่ได้ และไม่สนใจว่าอยู่ใน RAM ตอนนี้หรือไม่ ใน PROCESS_MEMORY_COUNTERS_EX ของ Microsoft PrivateUsage สอดคล้องกับค่านี้102
Win32 API ยังมีฟิลด์ชื่อสับสน PagefileUsage แต่เอกสารปัจจุบันนิยามว่าเป็น “Commit Charge ของโปรเซสนั้น” และระบุว่าเป็นค่าเดียวกับ PrivateUsage นั่นคือ Private Bytes 2GB ไม่ได้หมายถึง “เขียน 2GB ลง pagefile.sys”2
Private Bytes มักได้รับผลจากสิ่งต่อไปนี้
- commit ของฮีปเนทีฟที่
HeapAlloc,malloc,newและคล้ายกันใช้ - Private Data ที่ commit โดยตรงด้วย
VirtualAlloc - บริเวณที่ commit ของฮีป GC .NET
- ส่วนของสแตกเธรดที่ commit จริง
- Commit Charge สำหรับทั้งวิวที่จองเมื่อแมปวิว copy-on-write (
FILE_MAP_COPY) - บัฟเฟอร์ส่วนตัวที่ไลบรารีและ SDK อุปกรณ์ถือภายใน
ในวิว copy-on-write ที่สร้างด้วย FILE_MAP_COPY แต่ละเพจอาจกลายเป็นส่วนตัวในที่สุด ดังนั้นตอนแมป Windows จอง Commit Charge พอรองรับทั้งวิวด้วยเพจไฟล์ ด้วยเหตุนี้ System Commit และ Commit Charge ของโปรเซส (Private Bytes) อาจขึ้นเท่าขนาดทั้งวิว แม้ยังไม่มีการเขียนที่สร้างสำเนาส่วนตัวจริง11
5.1. ทำไม Private Bytes ไม่ลงหลัง free หรือ GC
แม้หน่วยความจำถูก “ปล่อย” จากมุมแอป รันไทม์หรือตัวจัดสรรฮีปอาจไม่ Decommit บริเวณนั้นคืน OS และเก็บไว้ใช้ซ้ำในอนาคต ในกรณีนั้น Private Bytes ยังสูงแม้บริเวณนั้นใช้ซ้ำได้ภายในแอป
มันยังสูงได้จากเหตุเช่นมีเพียงส่วนของบริเวณใหญ่ที่ยังมีชีวิต การแตกเป็นชิ้น หรือแคชหรือพูลอุ่นถึงเพดานแล้ว
ดังนั้น Private Bytes สูงอย่างเดียวไม่พิสูจน์การรั่ว สิ่งที่ควรดูคือการเปรียบเทียบตามเวลา
- ทำประมวลผลเดิมซ้ำจำนวนครั้งเท่ากัน
- รอเวลาเท่ากันหลังประมวลผล
- ตรวจว่า Private Bytes กลับสู่ระดับเดิม หรือราบที่ค่าคงที่
- ใช้ VMMap หรือดัมป์ฮีปตรวจว่าบริเวณหรือชนิดใดโต
flowchart TB
accTitle: ทำไม Private Bytes ไม่ลงหลัง free หรือ GC
accDescr: Private Bytes เปลี่ยนต่างกันตามว่าตัวจัดสรรคืนบริเวณที่แอปไม่ต้องการแล้วให้ OS หรือเก็บไว้ใช้ซ้ำ
release["แอปปล่อยบริเวณผ่าน free / GC"]
release --> decision{"ตัวจัดสรรคืนให้ OS หรือไม่"}
decision -->|Decommit / Release| returned["Commit Charge ลด"]
returned --> lower["Private Bytes ลง"]
decision -->|เก็บไว้ใช้ซ้ำ| retained["บริเวณยัง commit"]
retained --> high["Private Bytes ราบสูง"]
retained --> reasons["พูล แคช การแตกเป็นชิ้น"]
ภาพ 6: บริเวณที่ใช้ซ้ำได้ภายในแอปไม่เท่ากับ Commit ถูกคืนให้ OS
5.2. รูปแบบผู้สมัครแข็งของการรั่ว
การเพิ่มแบบต่อไปนี้ ที่พื้นไต่ขึ้นทุกวอบโหลดเป็น “บันได” น่าสนใจ
Private Bytes
^
| ________
| ______|
| ______|
|_____|
+----------------------------> การทำประมวลผลเดิมซ้ำ
อย่างไรก็ตามแม้รูปบันไดอาจเป็นเพียงไม่กี่วอบของการโตจาก JIT ครั้งแรก ฟอนต์ ตัวถอดรหัสภาพ พูลการเชื่อมต่อ หรือการอุ่นแคช แล้วจึงคงที่ สิ่งสำคัญไม่ใช่ ว่ามันเพิ่ม แต่คือ ว่ามันไม่ลู่เข้าสถานะคงที่
6. System Commit — “Committed X/Y” คืออะไรจริง ๆ
ตัวเลข “Committed X/Y” บนแท็บ [Performance] → [Memory] ของ Task Manager เป็นเมตริกทั้งระบบ
- X: System Commit Charge — หน่วยความจำที่ commit ซึ่ง Windows สัญญาจะรองรับทั้งระบบตอนนี้
- Y: System Commit Limit — เพดาน commit ที่ระบบรับได้
Commit Limit ประมาณจาก RAM กายภาพบวกผลรวมเพจไฟล์ทั้งหมด หากไม่มีเพจไฟล์จะออกมาน้อยกว่า RAM ที่ติดตั้งเล็กน้อย36
flowchart TB
accTitle: ความสัมพันธ์ของ System Commit Charge กับ Commit Limit
accDescr: commit ต่อโปรเซส ส่วนร่วม และเคอร์เนลประกอบค่าปัจจุบัน X ขณะ RAM กายภาพและเพจไฟล์รองรับเพดาน Y
processCommit["Private Commit ของแต่ละโปรเซส"] --> charge["System Commit Charge - X"]
sharedCommit["Commit ของส่วนร่วมที่แบ็กด้วยเพจไฟล์"] --> charge
kernelCommit["Commit ของเคอร์เนล"] --> charge
physicalRam["RAM กายภาพ"] --> limit["System Commit Limit - Y"]
pageFiles["เพจไฟล์"] --> limit
charge -->|X เกิน Y ไม่ได้| limit
ภาพ 7: X คือปริมาณที่สัญญาตอนนี้ และ Y คือเพดานที่รองรับสัญญานั้น — ไม่ใช่การแสดงการใช้เพจไฟล์
System Commit Charge รวมไม่เฉพาะผลรวม Private Bytes ของแต่ละโปรเซส แต่รวม Commit ของส่วนร่วมที่แบ็กด้วยเพจไฟล์ และ Commit ที่เคอร์เนลบริโภค ดังนั้นผลรวม Private Bytes ต่อโปรเซสอย่างเดียวอธิบาย X ไม่ครบ
6.1. Commit Charge ไม่ใช่การใช้เพจไฟล์
พิจารณาระบบที่มี RAM 16GB เพจไฟล์ 16GB และ Committed ที่ 20/31GB
20GB นั้นไม่ได้หมายถึง “เขียน 20GB ลงเพจไฟล์” เป็นปริมาณรวมที่ Windows สัญญาจะจัด RAM หรือแบ็กกิงเพจไฟล์เมื่อต้องการ สำหรับเพจส่วนตัวที่เขียนได้ เป็นต้น
ณ ขณะนั้นสถานะผสมต่อไปนี้อาจเป็นจริง
- ส่วนใหญ่อยู่ใน RAM
- บางส่วนถูกเพจออกไปเพจไฟล์
- บางส่วน commit แล้วแต่ยังไม่เข้าถึงครั้งแรก
- บางส่วนถูกบริโภคเป็น commit ฝั่งเคอร์เนล
หากอยากเห็นการใช้เพจไฟล์จริง ให้ตรวจ Paging File(*)\% Usage แยกจาก Commit แม้เอกสารของ Microsoft เองอธิบายว่าการใช้เพจไฟล์สูงอย่างเดียวไม่จำเป็นต้องเป็นปัญหาประสิทธิภาพ และควรวินิจฉัยร่วมกับการถึง Commit Limit, Modified Page List และ I/O เพจจริง6
6.2. จะเกิดอะไรเมื่อเข้าใกล้ Commit Limit
เมื่อ System Commit Charge ถึง Commit Limit คำขอ commit ใหม่รองรับไม่ได้ สิ่งนี้นำไปสู่การจัดสรรหน่วยความจำโปรเซสล้มเหลว แอปครैช และระบบไม่ตอบสนอง3
ที่นี่ X/Y ของ Commit สำคัญกว่า “RAM ว่าง” แม้คุณ Trim Working Set เพื่อว่าง RAM การถึง Commit Limit ไม่คลี่คลายจนกว่า Commit Charge เองจะลด
6.3. สามบทบาทของเพจไฟล์
เพจไฟล์มีบทบาทหลักดังนี้
- ขยาย Commit Limit
- ให้เพจที่ถูกแก้และใช้น้อยถูกเพจออกจาก RAM ได้
- รองรับครैชดัมป์ของระบบ ตามการตั้งค่า
การปิดเพจไฟล์ไม่ใช่กรณีง่ายว่า “I/O ดิสก์ลดเสมอแล้วทุกอย่างเร็วขึ้น” กลับกันมันลด Commit Limit ทำให้เพจที่ถูกแก้แต่ยังไม่ต้องการอยู่ค้างใน RAM ได้ง่ายขึ้น และอาจทำให้จับดัมป์ที่ต้องการเมื่อครैชไม่ได้36
ขนาดเพจไฟล์ที่เหมาะสมตัดสินจาก RAM ที่ติดตั้งอย่างเดียวไม่ได้ Microsoft เองอธิบายว่าสรุปทั่วไปไม่ได้ เพราะยอด System Commit Charge และชนิดครैชดัมป์ที่ต้องการต่างกันตามระบบ6
7. รายละเอียด RAM กายภาพ — อย่าตัดสินจาก Available ต่ำอย่างเดียว
RAM กายภาพไม่ได้ใช้เฉพาะ Working Set ของโปรเซสผู้ใช้
- Working Set ของแต่ละโปรเซส
- แคชไฟล์ระบบ
- รายการเพจอย่าง Standby, Modified, Free และ Zeroed
- Paged Pool / Nonpaged Pool ของเคอร์เนล
- หน่วยความจำที่ไดรเวอร์อุปกรณ์ถือ
- ที่เก็บการบีบหน่วยความจำ
- บริเวณที่ใช้ร่วมหรือจองให้ GPU และอุปกรณ์อื่น
- หน่วยความจำที่ฮาร์ดแวร์จอง
7.1. Available รวมแคชที่ใช้ซ้ำได้ด้วย
Available MBytes ของ Windows ไม่ใช่เพียง RAM ที่ไม่ใช้เลย เป็นเมตริกที่รวม Free และ Zeroed พร้อมเพจ Standby ที่ใช้ซ้ำได้เมื่อต้องการ12
- Free: เพจที่ยังไม่ถูกจัดสรรให้จุดประสงค์ใด
- Zeroed: เพจที่ถูกตั้งศูนย์เพื่อส่งต่อให้โปรเซสอื่นอย่างปลอดภัย
- Standby: เพจที่ออกจาก Working Set แล้วแต่เนื้อหายังแคชใน RAM
- Modified: เพจที่เนื้อหาเปลี่ยนและต้องเขียนกลับไปแบ็กกิงที่เหมาะก่อนใช้ซ้ำ
flowchart TB
accTitle: การเคลื่อนระหว่าง Working Set กับรายการเพจ
accDescr: แสดงเพจที่ไม่เปลี่ยนไป Standby และเพจที่เปลี่ยนไป Modified แล้วเข้าถึงอีก เขียนกลับ และใช้ซ้ำ
workingSet["Working Set - กำลังใช้"]
workingSet -->|ถอดเพจที่ไม่เปลี่ยน| standby["Standby - ผู้สมัครใช้ซ้ำที่เก็บเนื้อหา"]
workingSet -->|ถอดเพจที่เปลี่ยน| modified["Modified - รอเขียนกลับ"]
modified -->|เขียนกลับเสร็จ| standby
standby -->|เข้าถึงอีก| workingSet
standby -->|ใช้ซ้ำเพื่อจุดประสงค์อื่น| reused["จัดสรรให้จุดประสงค์อื่น"]
free["Free - ไม่ได้ใช้"] -->|ตั้งศูนย์| zeroed["Zeroed - พร้อมจัดสรรใหม่"]
zeroed -->|เข้าถึงหลังจัดสรร| workingSet
standby -.-> available["รวมใน Available"]
free -.-> available
zeroed -.-> available
ภาพ 8: Available รวมไม่เฉพาะหน่วยความจำว่างสนิท แต่รวม Standby ที่ใช้ซ้ำได้เมื่อต้องการ
“ทิ้งแคชทั้งหมดเพื่อเพิ่ม RAM ว่าง” ไม่ใช่ชัยชนะเสมอ หากข้อมูลที่ต้องการยังอยู่ใน Standby การเข้าถึงอีกนำกลับ Working Set ได้เร็วโดยไม่อ่านดิสก์
ดังนั้นแม้ Free ต่ำใน Task Manager หาก Available พอและฮาร์ดเพจฟอลต์หรือการรอดิสก์ไม่เป็นปัญหา Windows อาจเพียงใช้ RAM เป็นแคชอย่างมีประสิทธิภาพ
7.2. เมื่อ RAM หดโดยไม่มีโปรเซสใหญ่
ไม่แปลกที่การใช้หน่วยความจำอธิบายไม่ได้แม้รวม Private Working Set ของทุกโปรเซส
- แคชไฟล์และไฟล์แมปหน่วยความจำ
- Nonpaged Pool / Paged Pool
- เพจที่ไดรเวอร์ล็อก
- เพจร่วม
- การบีบหน่วยความจำ
- การจัดสรรที่เกี่ยวกับการจำลองเสมือนหรือ GPU
ในกรณีนี้แทนที่จะจ้องรายการโปรเซสต่อไป ให้ตรวจ Use Counts, Processes, Priority Summary และ File Summary ใน RAMMap ของ Sysinternals RAMMap เป็นเครื่องมือทางการสำหรับแยกหน่วยความจำกายภาพตามจุดประสงค์ รายการเพจ และไฟล์13
หากเฉพาะ Nonpaged Pool โตต่อเนื่อง นั่นคือจุดที่ควรสงสัยการรั่วฝั่งไดรเวอร์หรือเคอร์เนล ไม่ใช่ Private Bytes ของแอปโหมดผู้ใช้
8. เพจฟอลต์ — จำนวนสูงอย่างเดียวไม่ผิดปกติ
Page Fault เกิดเมื่อโปรเซสเข้าถึงเพจที่ยังไม่อยู่ใน Working Set แม้ชื่อมีคำว่า “Fault” นี่ไม่ใช่ความล้มเหลวพิเศษ — เป็นกลไกปกติที่ขับหน่วยความจำเสมือน1
8.1. ซอฟต์เพจฟอลต์
แก้โดยไม่อ่านจากดิสก์
- เพจยังอยู่ใน Standby หรือ Transition
- เพจร่วมเดียวกันอยู่ใน Working Set ของโปรเซสอื่นแล้ว
- เพจที่ commit ถูกเข้าถึงครั้งแรกและได้รับเพจศูนย์
- การอ่านล่วงหน้าของตัวจัดการหน่วยความจำพาเข้า RAM แล้ว
ดังนั้น \Memory\Page Faults/sec ที่ใหญ่ไม่จำเป็นต้องหมายถึงมี I/O ดิสก์หรือความหน่วง
8.2. ฮาร์ดเพจฟอลต์
ต้องอ่านเนื้อหาจาก Backing Store บนดิสก์ แหล่งไม่จำกัดที่เพจไฟล์
- โค้ดและข้อมูลใน
.exeหรือ.dll - ไฟล์แมปหน่วยความจำ
- เพจไฟล์
flowchart TB
accTitle: กิ่งของซอฟต์และฮาร์ดเพจฟอลต์
accDescr: เมื่อเข้าถึงเพจที่ไม่อยู่ใน Working Set จัดการเป็นซอฟต์เพจฟอลต์หากไม่ต้อง I/O ที่เก็บ หรือฮาร์ดเพจฟอลต์หากต้อง
access["เข้าถึงเพจที่ไม่อยู่ใน Working Set"] --> storageIo{"ต้อง I/O ที่เก็บหรือไม่"}
storageIo -->|ไม่ - Standby ร่วม demand-zero ฯลฯ| soft["ซอฟต์เพจฟอลต์"]
soft --> resident["เข้า Working Set โดยไม่อ่านดิสก์"]
storageIo -->|ใช่| hard["ฮาร์ดเพจฟอลต์"]
hard --> source{"อ่านจากที่ใด"}
source --> image["EXE / DLL"]
source --> mapped["ไฟล์แมปหน่วยความจำ"]
source --> pagefile["เพจไฟล์"]
image --> loaded["เข้า Working Set หลังโหลด"]
mapped --> loaded
pagefile --> loaded
ภาพ 9: ชื่อ “Page Fault” อย่างเดียวบอกไม่ได้ว่าเกิด I/O ดิสก์หรือไม่
Microsoft ระบุ \Memory\Pages/sec, \Memory\Page Reads/sec และ \Memory\Pages Input/sec เป็นตัวนับวัดฮาร์ดฟอลต์ การที่ค่าเหล่านี้สูงไม่จำเป็นต้องหมายถึงหน่วยความจำต่ำ จึงควรสัมพันธ์กับ Available MBytes ความหน่วงดิสก์ และเวลาตอบสนองจริง6
8.3. อย่าตั้งเกณฑ์ผ้าห่มเดียว
ค่าคงที่เช่น “เกิน 1000 Page Faults/sec คือผิดปกติ” เปลี่ยนความหมายตามที่เก็บ ขนาดเพจ ภาระงาน และความใกล้ของการเข้าถึง
ในทางปฏิบัติให้เรียงสิ่งต่อไปนี้บนไทม์ไลน์เดียวกัน
Memory\Available MBytesMemory\Pages Input/secMemory\Page Reads/sec- ความหน่วงอ่าน / คิวของดิสก์เป้าหมาย
- Working Set และ Private Bytes ของโปรเซสเป้าหมาย
- เวลาประมวลผลของแอป หมดเวลา และการตอบสนอง UI
หาก Available ลดพร้อมโหลดขึ้น Pages Input/sec และการรอดิสก์ขึ้น และเวลาประมวลผลแย่ลง นั่นให้เหตุสงสัยการเพจจากแรงดันหน่วยความจำกายภาพ
9. ดูหน้าจอหรือเครื่องมือใดเพื่ออะไร
| สิ่งที่อยากรู้ | เมตริกที่ตรวจก่อน | เครื่องมือหลัก |
|---|---|---|
| ปริมาณที่โปรเซสเป้าหมายมีใน RAM ตอนนี้ | Working Set | Task Manager, Process Explorer, Get-Process |
| ส่วนส่วนตัวของนั้น — RAM ของโปรเซส | Private Working Set / Working Set - Private | คอลัมน์ Details ของ Task Manager, Process Explorer, PerfMon |
| ปริมาณ commit ของโปรเซสเป้าหมาย | Private Bytes / Commit Size | Process Explorer, PerfMon, VMMap, Get-Process |
| ช่วงที่อยู่เสมือนของโปรเซส | Virtual Bytes / Size | Process Explorer, VMMap, Get-Process |
| หัว commit รวมของระบบ | Committed Bytes / Commit Limit | Task Manager [Performance], PerfMon |
| หัวการใช้ซ้ำของ RAM กายภาพ | Available MBytes | Task Manager, PerfMon |
| รายละเอียด Standby, Modified และแคชไฟล์ | รายการเพจ / แยกตามจุดประสงค์ | RAMMap |
| อะไรโตใน Private Bytes | Heap / Private Data / Managed Heap ฯลฯ | VMMap, WinDbg, ดัมป์เฉพาะรันไทม์ |
| การเพจที่เกี่ยวกับดิสก์ | Pages Input/sec, Page Reads/sec, ความหน่วงดิสก์ | PerfMon, WPR/WPA |
flowchart TB
accTitle: เลือกเครื่องมือสอบสวนหน่วยความจำ Windows
accDescr: เครื่องมือที่ใช้ขึ้นกับว่าเป้าเป็นโปรเซสเดียวหรือทั้งระบบ จุดเวลาเดียวหรืออนุกรมเวลา และต้องไล่การถือในรันไทม์หรือไม่
question["คุณอยากแยกอะไร"]
question --> processScope{"เป้าเป็นโปรเซสเดียวหรือไม่"}
processScope -->|ใช่| processTime{"จุดเวลาเดียวหรืออนุกรมเวลา"}
processTime -->|รายละเอียดจุดเดียว| vmmap["VMMap"]
processTime -->|อนุกรมเวลา| perfmon["PerfMon / PowerShell"]
processScope -->|ทั้งระบบ| systemView{"รายละเอียด RAM กายภาพหรือไทม์ไลน์"}
systemView -->|รายละเอียด RAM กายภาพ| rammap["RAMMap"]
systemView -->|ไทม์ไลน์รวม CPU I/O และการรอ| wpa["WPR / WPA"]
question --> runtime{"ต้องไล่การถือในรันไทม์หรือไม่"}
runtime -->|ฮีป .NET| dotnet["dotnet-dump / PerfView"]
runtime -->|ฮีปเนทีฟ| native["WinDbg / Application Verifier"]
ภาพ 10: ตัดสินขอบเขตและไทม์ไลน์ก่อนทำให้เลือกเครื่องมือได้พอดี ไม่มากไม่น้อย
9.1. Task Manager
ใน Task Manager ให้ดูหน้าจอแยกกัน
- [Processes] หรือ [Details]: ตระกูล Working Set และตระกูล Commit Size ของแต่ละโปรเซส
- [Performance] → [Memory]: In use, Available, Committed, Cached, Paged pool, Non-paged pool ทั้งระบบ
อย่าตัดสินจากคอลัมน์ชื่อ “Memory” อย่างเดียว — คลิกขวาหัวคอลัมน์บนแท็บ [Details] แล้วเพิ่มคอลัมน์ที่ต้องการ เช่น Working Set, Peak Working Set และ Commit Size ชื่อคอลัมน์ต่างกันบ้างตามรุ่น Windows และภาษาแสดงผล ดังนั้น ยืนยันความหมายของคอลัมน์ก่อนบันทึก
9.2. จับอนุกรมเวลาด้วย PowerShell
หากรู้ ID ของโปรเซสเป้าหมาย คุณจับแนวโน้ม Working Set, Private Bytes และ Virtual Bytes พร้อมกันด้วย Get-Process ได้
param(
[Parameter(Mandatory)]
[int]$ProcessId,
[int]$IntervalSeconds = 5,
[int]$SampleCount = 60
)
$samples = for ($i = 0; $i -lt $SampleCount; $i++) {
$process = Get-Process -Id $ProcessId -ErrorAction Stop
[pscustomobject]@{
Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
ProcessId = $process.Id
WorkingSetMB = [math]::Round($process.WorkingSet64 / 1MB, 1)
PrivateBytesMB = [math]::Round($process.PrivateMemorySize64 / 1MB, 1)
VirtualBytesMB = [math]::Round($process.VirtualMemorySize64 / 1MB, 1)
Handles = $process.HandleCount
Threads = $process.Threads.Count
}
Start-Sleep -Seconds $IntervalSeconds
}
$samples | Format-Table -AutoSize
$samples | Export-Csv .\memory-samples.csv -NoTypeInformation -Encoding utf8
Process.WorkingSet64 ของ .NET สอดคล้องกับ Working Set, PrivateMemorySize64 กับ Private Bytes และ VirtualMemorySize64 กับ Virtual Bytes141516
สำหรับแอปที่มีหลายอินสแตนซ์ ให้ตามด้วย PID ไม่ใช่ชื่อ สำหรับการเฝ้าดูระยะยาวที่การรีสตาร์ตเปลี่ยน PID ให้ออกแบบการเก็บให้บันทึกเวลาเริ่ม ชื่อบริการ และคล้ายกัน เพื่อไม่สับสนเป้า
9.3. วางระบบกับโปรเซสบนไทม์ไลน์เดียวกันด้วย PerfMon
การบันทึกอย่างน้อยสิ่งต่อไปนี้พร้อมกันทำให้การแยกง่ายขึ้นมาก
\Process(<เป้าหมาย>)\ID Process
\Process(<เป้าหมาย>)\Working Set
\Process(<เป้าหมาย>)\Working Set - Private
\Process(<เป้าหมาย>)\Private Bytes
\Process(<เป้าหมาย>)\Virtual Bytes
\Memory\Available MBytes
\Memory\Committed Bytes
\Memory\Commit Limit
\Memory\Pages Input/sec
\Memory\Page Reads/sec
\Memory\Pool Nonpaged Bytes
\Memory\Pool Paged Bytes
เมื่อหลายโปรเซสใช้ชื่อเดียวกัน หรือเกิดรีสตาร์ตระหว่างเฝ้าดู ชื่ออินสแตนซ์อย่างเดียว — Process(name) หรือ Process(name#N) — ตรึงเป้าไม่ได้ บันทึก ID Process ของแต่ละตัวอย่างด้วย และรับเฉพาะอินสแตนซ์ที่ค่าตรงกับ PID ที่ตาม เมื่อข้ามรีสตาร์ตที่เปลี่ยน PID ให้บันทึกเวลาที่สลับแยกด้วย
ชื่อตัวนับประสิทธิภาพ Windows อาจถูกแปลตามภาษาแสดงผล หากระบุชื่อภาษาอังกฤษตรง ๆ ใน PowerShell แล้วไม่พบ ให้เพิ่มตัวนับผ่าน GUI ของ PerfMon หรือตรวจชื่อในสภาพแวดล้อมท้องถิ่นด้วย Get-Counter -ListSet *
9.4. อย่าสับสนบทบาทของ VMMap กับ RAMMap
- VMMap: แยกหน่วยความจำเสมือนและ Working Set ของหนึ่งโปรเซสเป็น Heap, Image, Mapped File, Private Data, Managed Heap เป็นต้น
- RAMMap: แยก RAM กายภาพทั้งระบบตามจุดประสงค์ รายการเพจ โปรเซส และไฟล์
“อะไรทำให้ Private Bytes ของโปรเซสนี้โต” เป็นงานของ VMMap “RAM ที่รายการโปรเซสอธิบายไม่ได้ถูกใช้ทำอะไร” เป็นงานของ RAMMap1713
10. อ่านอาการจากชุดตัวเลข
| รูปแบบที่สังเกต | สมมติฐานแรก | สิ่งที่ตรวจต่อ |
|---|---|---|
| Working Set ขึ้น Private Bytes คงที่ | เข้าถึงเพจที่มีอยู่ครั้งแรก DLL ร่วม ไฟล์แมป แคชไฟล์ | Image / Mapped File ของ VMMap, Pages Input/sec |
| Private Bytes ขึ้น Working Set คงที่ | Commit ส่วนตัวโตแต่ไม่อยู่หรือถูก Trim | Heap / Private Data / Managed Heap ของ VMMap |
| ทั้งคู่ขึ้นทันทีหลังเริ่ม แล้วราบ | JIT แคช พูล การอุ่นเริ่มต้น | ว่าโตอีกภายใต้โหลดเพิ่มเดิมหรือไม่ |
| พื้น Private Bytes ขึ้นทุกวอบโหลด | การรั่ว แคชไร้เพดาน หรือตัวจัดสรรที่ถือหลังปล่อย | สแนปช็อต VMMap ก่อนหลัง ดัมป์ฮีป |
| เฉพาะ Working Set ลดฉับพลันแล้วกลับเมื่อทำงาน | OS หรือแอป Trim Working Set | Private Bytes, Pages Input/sec เวลาตอบสนอง |
| X ใน Committed X/Y เข้าใกล้ Y | แรงดัน commit ทั้งระบบ | ผู้บริโภค Private Bytes สูงสุด Paged/Nonpaged Pool การตั้งเพจไฟล์ |
| Available ต่ำ Pages Input/sec และความหน่วงดิสก์สูง | แรงดัน RAM กายภาพและการเพจหนัก | ผู้บริโภค Working Set สูงสุด RAMMap ความสัมพันธ์ภาระงาน |
| การใช้ RAM สูงแต่ไม่มีโปรเซสใหญ่ | แคช เพจร่วม พูลเคอร์เนล ไดรเวอร์ การบีบ ฯลฯ | RAMMap, Pool Nonpaged/Paged Bytes |
| มี RAM ว่าง แต่เฉพาะแอป 32 บิตล้ม | เพดานพื้นที่ที่อยู่เสมือนหรือการแตก | Free/Reserved ของ VMMap การตั้ง LAA ของไฟล์ปฏิบัติการ |
| Private Bytes สูงแต่ไม่โตเมื่อประมวลผลซ้ำ | พูลหรือแคชที่อาจถือลายน้ำสูง | ขีดจำกัด พฤติกรรมใช้ซ้ำ ความคงที่หลังยอด |
สิ่งสำคัญที่สุดของตารางนี้คือ อ่านเป็นชุด ไม่ใช่จากค่าเดียว
11. ขั้นตอนปฏิบัติสอบสวนการรั่วหน่วยความจำ
11.1. ตัดสินเงื่อนไขทำซ้ำและจุดคงที่ก่อน
“มันโตในไม่กี่วัน” อย่างเดียวเปรียบเทียบไม่ได้
- รวมการอุ่นหลังเริ่มมากน้อยเพียงใด
- หนึ่งรอบการทำงานประกอบด้วยอะไร
- รอหลังหนึ่งรอบกี่วินาที
- กี่วอบถึงเพดานแคช
- ใช้ข้อมูลเข้าเดียวกันสำหรับบิลด์ปกติและบิลด์ปัญหาได้หรือไม่
ตัดสินทั้งหมดนี้
11.2. บันทึกโปรเซสและระบบพร้อมกัน
อย่างน้อยให้สิ่งต่อไปนี้อยู่ในล็อกที่ตราเวลาเดียวกัน
- Working Set ของเป้า
- Private Bytes ของเป้า
- Virtual Bytes ของเป้า
- Committed Bytes / Commit Limit ของระบบ
- Available MBytes
- Pages Input/sec
- จำนวนแฮนเดิล จำนวนเธรด
- จำนวนการทำงานหรือรายการที่ประมวลผล
หาก Private Bytes ของโปรเซสคงที่ขณะ Commit ของระบบยังโต คุณต้องขยายขอบเขตไปยังโปรเซสอื่น เคอร์เนล ไดรเวอร์ และส่วนร่วม
11.3. ตัดสินว่า “มิติ” ใดกำลังโตก่อน
- เฉพาะ Working Set: เพจที่อยู่ ร่วมหรือมาจากไฟล์ Trim และโหลดใหม่
- Private Bytes: commit ของโปรเซส
- เฉพาะ Virtual Bytes: Reserve การแมป การแตกของพื้นที่ที่อยู่
- เฉพาะ System Commit: รวมโปรเซสอื่นและฝั่งเคอร์เนล
- Nonpaged Pool: ฝั่งไดรเวอร์/เคอร์เนล
- Handles / GDI / USER: การรั่วของทรัพยากรที่ไม่ใช่หน่วยความจำ
ข้ามลำดับนี้แล้วกระโดดไปจับดัมป์ คุณจะอ่านภูเขาข้อมูลขณะเล็งผิดเป้า
11.4. ไปต่อที่รายละเอียด
- โปรเซสเนทีฟ: VMMap, WinDbg, Application Verifier การตามรอยฮีป
- .NET:
dotnet-counters,dotnet-gcdump,dotnet-dump, PerfView - ทั้งระบบ: RAMMap, PerfMon, WPR/WPA
- พูลเคอร์เนล: PoolMon, WinDbg
VMMap แสดงหน่วยความจำเสมือนที่ commit ของโปรเซส และ Working Set ที่จัดสรรให้แต่ละส่วน แยกตามชนิด ว่าคุณแคบการโตของ Private Bytes ลงถึง Heap, Private Data, Managed Heap หรือ Mapped File ได้ไกลเพียงใด ทำให้ต้นทุนการสอบสวนต่อต่างกันมาก17
11.5. หลังแก้ เปรียบเทียบแนวโน้มภายใต้เงื่อนไขเดิม
ค่ายอดต่างก่อนหลังแก้ยังไม่พอ หากค่าเริ่มต่าง การเปรียบเทียบพลิกง่าย
- สถานะเริ่มเดียวกัน
- ข้อมูลเข้าเดียวกัน
- จำนวนการทำงานเดียวกัน
- เวลารอเดียวกัน
- ช่วงสุ่มตัวอย่างเดียวกัน
— แล้วเปรียบเทียบค่าพื้นและแนวโน้มหลังแต่ละรอบ การพิสูจน์การแก้รั่วไม่ใช่ “ค่าสูงสุดเล็กลง” แต่คือ การโตลู่เข้าแม้ทำโหลดเดิมซ้ำ
12. กล่าวใหม่ความเข้าใจผิดทั่วไป
ความเข้าใจผิด 1: Memory ของ Task Manager เท่ากับปริมาณทั้งหมดที่แอปจัดสรร
กล่าวใหม่: ตรวจว่าเป็นคอลัมน์ใด สำหรับตระกูล Working Set คือปริมาณที่อยู่ใน RAM ตอนนี้ สำหรับตระกูล Commit Size คือ commit ของโปรเซสนั้น
ความเข้าใจผิด 2: Private Bytes เท่ากับไบต์บนเพจไฟล์
กล่าวใหม่: Private Bytes คือ Commit Charge ส่วนตัว เป็นปริมาณที่สัญญาเชิงตรรกะซึ่งรวมทั้งเพจที่อยู่ใน RAM ตอนนี้และเพจที่จะถูกแบ็กด้วยเพจไฟล์เมื่อต้องการ
ความเข้าใจผิด 3: Commit X/Y เท่ากับการใช้เพจไฟล์ / ความจุเพจไฟล์
กล่าวใหม่: X คือ Commit Charge ทั้งระบบ และ Y คือ Commit Limit เพจไฟล์ขยาย Y แต่ X ไม่แปลงตรงเป็นการใช้บนดิสก์
ความเข้าใจผิด 4: Page Faults/sec สูงหมายถึงกำลังสลับไปดิสก์
กล่าวใหม่: รวมซอฟต์ฟอลต์ด้วย ตรวจ Pages Input/sec, Page Reads/sec และความหน่วงดิสก์เพื่อดูว่ามี I/O ดิสก์จริงหรือไม่
ความเข้าใจผิด 5: Free RAM ต่ำหมายถึงขาดหน่วยความจำ
กล่าวใหม่: ดู Available, Standby การเพจหนัก และเวลาตอบสนอง การเติม RAM ด้วยแคชที่ใช้ซ้ำได้เป็นเรื่องปกติ
ความเข้าใจผิด 6: ย่อ Working Set หมายถึงแก้การรั่วหน่วยความจำแล้ว
กล่าวใหม่: คุณอาจเพียงไล่เพจออกจาก RAM ตรวจว่า Private Bytes และสิ่งที่ถือในฮีปลดจริงหรือไม่
ความเข้าใจผิด 7: Private Bytes เพิ่มยืนยันการรั่ว
กล่าวใหม่: ตัดสินได้หลังตรวจว่าลู่เข้าเมื่อทำภาระงานเดิมซ้ำ หน่วยความจำชนิดใดโต และเป็นแคชที่ปล่อยได้หรือไม่
13. สรุป
- “การใช้หน่วยความจำ” ของ Windows ไม่ใช่ตัวเลขเดียว คิดพื้นที่ที่อยู่ commit การอยู่ใน RAM และความใช้ร่วมแยกกัน
- Working Set คือเพจที่อยู่ใน RAM ตอนนี้ รวมทั้ง Private และ Shared Private Working Set คือเพจส่วนตัวที่อยู่ในนั้น
- Private Bytes คือ Commit Charge ของโปรเซส ไม่ใช่ปริมาณที่อยู่ใน RAM ตอนนี้ และไม่ใช่ปริมาณที่เขียนลงเพจไฟล์จริง
- Committed X/Y คือ Commit Charge / Commit Limit ทั้งระบบ เพจไฟล์รองรับหลัก ๆ คือ Commit Limit การไล่เพจที่ถูกแก้ และครैชดัมป์
- ที่อยู่เสมือนที่จอง เพจที่ commit และเพจที่ถูกแตะจริงแล้วเข้า Working Set เป็นขั้นแยก
- Page Fault เป็นการทำงานปกติ และซอฟต์ฟอลต์ไม่อ่านจากดิสก์ ฮาร์ดฟอลต์ก็เกิดได้ไม่เฉพาะจากเพจไฟล์ แต่จาก EXE DLL หรือไฟล์แมป
- การรั่วหน่วยความจำพิสูจน์ไม่ใช่ด้วยขนาด ณ จุดเดียว แต่ด้วยค่าพื้นและแนวโน้มหลังโหลดเดิม พร้อมรายละเอียด
- เส้นทางพื้นฐาน: VMMap สำหรับรายละเอียดโปรเซสเดี่ยว RAMMap สำหรับ RAM กายภาพทั้งระบบ PerfMon สำหรับอนุกรมเวลา และเครื่องมือดัมป์เฉพาะสำหรับสิ่งในรันไทม์
ครั้งถัดไปที่สังเกตใน Task Manager ว่า “หน่วยความจำกำลังโต” ให้เริ่มด้วยการถามตัวเองดังนี้
สิ่งที่กำลังโตคือ Working Set, Private Bytes, Virtual Bytes หรือ System Commit
คำถามนั้นอย่างเดียวทำให้ทางเข้าการสอบสวนแม่นขึ้นมาก
บทความที่เกี่ยวข้อง
- แยกความหน่วง GC ออกจากการรั่วหน่วยความจำใน .NET — ขั้นตอนปฏิบัติสังเกต เปรียบเทียบ และพิสูจน์การโตของหน่วยความจำ
- Process Explorer / Handle / VMMap ในทางปฏิบัติ — ตามแฮง รั่ว และ “ไฟล์กำลังถูกใช้” จากสถานะตอนนี้
- กับดักของ Shared Memory และแนวปฏิบัติจริง
- ความลึกของ Windows I/O ส่วนที่ 4 — Cache Manager: เมื่อใด WriteFile ของคุณถึงดิสก์จริง
- สอบสวนครैชหลังทำงานต่อเนื่องนานของแอปกล้องอุตสาหกรรม - การรั่วแฮนเดิล (ส่วนที่ 1)
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับการสอบสวนสาเหตุที่แท้จริงซึ่งผสม PerfMon, VMMap, RAMMap, WinDbg และเครื่องมือวินิจฉัย .NET สำหรับหน่วยความจำแอป Windows ที่โต ประสิทธิภาพตกหลังทำงานต่อเนื่องนาน OutOfMemory ในโปรเซส 32 บิต และการขาดหน่วยความจำที่เกิดเฉพาะในสภาพแวดล้อมลูกค้า เราไม่หยุดที่เพียง “หน่วยความจำสูง” — เราแยกว่าบริเวณใดโต ผ่านการทำงานใด ทำไม และถูกอ้างหรือถือจากที่ใด
ลิงก์อ้างอิง
-
Microsoft Learn, Working Set. ว่าด้วย Working Set ของโปรเซสเป็นชุดเพจที่อยู่ในหน่วยความจำกายภาพตอนนี้ รวมเพจร่วม ความต่างของซอฟต์และฮาร์ดเพจฟอลต์ เพจ Transition และการถอดเพจออกจาก Working Set ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, PROCESS_MEMORY_COUNTERS_EX2 structure. ว่าด้วยนิยามของ WorkingSetSize, PrivateWorkingSetSize, PrivateUsage และ SharedCommitUsage และว่า PagefileUsage กับ PrivateUsage ทั้งคู่แทน Commit Charge ของโปรเซส ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to page files. ว่าด้วยเพจไฟล์รองรับการไล่เพจที่ถูกแก้ ครैชดัมป์ระบบ และการขยาย System Commit Limit นิยามของ System Commit Charge และ Commit Limit และการวัดผ่าน Task Manager และตัวนับประสิทธิภาพ ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Page State. ว่าด้วยสถานะ Free, Reserved และ Committed ของเพจเสมือน และว่าเพจ Reserved ไม่มีที่เก็บกายภาพผูกและเข้าถึงไม่ได้ ↩ ↩2
-
Microsoft Learn, VirtualAlloc function. ว่าด้วยความต่างของ MEM_RESERVE กับ MEM_COMMIT การ commit ถูกคิดกับหน่วยความจำรวมและเพจไฟล์ของระบบ และเพจกายภาพจริงบางครั้งยังไม่ถูกจัดสรรจนเข้าถึงครั้งแรก ↩ ↩2 ↩3
-
Microsoft Learn, How to determine the appropriate page file size for 64-bit versions of Windows. ว่าด้วยขนาดเพจไฟล์ขึ้นกับยอด Commit Charge และความต้องการครैชดัมป์ ฮาร์ดเพจฟอลต์อ่านไม่เฉพาะจากเพจไฟล์แต่จาก EXE DLL และไฟล์แมปหน่วยความจำด้วย และตัวนับประสิทธิภาพที่เกี่ยวข้อง ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Virtual Address Space. ว่าด้วยแต่ละโปรเซสมีพื้นที่ที่อยู่เสมือนและตารางเพจอิสระของตนเอง และที่อยู่เสมือนไม่ใช่ที่อยู่กายภาพเอง ↩
-
Microsoft Learn, Memory Limits for Windows and Windows Server Releases. ว่าด้วยพื้นที่ที่อยู่เสมือนโหมดผู้ใช้ของโปรเซส 32 บิตมักเป็น 2GB และเป็น 2GB หรือ 4GB บน Windows 64 บิตตาม IMAGE_FILE_LARGE_ADDRESS_AWARE ↩
-
Microsoft Learn, SetProcessWorkingSetSize function. ว่าด้วยค่าต่ำสุดและสูงสุดของ Working Set ไม่รับประกันการอยู่ การล้าง Working Set ได้ และการตั้งค่าหรือการกระทำที่มากเกินไปอาจทำให้ประสิทธิภาพระบบแย่ ↩
-
Microsoft Learn, Memory Performance Information. ว่าด้วยความสอดคล้องระหว่างตัวนับประสิทธิภาพ Windows API จัดการหน่วยความจำ และการแสดงของ Task Manager รวม Working Set / Working Set - Private / Private Bytes ของอ็อบเจกต์ Process และ Committed Bytes / Commit Limit ของอ็อบเจกต์ System ↩
-
Microsoft Learn, MapViewOfFile function. ว่าด้วย
FILE_MAP_COPYที่ทำให้ทุกเพจอาจ copy-on-write จึงจอง Commit Charge ทั้งวิวให้แบ็กด้วยเพจไฟล์ตอนแมป ↩ -
Microsoft Learn, Understanding Node Metrics and Properties in HPC Cluster Manager. ว่าด้วย Available Physical Memory คำนวณเป็นผลรวมของรายการ Zeroed, Free และ Standby และความหมายของแต่ละรายการเพจ ↩
-
Microsoft Sysinternals, RAMMap. ว่าด้วยการวิเคราะห์การใช้หน่วยความจำกายภาพของ Windows ตามจุดประสงค์ รายการเพจ โปรเซส ลำดับความสำคัญ เพจกายภาพ และไฟล์ ↩ ↩2
-
Microsoft Learn, Process.WorkingSet64 Property. ว่าด้วย
WorkingSet64คืน Working Set ของโปรเซสเป็นไบต์ สอดคล้องกับตัวนับประสิทธิภาพ Working Set ของอ็อบเจกต์ Process ↩ -
Microsoft Learn, Process.PrivateMemorySize64 Property. ว่าด้วย
PrivateMemorySize64คืนหน่วยความจำของโปรเซสที่ใช้ร่วมกับโปรเซสอื่นไม่ได้ สอดคล้องกับตัวนับประสิทธิภาพ Private Bytes ↩ -
Microsoft Learn, Process.VirtualMemorySize64 Property. ว่าด้วย
VirtualMemorySize64คืนปริมาณหน่วยความจำเสมือนที่จัดสรรให้โปรเซส สอดคล้องกับตัวนับประสิทธิภาพ Virtual Bytes ↩ -
Microsoft Sysinternals, VMMap. ว่าด้วยการแยกหน่วยความจำเสมือนที่ commit ของโปรเซสตามชนิด และการแสดงหน่วยความจำกายภาพ (Working Set) ที่จัดสรรให้แต่ละส่วน พร้อมแผนที่หน่วยความจำละเอียด ↩ ↩2
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
ความลึกของหน่วยความจำ Windows (ตอนที่ 2) — ชีวิตของเพจทางกายภาพ: ห้าบัญชีและเรื่องจริงของไฟล์เพจ
บทความนี้เชื่อมฐานข้อมูล PFN, Standby, Modified, การบีบอัดหน่วยความจำ และไฟล์เพจ เพื่ออธิบายว่าเพจทางกายภาพไปไหนหลังจากออกจาก Working Set.
แอปที่พังเมื่อรีซูมจากสลีป — อีเวนต์พลังงานของ Windows ทำงานอย่างไร และสร้างแอปธุรกิจที่รอดได้อย่างไร
เปิดแล็ปท็อปแล้วการเชื่อมต่อของแอปธุรกิจตาย — สาเหตุคือการออกแบบที่ไม่เคยคิดถึงสลีป บทความนี้ครอบคลุมลำดับการแจ้ง WM_POWERBROADCAST พฤติก...
DllMain และ Loader Lock — เหตุผลจริงที่ถูกบอกให้ "อย่าทำอะไรในการเริ่มต้น DLL"
ทำไมคุณต้องไม่เรียก LoadLibrary หรือซิงโครไนซ์กับเธรดอื่นจาก DllMain บทความนี้อธิบายจากแหล่งปฐมภูมิว่า loader lock ทำให้การแจ้ง DLL ทุกคร...
"ไม่ตอบสนอง" คืออะไรจริง ๆ — Windows ตัดสินว่าแอปแฮงอย่างไร และออกแบบอย่างไรให้ไม่แฮง
"ไม่ตอบสนอง" ของ Windows คือกลไกที่ OS ตัดสินว่าหน้าต่างไม่ได้ดึงข้อความเป็นเวลา 5 วินาทีแล้วแทนที่ด้วยหน้าต่างโกสต์ บทความนี้ครอบคลุมภาย...
WPR/WPA ในทางปฏิบัติ — บทนำการสอบสวนประสิทธิภาพทั้งระบบเมื่อ "พีซีทั้งเครื่องช้า"
ปัญหาประสิทธิภาพอย่าง "พีซีทั้งเครื่องช้า" หรือ "สตาร์ตช้า" ที่ Task Manager ตามไม่ทัน สอบสวนได้ด้วยการจับและอ่าน ETW เทรซทั้ง OS ด้วย WP...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- คอลัมน์ "Memory" ใน Task Manager แสดงหน่วยความจำทั้งหมดที่แอปจัดสรรหรือไม่
- ไม่ Task Manager มีหลายคอลัมน์หน่วยความจำ — ตระกูล Working Set ตระกูล Private Working Set Commit Size และอื่น ๆ — และความหมายขึ้นกับหน้าจอและคอลัมน์ที่คุณดู Working Set คือเพจที่อยู่ใน RAM ตอนนี้ Private Bytes หรือ Commit Size คือปริมาณ commit ของโปรเซสนั้นเอง อย่าอ่านคอลัมน์ "Memory" เดียวเป็นความจุทั้งหมดที่แอปจัดสรร หรือเป็นขนาดของการรั่ว
- Working Set ต่างจาก Private Bytes อย่างไร
- Working Set คือปริมาณเพจที่โปรเซสนั้นมองเห็นและอยู่ใน RAM กายภาพตอนนี้ รวมเพจที่ใช้ร่วมได้เช่นโค้ด DLL และไฟล์ที่แมปหน่วยความจำ Private Bytes คือปริมาณหน่วยความจำที่ commit เฉพาะโปรเซสนั้น ไม่ว่าจะอยู่ใน RAM ตอนนี้หรือไม่ ทั้งคู่จึงไม่เท่ากัน และไม่มีอันใดใหญ่กว่าอีกอันเสมอ
- "Committed 18/32GB" ใน Task Manager หมายความว่าเขียน 18GB ลงเพจไฟล์หรือไม่
- ไม่ ตัวเลขซ้ายคือ commit รวมที่ระบบทั้งก้อนสัญญาจะรองรับตอนนี้ ตัวเลขขวาคือเพดาน commit ที่ระบบรับได้ เพดานประมาณจาก RAM บวกเพจไฟล์ แต่ยอดซ้ายทั้งก้อนไม่ได้วนอยู่บนเพจไฟล์ เพจที่ commit ส่วนใหญ่อยู่ใน RAM และบางเพจที่ commit ยังไม่เคยได้เพจกายภาพ ขณะที่เพจที่โหลดใหม่จากไฟล์ต้นได้ เช่น EXE DLL และไฟล์แมปหน่วยความจำ ไม่จำเป็นต้องเพิ่ม Private Commit เท่าที่เพิ่ม Working Set
- เกิด OutOfMemory ได้แม้ยังมี RAM ว่างหรือไม่
- ได้ การจัดสรรล้มเหลวได้จากเหตุอื่นนอกจาก RAM กายภาพ รวมถึงโปรเซส 32 บิตหมดพื้นที่ที่อยู่เสมือน ไม่มีช่วงที่อยู่ว่างต่อเนื่องขนาดที่ต้องการ เพดาน commit ของระบบ หรือขีดจำกัดเฉพาะของ Job Object หรือรันไทม์ โดยเฉพาะโปรเซส 32 บิตบน Windows 64 บิตมักจำกัดที่ 2GB ของพื้นที่ที่อยู่เสมือนโหมดผู้ใช้ เว้นแต่จะเป็น Large Address Aware
- ปิดเพจไฟล์แล้ว Windows จะเร็วขึ้นหรือไม่
- ไม่ควรถือเป็นกฎทั่วไป การปิดเพจไฟล์ลดเพดาน commit ของระบบ ทำให้ไล่เพจที่ถูกแก้แต่ไม่ได้ใช้ออกจาก RAM ยากขึ้น และกระทบการตั้งค่าครैชดัมป์ ขนาดเพจไฟล์ควรวัดจากยอด commit charge และครैชดัมป์ที่ต้องการ ไม่ใช่การตั้งค่าที่ปิดโดยไม่มีเหตุ
- Page Faults/sec สูงหมายความว่าระบบขาดหน่วยความจำหรือไม่
- บอกจากแค่นั้นไม่ได้ เพจฟอลต์มีซอฟต์ฟอลต์ ซึ่งแก้จากเพจ Standby ใน RAM หรือเพจที่ใช้ร่วมกับโปรเซสอื่น และฮาร์ดฟอลต์ ซึ่งอ่านจากดิสก์ แทนที่จะดู Page Faults/sec โดด ๆ ให้ดู Pages Input/sec, Page Reads/sec, Available MBytes ความหน่วงดิสก์ และเวลาประมวลผลร่วมกันบนไทม์ไลน์เดียวกัน