การวินิจฉัยการรั่วไหลของหน่วยความจำ - ขนาดหน่วยความจำที่อนุญาตเป็น # ไบต์หมด


98

ฉันพบข้อความแสดงข้อผิดพลาดที่น่ากลัวซึ่งอาจเป็นไปได้ด้วยความพยายามอย่างเต็มที่ PHP มีหน่วยความจำไม่เพียงพอ:

ขนาดหน่วยความจำที่อนุญาต #### ไบต์หมด (พยายามจัดสรร #### ไบต์) ใน file.php ในบรรทัด 123

เพิ่มขีด จำกัด

หากคุณรู้ว่าคุณกำลังทำอะไรอยู่และต้องการเพิ่มขีด จำกัด โปรดดูmemory_limit :

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

ระวัง! คุณอาจจะแก้ปัญหาได้เท่านั้นไม่ใช่ปัญหา!

การวินิจฉัยการรั่วไหล:

ข้อความแสดงข้อผิดพลาดชี้ไปที่เส้นที่มีการวนซ้ำซึ่งฉันเชื่อว่ามีการรั่วไหลหรือกำลังสะสมหน่วยความจำโดยไม่จำเป็น ฉันได้พิมพ์memory_get_usage()คำสั่งเมื่อสิ้นสุดการทำซ้ำแต่ละครั้งและสามารถเห็นจำนวนที่เพิ่มขึ้นอย่างช้าๆจนกว่าจะถึงขีด จำกัด :

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

สำหรับวัตถุประสงค์ของคำถามนี้สมมติที่เลวร้ายที่สุดเท่าที่จะเป็นรหัสปาเก็ตตี้เป็นที่ซ่อนตัวอยู่ในขอบเขตทั่วโลกที่ไหนสักแห่งในหรือ$userTask

เครื่องมือกลเม็ด PHP หรือการดีบักวูดูสามารถช่วยฉันค้นหาและแก้ไขปัญหาอะไรได้บ้าง


ป.ล. - เมื่อเร็ว ๆ นี้ฉันพบปัญหาเกี่ยวกับสิ่งที่แน่นอนนี้ น่าเสียดายที่ฉันยังพบว่า php มีปัญหาการทำลายวัตถุเด็ก หากคุณยกเลิกการตั้งค่าอ็อบเจ็กต์พาเรนต์อ็อบเจ็กต์ลูกของมันจะไม่เป็นอิสระ ต้องแน่ใจว่าฉันใช้ unset ที่แก้ไขแล้วซึ่งรวมถึงการเรียกซ้ำไปยังวัตถุลูกทั้งหมด __destruct และอื่น ๆ รายละเอียดที่นี่: paul-m-jones.com/archives/262 :: ฉันกำลังทำสิ่งที่ชอบ: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ รายการ -> __ ทำลาย (); } ยกเลิกการตั้งค่า ($ item); }
Josh

คำตอบ:


48

PHP ไม่มีตัวเก็บขยะ ใช้การนับอ้างอิงเพื่อจัดการหน่วยความจำ ดังนั้นแหล่งที่มาของการรั่วไหลของหน่วยความจำที่พบบ่อยที่สุดคือการอ้างอิงแบบวนรอบและตัวแปรส่วนกลาง หากคุณใช้เฟรมเวิร์กคุณจะมีโค้ดจำนวนมากที่จะลากผ่านเพื่อค้นหามันฉันกลัว เครื่องมือที่ง่ายที่สุดคือการเลือกโทรออกmemory_get_usageและ จำกัด ขอบเขตให้แคบลงจนถึงจุดที่รหัสรั่วไหล คุณยังสามารถใช้xdebugเพื่อสร้างการติดตามของโค้ด รันโค้ดที่มีการติดตามการดำเนินการและshow_mem_delta.


3
แต่ระวัง ... ไฟล์การติดตามที่สร้างขึ้นจะเป็น ENORMOUS ครั้งแรกที่ฉันรันการติดตาม xdebug บนแอป Zend Framework ต้องใช้เวลานานในการรันและสร้างไฟล์ขนาดหลาย GB (ไม่ใช่ KB หรือ MB ... GB) เพียงแค่ตระหนักถึงเรื่องนี้
rg88

1
ใช่มันค่อนข้างหนัก .. GB ฟังดูค่อนข้างมาก - เว้นแต่คุณจะมีสคริปต์ขนาดใหญ่ อาจลองประมวลผลสองสามแถว (ควรจะเพียงพอที่จะระบุการรั่วไหล) นอกจากนี้อย่าติดตั้งส่วนขยาย xdebug บนเซิร์ฟเวอร์ที่ใช้งานจริง
troelskn

31
เนื่องจาก 5.3 PHP มีตัวเก็บขยะ ในทางกลับกันฟังก์ชั่นการสร้างโปรไฟล์หน่วยความจำถูกลบออกไปแล้ว xdebug :(
wdev

3
+1 พบจุดรั่ว! คลาสที่มีการอ้างอิงแบบวนซ้ำ! เมื่อไม่ได้ตั้งค่าการอ้างอิงเหล่านี้ () วัตถุก็ถูกเก็บรวบรวมตามที่คาดไว้! ขอบคุณ! :)
rinogo

@rinogo แล้วคุณรู้ได้อย่างไรเกี่ยวกับการรั่วไหล? คุณสามารถแบ่งปันขั้นตอนที่คุณทำได้หรือไม่?
JohnnyQ

11

นี่คือเคล็ดลับที่เราใช้ในการระบุว่าสคริปต์ใดใช้หน่วยความจำมากที่สุดบนเซิร์ฟเวอร์ของเรา

บันทึกข้อมูลโค้ดต่อไปนี้ในไฟล์ที่เช่น/usr/local/lib/php/strangecode_log_memory_usage.inc.php:

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

ใช้งานได้โดยเพิ่มสิ่งต่อไปนี้ใน httpd.conf:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

จากนั้นวิเคราะห์ไฟล์บันทึกที่ /var/log/httpd/php_memory_log

คุณอาจต้องทำtouch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logก่อนที่ผู้ใช้เว็บของคุณจะสามารถเขียนไฟล์บันทึกได้


8

ฉันสังเกตเห็นครั้งหนึ่งในสคริปต์เก่าว่า PHP จะคงตัวแปร "as" ไว้ในขอบเขตแม้หลังจาก foreach loop ของฉันแล้วก็ตาม ตัวอย่างเช่น,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

ฉันไม่แน่ใจว่าเวอร์ชัน PHP ในอนาคตได้รับการแก้ไขหรือไม่ตั้งแต่ฉันได้เห็นมัน ในกรณีนี้คุณสามารถunset($user)หลังจากdoSomething()บรรทัดเพื่อล้างออกจากหน่วยความจำ YMMV.


13
PHP ไม่กำหนดขอบเขตลูป / เงื่อนไขเช่น C / Java / ฯลฯ สิ่งที่ประกาศภายในลูป / เงื่อนไขยังคงอยู่ในขอบเขตแม้ว่าจะออกจากลูป / เงื่อนไขแล้วก็ตาม (โดยการออกแบบ [?]) ในทางกลับกันวิธีการ / ฟังก์ชั่นจะถูกกำหนดขอบเขตตามที่คุณคาดหวัง - ทุกอย่างจะถูกปล่อยออกมาเมื่อการเรียกใช้ฟังก์ชันสิ้นสุดลง
Frank Farmer

ฉันคิดว่าเป็นเพราะการออกแบบ ข้อดีอย่างหนึ่งก็คือหลังจากวนซ้ำคุณสามารถทำงานกับรายการสุดท้ายที่คุณพบเช่นตรงตามเกณฑ์ที่กำหนด
joachim

คุณทำได้unset()แต่จำไว้ว่าสำหรับวัตถุสิ่งที่คุณทำคือการเปลี่ยนตำแหน่งที่ตัวแปรของคุณชี้ไป - คุณยังไม่ได้ลบออกจากหน่วยความจำจริงๆ PHP จะเพิ่มหน่วยความจำโดยอัตโนมัติเมื่ออยู่นอกขอบเขตดังนั้นทางออกที่ดีกว่า (ในแง่ของคำตอบนี้ไม่ใช่คำถามของ OP) คือการใช้ฟังก์ชั่นสั้น ๆ ดังนั้นจึงไม่แขวนอยู่กับตัวแปรนั้นจากการวนซ้ำเช่นกัน ยาว.
Rich Court

@patcoll นี้ไม่มีส่วนเกี่ยวข้องกับการรั่วไหลของหน่วยความจำ นี่เป็นเพียงตัวชี้อาร์เรย์ที่เปลี่ยนไป ดูที่นี่: prismnet.com/~mcmahon/Notes/arrays_and_pointers.htmlที่เวอร์ชัน 3a
Harm Smits

7

มีหลายจุดที่เป็นไปได้ที่หน่วยความจำรั่วไหลใน php:

  • php นั่นเอง
  • นามสกุล php
  • php ไลบรารีที่คุณใช้
  • รหัส php ของคุณ

มันค่อนข้างยากที่จะค้นหาและแก้ไข 3 ตัวแรกโดยไม่มีความรู้ด้านวิศวกรรมย้อนกลับหรือซอร์สโค้ด php สำหรับข้อสุดท้ายคุณสามารถใช้การค้นหาแบบไบนารีสำหรับรหัสการรั่วไหลของหน่วยความจำด้วยmemory_get_usage


91
คำตอบของคุณเป็นเรื่องทั่วไปที่จะได้รับ
TravisO

2
น่าเสียดายที่แม้แต่ php 7.2 ก็ไม่สามารถแก้ไขการรั่วไหลของหน่วยความจำ core php ได้ คุณไม่สามารถเรียกใช้กระบวนการที่ทำงานเป็นเวลานานได้
Aftab Naveed

6

เมื่อเร็ว ๆ นี้ฉันพบปัญหานี้ในแอปพลิเคชันภายใต้สิ่งที่ฉันรวบรวมว่าเป็นสถานการณ์ที่คล้ายคลึงกัน สคริปต์ที่ทำงานใน cli ของ PHP ที่วนซ้ำหลายครั้ง สคริปต์ของฉันขึ้นอยู่กับไลบรารีพื้นฐานหลายแห่ง ฉันสงสัยว่าห้องสมุดแห่งใดแห่งหนึ่งเป็นสาเหตุและฉันใช้เวลาหลายชั่วโมงโดยเปล่าประโยชน์ในการพยายามเพิ่มวิธีการทำลายที่เหมาะสมให้กับชั้นเรียนโดยไม่มีประโยชน์ ต้องเผชิญกับขั้นตอนการแปลงที่ยาวนานไปยังไลบรารีอื่น (ซึ่งอาจกลายเป็นปัญหาเดียวกันได้) ฉันจึงคิดวิธีแก้ปัญหาอย่างคร่าวๆสำหรับปัญหาในกรณีของฉัน

ในสถานการณ์ของฉันบน linux cli ฉันกำลังวนซ้ำบันทึกผู้ใช้จำนวนมากและสำหรับแต่ละคนสร้างอินสแตนซ์ใหม่ของคลาสต่างๆที่ฉันสร้างขึ้น ฉันตัดสินใจที่จะลองสร้างอินสแตนซ์ใหม่ของคลาสโดยใช้วิธี exec ของ PHP เพื่อให้กระบวนการเหล่านั้นทำงานใน "เธรดใหม่" นี่คือตัวอย่างพื้นฐานจริงๆของสิ่งที่ฉันอ้างถึง:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

เห็นได้ชัดว่าวิธีนี้มีข้อ จำกัด และเราจำเป็นต้องตระหนักถึงอันตรายของสิ่งนี้เนื่องจากการสร้างงานกระต่ายทำได้ง่าย แต่ในบางกรณีที่หายากอาจช่วยให้ผ่านพ้นจุดที่ยากลำบากได้จนกว่าจะพบการแก้ไขที่ดีขึ้น เช่นในกรณีของฉัน


6

ฉันเจอปัญหาเดียวกันและวิธีแก้ปัญหาของฉันคือแทนที่ foreach ด้วยปกติสำหรับ. ฉันไม่แน่ใจเกี่ยวกับข้อมูลจำเพาะ แต่ดูเหมือนว่า foreach จะสร้างสำเนา (หรือการอ้างอิงใหม่) ไปยังวัตถุ การใช้แบบปกติสำหรับการวนซ้ำคุณจะเข้าถึงรายการได้โดยตรง


5

ฉันขอแนะนำให้คุณตรวจสอบคู่มือ php หรือเพิ่มgc_enable()ฟังก์ชั่นเพื่อรวบรวมขยะ ... นั่นคือการรั่วไหลของหน่วยความจำไม่ส่งผลกระทบต่อการทำงานของโค้ดของคุณ

PS: php มีตัวรวบรวมขยะgc_enable()ที่ไม่มีข้อโต้แย้ง


3

เมื่อเร็ว ๆ นี้ฉันสังเกตเห็นว่าฟังก์ชันแลมบ์ดา PHP 5.3 ทำให้หน่วยความจำเพิ่มเติมถูกใช้เมื่อถูกลบ

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

ฉันไม่แน่ใจว่าทำไม แต่ดูเหมือนว่าจะใช้ lambda เพิ่มอีก 250 ไบต์แม้ว่าจะลบฟังก์ชันออกไปแล้วก็ตาม


2
ฉันจะพูดเหมือนกัน สิ่งนี้ได้รับการแก้ไขเมื่อ 5.3.10 ( # 60139 )
Kristopher Ives

@KristopherIves ขอบคุณสำหรับการอัปเดต! ถูกต้องนี่ไม่ใช่ปัญหาอีกต่อไปดังนั้นฉันไม่ควรกลัวที่จะใช้มันอย่างบ้าคลั่งในตอนนี้
Xeoncross

2

หากสิ่งที่คุณพูดเกี่ยวกับ PHP ทำ GC หลังจากฟังก์ชันเป็นจริงคุณสามารถรวมเนื้อหาของลูปไว้ในฟังก์ชันเป็นวิธีแก้ปัญหา / การทดลอง


1
@DavidKullmann จริงๆแล้วฉันคิดว่าคำตอบของฉันผิด run()ท้ายที่สุดสิ่งที่เรียกว่าเป็นฟังก์ชันเช่นกันเมื่อสิ้นสุด GC ควรเกิดขึ้น
Bart van Heukelom

2

หนึ่งในปัญหาใหญ่ที่ฉันได้ถูกโดยใช้create_function เช่นเดียวกับในฟังก์ชันแลมบ์ดามันจะออกจากชื่อชั่วคราวที่สร้างขึ้นในหน่วยความจำ

อีกสาเหตุหนึ่งของการรั่วไหลของหน่วยความจำ (ในกรณีของ Zend Framework) คือ Zend_Db_Profiler ตรวจสอบให้แน่ใจว่าปิดใช้งานอยู่หากคุณเรียกใช้สคริปต์ภายใต้ Zend Framework ตัวอย่างเช่นฉันมีใน application.ini ต่อไปนี้:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

การเรียกใช้แบบสอบถามประมาณ 25,000 คำสั่ง + การประมวลผลจำนวนมากก่อนหน้านั้นทำให้หน่วยความจำมีขนาด 128Mb ที่ดี (ขีด จำกัด หน่วยความจำสูงสุดของฉัน)

เพียงแค่ตั้งค่า:

resources.db.profiler.enabled    = false

มันเพียงพอที่จะให้มันต่ำกว่า 20 Mb

และสคริปต์นี้กำลังทำงานใน CLI แต่มันกำลังสร้างอินสแตนซ์ Zend_Application และเรียกใช้ Bootstrap ดังนั้นจึงใช้การกำหนดค่า "การพัฒนา"

มันช่วยในการรันสคริปต์ด้วยการทำโปรไฟล์ xDebug


2

ฉันไม่เห็นว่ามีการกล่าวถึงอย่างชัดเจน แต่xdebugทำงานได้ดีเยี่ยมเวลาในการทำโปรไฟล์และหน่วยความจำ (ณ2.6 ) คุณสามารถนำข้อมูลที่สร้างขึ้นและส่งต่อไปยังส่วนหน้าของ gui ที่คุณเลือก: webgrind (เวลาเท่านั้น), kcachegrind , qcachegrindหรืออื่น ๆ และสร้างแผนภูมิการโทรและกราฟที่มีประโยชน์มากเพื่อให้คุณค้นหาแหล่งที่มาของความทุกข์ยากต่างๆของคุณ .

ตัวอย่าง (ของ qcachegrind): ป้อนคำอธิบายภาพที่นี่


1

ฉันสายไปหน่อยสำหรับการสนทนานี้ แต่ฉันจะแบ่งปันบางอย่างที่เกี่ยวข้องกับ Zend Framework

ฉันมีปัญหาหน่วยความจำรั่วหลังจากติดตั้ง php 5.3.8 (โดยใช้ phpfarm) เพื่อทำงานกับแอป ZF ที่พัฒนาด้วย php 5.2.9 ผมค้นพบว่าการรั่วไหลของหน่วยความจำที่ถูกเรียกในไฟล์ httpd.conf ของ Apache SetEnv APPLICATION_ENV "development"ในนิยามของฉันโฮสต์เสมือนที่จะกล่าวว่า หลังจากแสดงความคิดเห็นในบรรทัดนี้การรั่วไหลของหน่วยความจำก็หยุดลง ฉันกำลังพยายามหาวิธีแก้ปัญหาแบบอินไลน์ในสคริปต์ php ของฉัน (ส่วนใหญ่กำหนดด้วยตนเองในไฟล์ index.php หลัก)


1
คำถามบอกว่าเขาทำงานใน CLI นั่นหมายความว่า Apache ไม่มีส่วนเกี่ยวข้องใด ๆ ในกระบวนการนี้
Maxime

1
@Maxime จุดดีฉันจับไม่ได้ขอบคุณ หวังว่า Googler แบบสุ่มบางคนจะได้รับประโยชน์จากโน้ตที่ฉันทิ้งไว้ที่นี่เนื่องจากหน้านี้ปรากฏขึ้นสำหรับฉันในขณะที่พยายามแก้ปัญหาของฉัน
fronzee

ตรวจสอบคำตอบของฉันสำหรับคำถามนี้อาจเป็นกรณีของคุณด้วย
Andy

แอปพลิเคชันของคุณควรมีการกำหนดค่าที่แตกต่างกันขึ้นอยู่กับสภาพแวดล้อม "development"สภาพแวดล้อมที่มักจะมีพวงของการเข้าสู่ระบบและ profiling ว่าสภาพแวดล้อมอื่น ๆ อาจไม่ได้ ความเห็นเส้นออกมาก็ทำให้ใบสมัครของคุณใช้สภาพแวดล้อมเริ่มต้นแทนซึ่งมักจะหรือ"production" "prod"หน่วยความจำรั่วยังคงมีอยู่ รหัสที่มีอยู่นั้นไม่ได้ถูกเรียกใช้ในสภาพแวดล้อมนั้น
Marco Roy

0

ฉันไม่เห็นมันกล่าวถึงที่นี่ แต่สิ่งหนึ่งที่อาจเป็นประโยชน์คือการใช้ xdebug และ xdebug_debug_zval ('variableName') เพื่อดูจำนวนการอ้างอิง

ฉันยังสามารถให้ตัวอย่างของส่วนขยาย php ที่เข้ามาในทาง: Z-Ray ของ Zend Server หากเปิดใช้งานการรวบรวมข้อมูลการใช้หน่วยความจำจะบอลลูนในการทำซ้ำแต่ละครั้งเหมือนกับการปิดการรวบรวมขยะ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.