Rò rỉ bộ nhớ trong PHP là gì?

🥳🥳🥳
// The `count` variable is the number of freed objects during the last gc_collect_cycles() call
static void gc_adjust_threshold(int count)
{
	uint32_t new_threshold;

	/* TODO Very simple heuristic for dynamic GC buffer resizing:
	 * If there are "too few" collections, increase the collection threshold
	 * by a fixed step */
	if (count < GC_THRESHOLD_TRIGGER) {
		/* increase */
		if (GC_G(gc_threshold) < GC_THRESHOLD_MAX) {
			new_threshold = GC_G(gc_threshold) + GC_THRESHOLD_STEP;
			if (new_threshold > GC_THRESHOLD_MAX) {
				new_threshold = GC_THRESHOLD_MAX;
			}
			if (new_threshold > GC_G(buf_size)) {
				gc_grow_root_buffer();
			}
			if (new_threshold <= GC_G(buf_size)) {
				GC_G(gc_threshold) = new_threshold;
			}
		}
	} else if (GC_G(gc_threshold) > GC_THRESHOLD_DEFAULT) {
		new_threshold = GC_G(gc_threshold) - GC_THRESHOLD_STEP;
		if (new_threshold < GC_THRESHOLD_DEFAULT) {
			new_threshold = GC_THRESHOLD_DEFAULT;
		}
		GC_G(gc_threshold) = new_threshold;
	}
}
1 không còn cần thiết nữa và lập lịch trình GC được cải thiện phần lớn ngay cả trong các trường hợp khác, giúp giảm đáng kể dung lượng bộ nhớ của chúng tôi. 🥳🥳🥳

Nếu lỗi này nghe có vẻ quen thuộc, thì bài đăng này là dành cho bạn. Vấn đề với thông báo này là nó không cho bạn biết nhiều. nó không cho bạn biết tất cả bộ nhớ đã được phân bổ ở đâu. Định vị những nơi tiêu thụ nhiều bộ nhớ trong các hệ thống lớn và phức tạp là điều không dễ dàng. May mắn thay, có một số công cụ có thể giúp tìm mã có vấn đề. Trong bài đăng này, chúng tôi sẽ đề cập đến hai phương pháp để tìm các vị trí trong chương trình của bạn, nơi có nhiều bộ nhớ được phân bổ

Chạy ví dụ

Chúng tôi sẽ sử dụng đoạn mã sau làm ví dụ đang chạy. Mục đích của mã là tìm Nemo. Có hai chức năng. một cái đọc tệp có thể định vị Nemo và cái còn lại cố gắng tìm một dòng có nội dung bằng 'nemo'. Vấn đề với đoạn mã này là đôi khi nó tiêu tốn quá nhiều bộ nhớ. Không phải lúc nào cũng vậy, nhưng với một số tệp nhất định, chương trình sẽ gặp sự cố

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}

finding_nemo("the_sea.txt");

Tất cả các kỹ thuật và công cụ được mô tả là không cần thiết để giải quyết vấn đề này (bạn đã có thể phát hiện ra vấn đề chưa?), nhưng nó cho phép chúng tôi xem các công cụ và kỹ thuật đó hoạt động như thế nào trong thực tế

memory_get_usage()

PHP có hai chức năng có thể cho bạn biết điều gì đó về việc sử dụng bộ nhớ của chương trình của bạn. memory_get_usage và memory_get_peak_usage
memory_get_usagechỉ cung cấp thông tin chi tiết về lượng bộ nhớ đang được sử dụng tại thời điểm gọi hàm. memory_get_peak_usage trả về số byte tối đa được cấp phát bởi chương trình cho đến khi gọi hàm. Cả hai hàm này đều nhận một đối số boolean. $real_usage. Nếu $real_usage được đặt thành

0, thì memory_get_usage trả về tổng dung lượng bộ nhớ thực sự được phân bổ từ hệ điều hành, nhưng một phần trong số đó có thể chưa (chưa) được chương trình của bạn sử dụng. Nếu nó được đặt thành
2, nó sẽ trả về số byte mà PHP đã yêu cầu (và nhận) từ hệ điều hành và thực tế đang được chương trình sử dụng. Mệnh đề sau luôn đúng.
3. Bộ nhớ được yêu cầu theo khối, không phải lúc nào cũng được sử dụng đầy đủ

Một lợi thế của việc sử dụng các chức năng này là chúng thực sự dễ sử dụng. Một trong những cách khả thi để tìm ra lỗi rò rỉ bộ nhớ của bạn là phân tán lệnh gọi tới ____26_______ trên toàn bộ mã của bạn và ghi nhật ký đầu ra của nó. Sau đó, bạn có thể thử tìm một mẫu. việc sử dụng bộ nhớ tăng ở đâu?
Một nhược điểm của các chức năng này là việc sử dụng chúng bị hạn chế vì chúng không cung cấp thông tin chi tiết về chức năng hoặc lớp nào đang sử dụng tất cả bộ nhớ đó

Hãy sử dụng các hàm này để biết được vấn đề hiện tại của chúng ta có thể nằm ở đâu. Trong ví dụ bên dưới, tôi sử dụng các ký tự đánh dấu như 'A', 'B', v.v. để có thể theo dõi một mục nhập nhật ký trở lại một vị trí trong mã. Một tùy chọn khác là đưa 'hằng số kỳ diệu' như

5 và 
6 vào đầu ra nhật ký của bạn

Khi chúng tôi chạy ví dụ của mình bây giờ, chúng tôi có đầu ra nhật ký sau

7
8
9
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
0

Nó thật thú vị. cho đến điểm đánh dấu A, không có vấn đề gì. Giữa dòng A và B, nỗi nhớ chợt dâng trào. Các điểm đánh dấu này tương ứng với phần đầu và phần cuối của hàm

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1. Hãy thử xác nhận giả thuyết này bằng một kỹ thuật khác

Trình hồ sơ Xdebug

Là một lập trình viên PHP, chắc hẳn bạn đã từng nghe đến (và sử dụng) Xdebug. Nếu bạn chưa có, hãy kiểm tra và đảm bảo cài đặt nó. Những gì bạn có thể không biết là nó cũng đi kèm với một hồ sơ. một công cụ cung cấp cái nhìn sâu sắc về hành vi thời gian chạy của một chương trình. Trình lược tả này phức tạp hơn nhiều so với các hàm PHP đã đề cập trước đó. thay vì chỉ cung cấp cho bạn thông tin về lượng bộ nhớ được sử dụng, nó cũng cung cấp thông tin chi tiết về chức năng nào đang thực sự cấp phát bộ nhớ. Đây là một lợi thế so với kỹ thuật trước đó, bởi vì nếu bạn thực sự không biết tìm kiếm vấn đề về bộ nhớ của mình ở đâu, thì bạn sẽ phải phân tán một lượng lớn lệnh gọi tới _______26_______ trên toàn bộ cơ sở mã của mình. Trước khi có thể sử dụng trình hồ sơ, có một số thứ cần được cấu hình trong

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
3 của bạn. Lưu ý rằng hầu hết các tùy chọn này không thể được đặt trong thời gian chạy bằng cách sử dụng
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
4
Trước tiên, bạn phải kích hoạt trình hồ sơ. Điều này có thể được thực hiện theo hai cách. bằng cách sử dụng
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
5 hoặc bằng cách sử dụng 
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
6. Khi sử dụng tùy chọn đầu tiên, một hồ sơ sẽ được tạo cho mỗi lần chạy chương trình của bạn. Tùy chọn thứ hai chỉ tạo hồ sơ cho chương trình đang chạy của bạn nếu có biến _____11_______7/
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
8 hoặc ____11_______9 được đặt với tên _____26_______0 Bạn cũng phải cho Xdebug biết nơi lưu trữ các tệp đã tạo, sử dụng ____26_______1
Có nhiều thứ hơn để định cấu hình, nhưng với những cài đặt này, bạn đã sẵn sàng

Bây giờ, hãy chạy lại tập lệnh với trình cấu hình Xdebug được bật. Nếu chúng tôi xem xét thư mục đầu ra đã định cấu hình của bạn, chúng tôi có thể tìm thấy cấu hình được tạo ở đó. Tuy nhiên trước khi mở được chúng ta cần một công cụ khác. qCachegrind dành cho Windows hoặc kCachegrind dành cho Linux. Tôi sẽ sử dụng qCachegrind ngay bây giờ

Khi mở profile bằng qCachegrind bạn sẽ thấy như hình bên dưới

Rò rỉ bộ nhớ trong PHP là gì?

Đảm bảo rằng bạn chọn 'Bộ nhớ' trong menu thả xuống ở đầu cửa sổ, thay vì 'Thời gian' (tùy chọn này có thể hữu ích nếu hiệu suất là một vấn đề)
Khi nhìn vào 'bản đồ callee' của mục nhập memory_get_usage2 trong danh sách chức năng, bạn có thể thấy kích thước của các khối mà các chức năng được gọi đã cấp phát bộ nhớ như thế nào. Các khối lớn hơn là thú vị nhất. đây là những chức năng phân bổ nhiều bộ nhớ nhất. Mỗi chức năng được gọi nằm bên trong người gọi trong bản đồ callee
Trong phần 'Cấu hình phẳng' ở bên trái, bạn có thể thấy danh sách các chức năng. Đối với mỗi chức năng, có một 'Incl. ' và cột 'Bản thân'. 'Incl' cho biết dung lượng bộ nhớ được cấp phát bởi chức năng này, bao gồm tất cả bộ nhớ được cấp phát bởi các callees của chức năng đó. 'Tự' hiển thị bộ nhớ được cấp phát bởi chính chức năng đó
Các chức năng thú vị nhất để xem xét là những chức năng có giá trị tương đối cao trong cột 'Bản thân'
Như chúng ta có thể thấy, có hai chức năng chiếm rất nhiều bộ nhớ. memory_get_usage3 và memory_get_usage4. Nhưng đợi đã. memory_get_usage5 chỉ cắt từng dòng một và memory_get_usage6 chỉ đọc từng dòng một phải không? . nó tạo ra một 'hồ sơ bộ nhớ tích lũy', tôi. e. khi một chức năng được gọi nhiều lần, nó sẽ hiển thị tổng của tất cả bộ nhớ đã được sử dụng trong các lần khác nhau mà nó được gọi

Mặc dù cấu hình Xdebug có nhược điểm của nó, nhưng nó cho phép chúng tôi xem vị trí (có khả năng) rất nhiều bộ nhớ được phân bổ. Chúng tôi có thể xác nhận giả thuyết của mình, cụ thể là

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1 dường như có vấn đề, vì hàm này gọi hai hàm PHP phân bổ nhiều bộ nhớ

sửa lỗi kịch bản

Lưu ý rằng trình lược tả hoặc ghi nhật ký sử dụng bộ nhớ hầu như sẽ không bao giờ cung cấp cho bạn câu trả lời chính xác về vấn đề bộ nhớ của bạn nằm ở đâu hoặc ở đâu. Phân tích thủ công sẽ luôn là một phần trong quy trình sửa lỗi của bạn. Tuy nhiên, các công cụ này giúp bạn hình thành ý tưởng về vấn đề có thể xảy ra. Tại thời điểm này, chúng tôi biết chức năng nào có thể có vấn đề. Khi phân tích kỹ hơn về hàm

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1, chúng ta có thể thấy rằng hàm này sử dụng một mảng để đệm toàn bộ tệp. Nếu tệp lớn, chương trình sẽ hết bộ nhớ. Bây giờ chúng tôi có đủ thông tin để sửa nó
Hãy làm việc với giả định rằng
 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1 cũng được sử dụng ở nơi khác và hành vi của nó sẽ không thay đổi. May mắn thay, có một giải pháp cho vấn đề này. chúng tôi không thực sự phải tải toàn bộ tệp

Một cách tương đối đơn giản để giải quyết vấn đề này là sử dụng Trình tạo. Bài đăng tuyệt vời này mô tả khái niệm chi tiết hơn

Nói tóm lại, Trình tạo cho phép bạn viết một trình vòng lặp cơ bản, nơi bạn có quyền kiểm soát thông tin nào cần thiết trong bộ nhớ. Khi lặp qua một trình vòng lặp, vòng lặp sẽ kiểm soát thời điểm nó tìm nạp mục tiếp theo từ trình vòng lặp. Vì trình vòng lặp biết cách tìm nạp mục tiếp theo, nên nó không nhất thiết phải có tất cả các mục trong bộ nhớ. Trong ví dụ này, điều này có nghĩa là sẽ chỉ có một dòng tệp trong bộ nhớ tại một thời điểm

Hãy xem mã ví dụ ở trên, với Trình tạo

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}

Thật thú vị, hàm memory_get_peak_usage0 không phải thay đổi. memory_get_peak_usage1 vòng lặp không có vấn đề gì với Máy phát điện. Hàm 

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1 đã thay đổi. nó hiện chứa câu lệnh memory_get_peak_usage3

Khi ghi nhật ký sử dụng bộ nhớ cho đoạn mã này, chúng tôi có thể thấy rằng mức sử dụng vẫn ở mức thấp. Tuy nhiên, vì Xdebug tạo ra một cấu hình bộ nhớ tích lũy, cấu hình Xdebug sẽ ít nhiều giống nhau. Điều này xảy ra bởi vì tổng cộng, hàm 

 $line) {
		if ($line === "nemo") {
			return $line_number;
		}
	}
	return -1; //nemo not found
}
1 thực sự phân bổ cùng một lượng bộ nhớ. Tuy nhiên, chức năng hiện giải phóng bộ nhớ được phân bổ sớm hơn, dẫn đến việc sử dụng bộ nhớ nói chung thấp hơn nhiều so với phiên bản trước. Đây là một trong những nhược điểm của việc sử dụng Xdebug profiler. Trong bài đăng tiếp theo, tôi sẽ trình bày cách sử dụng php-memory-profiler để tạo một loại cấu hình bộ nhớ khác

Phần kết luận

Trong bài đăng này, chúng ta đã thấy hai phương pháp xác định vị trí trong chương trình PHP của bạn, nơi có nhiều bộ nhớ được phân bổ. đầu tiên, bằng cách sử dụng hàm memory_get_usage của PHP, sau đó bằng cách tạo cấu hình bộ nhớ bằng Xdebug và phân tích cấu hình đó bằng qCachegrind. Một điều cần lưu ý là không có công cụ hoặc kỹ thuật nào có thể chỉ ra vấn đề một cách chắc chắn. Như vậy, quy trình sửa lỗi của bạn sẽ luôn bao gồm ít nhất một phần phân tích thủ công. Trong bài đăng tiếp theo, tôi sẽ trình bày cách php-memory-profiler có thể giúp bạn tìm lỗi rò rỉ bộ nhớ trong chương trình của bạn