การจัดการหน่วยความจำและการเก็บขยะ
ดูการทำงานของ zval การนับการอ้างอิง และ GC
การจัดการหน่วยความจำและการเก็บขยะ เป็นบทเรียน PHP Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PHP Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
PHP จัดการหน่วยความจำอย่างไร
ค่าของ PHP อยู่ใน โครงสร้าง zval ซึ่งส่วนใหญ่จัดการด้วย การนับจำนวนการอ้างอิง และมี ตัวเก็บกวาดวัฏจักรไว้จัดการกรณีที่การนับจำนวนการอ้างอิงไม่สามารถจัดการได้ การเข้าใจเรื่องนี้ช่วยอธิบายหน่วยความจำรั่วในเวิร์กเกอร์ที่ทำงานต่อเนื่อง ต้นทุนของการคัดลอก และเวลาที่ gc_collect_cycles() มีความสำคัญ
โครงสร้าง zval
zval เป็นภาชนะสำหรับค่า PHP ทุกชนิด ประกอบด้วยป้ายกำกับชนิดและส่วนเก็บค่าที่เป็นยูเนียน ค่าพื้นฐาน เช่น จำนวนเต็ม จำนวนทศนิยม และค่าบูลีน จะจัดเก็บตามค่าอยู่ภายใน zval และจะไม่ถูกนับจำนวนการอ้างอิง (NOT) ส่วนชนิดข้อมูลซับซ้อน เช่น สตริง อาร์เรย์ และออบเจ็กต์ จะชี้ไปยังโครงสร้างที่มีการนับจำนวนการอ้างอิงบนฮีป
<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr; // refcount of the array bumps, no data copy yet (CoW)
// Modifying triggers copy-on-write separation
$copy[] = 4;
var_dump($arr); // still [1,2,3]
var_dump($copy); // [1,2,3,4]
?>การนับจำนวนการอ้างอิง
ค่าแต่ละค่าที่มีการนับจำนวนการอ้างอิงจะติดตามว่ามี zval กี่ตัวชี้มายังค่าดังกล่าว การกำหนดค่าจะเพิ่มจำนวน การยกเลิกการตั้งค่าหรือการออกจากขอบเขตจะลดจำนวนลง เมื่อจำนวนลดลงเหลือศูนย์ หน่วยความจำจะถูกคืนทันที โดยไม่ต้องรอรอบการเก็บกวาด หน่วยความจำส่วนใหญ่ของ PHP ถูกคืนด้วยวิธีนี้
<?php
$a = str_repeat('x', 1000); // refcount 1 on the string
$b = $a; // refcount 2
unset($a); // refcount 1
unset($b); // refcount 0 -> freed instantly
echo "Refcounting frees deterministically\n";
?>การคัดลอกเมื่อเขียน
การกำหนดค่าไม่ได้คัดลอกข้อมูล แต่ใช้ข้อมูลร่วมกันและเพิ่มจำนวนการอ้างอิง (การคัดลอกเมื่อเขียน) การทำสำเนาจริงจะเกิดขึ้นเมื่อมีการเขียนค่าที่ใช้ร่วมกันเป็นครั้งแรกเท่านั้น วิธีนี้ทำให้การส่งอาร์เรย์ขนาดใหญ่มีต้นทุนต่ำ จนกว่าจะมีการแก้ไขอาร์เรย์
<?php
$big = range(1, 100000);
$shared = $big; // O(1): shared, refcount++
// Reads stay shared and cheap
echo count($shared), PHP_EOL;
// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL; // 1 vs -1
?>ปัญหา: วัฏจักร
การนับจำนวนการอ้างอิงเพียงอย่างเดียวไม่สามารถคืนหน่วยความจำของวัฏจักรการอ้างอิงได้ หากออบเจ็กต์ A ถือการอ้างอิงไปยัง B และ B อ้างอิงกลับมายัง A จำนวนการอ้างอิงของทั้งคู่จะไม่ลดลงเหลือศูนย์ แม้คุณจะยกเลิกการตั้งค่าตัวแปรภายนอกทั้งหมดแล้วก็ตาม หน่วยความจำจึงรั่วไหลจนกว่าตัวเก็บกวาดวัฏจักรจะทำงาน
<?php
class Node { public ?Node $ref = null; }
$a = new Node();
$b = new Node();
$a->ref = $b; // a -> b
$b->ref = $a; // b -> a (cycle!)
unset($a, $b); // external refs gone, but internal cycle keeps refcount > 0
// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>ตัวเก็บกวาดวัฏจักร
ตัวเก็บขยะของ PHP ใช้การทำเครื่องหมายและกวาดแบบซิงโครนัสเหนือราก รากของวัฏจักรที่อาจเป็นไปได้ (ค่าที่จำนวนการอ้างอิงถูกลดลงแต่ยังไม่เป็นศูนย์) จะถูกเก็บไว้ในบัฟเฟอร์ เมื่อบัฟเฟอร์รากเต็ม (ค่าเริ่มต้น 10,000) ตัวเก็บขยะจะทำงาน โดยจำลองการลดจำนวนการอ้างอิง ค้นหากลุ่มที่ไม่สามารถเข้าถึงได้จริง และคืนหน่วยความจำของกลุ่มเหล่านั้น
<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
$a = new stdClass();
$b = new stdClass();
$a->b = $b; $b->a = $a; // create a cycle
unset($a, $b); // becomes a GC root
}
print_r(gc_status());
?>ควบคุมตัวเก็บขยะ
คุณสามารถควบคุมตัวเก็บขยะได้ด้วย gc_disable()/gc_enable() ซึ่งใช้เปิดหรือปิดการทำงาน และ gc_collect_cycles() ซึ่งบังคับให้ทำงานหนึ่งรอบและคืนจำนวนรายการที่ถูกคืนหน่วยความจำ ในงานแบบกลุ่มที่ต้องการอัตราการประมวลผลสูง บางครั้งผู้พัฒนาจะปิดตัวเก็บขยะระหว่างช่วงการทำงานที่กระชับ แล้วเรียกเก็บกวาดเพียงครั้งเดียวเมื่อจบช่วง
<?php
gc_disable(); // no automatic cycle collection
// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
$x = new stdClass(); $y = new stdClass();
$x->y = $y; $y->x = $x; unset($x, $y);
}
$freed = gc_collect_cycles(); // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>เวิร์กเกอร์ที่ทำงานต่อเนื่อง
ในดีมอน CLI ตัวรับคิว และเวิร์กเกอร์ของ Swoole/RoadRunner หน่วยความจำจะไม่ถูกคืนจากการปิดคำขอ เพราะกระบวนการทำงานต่อเนื่องเป็นเวลาหลายชั่วโมง ควรเฝ้าระวังสิ่งต่อไปนี้: แคชแบบสแตติกที่เพิ่มขึ้นไม่สิ้นสุด วัฏจักรที่สะสม และทรัพยากรที่ไม่ได้ปิด ตรวจสอบด้วย memory_get_usage(true) และเริ่มเวิร์กเกอร์ใหม่เป็นระยะ
<?php
$peakStart = memory_get_usage(true);
// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
$payload = range(0, 10000);
// ... process ...
unset($payload);
}
printf("start: %d KB, peak: %d KB, now: %d KB\n",
$peakStart >> 10,
memory_get_peak_usage(true) >> 10,
memory_get_usage(true) >> 10
);
?>WeakReference และ WeakMap
หากต้องการแคชหรือเชื่อมโยงข้อมูลกับออบเจ็กต์โดยไม่ทำให้ออบเจ็กต์ยังคงอยู่ ให้ใช้ WeakReference (8.0) และ WeakMap (8.0) รายการใน WeakMap จะไม่เพิ่มจำนวนการอ้างอิงของออบเจ็กต์คีย์ ดังนั้นออบเจ็กต์จึงสามารถถูกเก็บกวาดได้ และรายการของออบเจ็กต์นั้นจะถูกลบโดยอัตโนมัติ เหมาะอย่างยิ่งสำหรับตารางเสริมของข้อมูลเมตาที่ต้องไม่ทำให้หน่วยความจำรั่ว
<?php
$map = new WeakMap();
$obj = new stdClass();
$map[$obj] = 'metadata'; // does NOT keep $obj alive
echo count($map) . PHP_EOL; // 1
unset($obj); // object collectible -> entry removed
echo count($map) . PHP_EOL; // 0
?>สตริงที่จัดเก็บร่วมและค่าที่ไม่เปลี่ยนรูป
สตริงลิเทอรัลในซอร์สโค้ดจะถูกจัดเก็บร่วม โดยจัดเก็บไว้เพียงครั้งเดียว ใช้ร่วมกัน และไม่ถูกนับจำนวนการอ้างอิงหรือคืนหน่วยความจำระหว่างคำขอ OPcache จะย้ายสตริงที่จัดเก็บร่วมและอาร์เรย์รหัสคำสั่งที่ไม่เปลี่ยนรูป (ซึ่งแคชไว้) ไปไว้ในหน่วยความจำใช้ร่วมกันระหว่างคำขอ นี่เป็นเหตุผลที่การคัดลอกสตริงลิเทอรัลแทบไม่มีต้นทุน
ขีดจำกัดหน่วยความจำและตัวจัดสรร
PHP ใช้ตัวจัดสรร emalloc ของตนเอง ซึ่งเป็นตัวจัดการหน่วยความจำของ Zend ที่ทำงานบนตัวจัดสรรของระบบอีกชั้นหนึ่ง และติดตามการใช้งานต่อคำขอเทียบกับ memory_limit เมื่อถึงขีดจำกัดจะเกิดข้อผิดพลาดร้ายแรง ค่า true ใน memory_get_usage(true) จะรายงานบล็อกที่จัดสรรจริงจาก OS ไม่ใช่เพียงหน่วยความจำที่สคริปต์ของคุณกำลังถืออยู่ในขณะนั้น
<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n"; // ZendMM tracked
echo 'real usage: ' . memory_get_usage(true) . " bytes\n"; // from OS
?>ตรวจสอบความเข้าใจ
เหตุใดการนับจำนวนการอ้างอิงเพียงอย่างเดียวจึงทำให้ออบเจ็กต์บางส่วนรั่วไหลได้
สรุป
PHP จัดเก็บค่าไว้ในโครงสร้าง zval ค่าพื้นฐานจัดเก็บตามค่า ส่วนชนิดข้อมูลซับซ้อนจัดสรรบนฮีปและมีการนับจำนวนการอ้างอิงร่วมกับการคัดลอกเมื่อเขียน การนับจำนวนการอ้างอิงจะคืนหน่วยความจำอย่างแน่นอนเมื่อจำนวนเป็นศูนย์ แต่ไม่สามารถคืนหน่วยความจำของวัฏจักรได้ ดังนั้นตัวเก็บกวาดแบบทำเครื่องหมายและกวาดจะทำงานเมื่อบัฟเฟอร์รากเต็ม (หรือเมื่อเรียกใช้ gc_collect_cycles()) ใช้ WeakMap/WeakReference สำหรับการเชื่อมโยงที่ไม่ได้เป็นเจ้าของข้อมูล และเฝ้าระวังการเพิ่มขึ้นไม่สิ้นสุดในเวิร์กเกอร์ที่ทำงานต่อเนื่อง
คำถามที่พบบ่อย
บทเรียน “การจัดการหน่วยความจำและการเก็บขยะ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การจัดการหน่วยความจำและการเก็บขยะ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PHP Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PHP Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การจัดการหน่วยความจำและการเก็บขยะ”
ดูการทำงานของ zval การนับการอ้างอิง และ GC คุณปฏิบัติ PHP Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PHP Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน PHP Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การจัดการหน่วยความจำและการเก็บขยะ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน PHP Academy นี้ได้ไหม
ได้ บทเรียน PHP Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การทำงานของ Zend Engine
- การจัดการหน่วยความจำและการเก็บขยะ
- OPcache และการคอมไพล์ JIT
- การเขียนส่วนขยาย PHP พื้นฐานด้วย C