0Pricing
PHP Academy · บทเรียน

การจัดการหน่วยความจำและการเก็บขยะ

ดูการทำงานของ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การทำงานของ Zend Engine
  2. การจัดการหน่วยความจำและการเก็บขยะ
  3. OPcache และการคอมไพล์ JIT
  4. การเขียนส่วนขยาย PHP พื้นฐานด้วย C
← กลับไปที่ PHP Academy