Phân mảnh đống đối tượng lớn


97

Ứng dụng C # /. NET mà tôi đang làm việc bị rò rỉ bộ nhớ chậm. Tôi đã sử dụng CDB với SOS để cố gắng xác định điều gì đang xảy ra nhưng dữ liệu dường như không có ý nghĩa gì vì vậy tôi hy vọng một trong số các bạn có thể đã trải qua điều này trước đây.

Ứng dụng đang chạy trên khuôn khổ 64 bit. Nó liên tục tính toán và tuần tự hóa dữ liệu tới một máy chủ từ xa và đang tấn công Khối Đối tượng Lớn (LOH) một chút. Tuy nhiên, hầu hết các đối tượng LOH mà tôi mong đợi là tạm thời: sau khi tính toán hoàn tất và đã được gửi đến máy chủ từ xa, bộ nhớ sẽ được giải phóng. Tuy nhiên, những gì tôi đang thấy là một số lượng lớn các mảng đối tượng (trực tiếp) xen kẽ với các khối bộ nhớ miễn phí, ví dụ: lấy một đoạn ngẫu nhiên từ LOH:

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Rõ ràng là tôi sẽ mong đợi điều này xảy ra nếu ứng dụng của tôi đang tạo ra các đối tượng lớn, tồn tại lâu dài trong mỗi lần tính toán. (Nó làm được điều này và tôi chấp nhận rằng sẽ có một mức độ phân mảnh LOH nhưng đó không phải là vấn đề ở đây.) Vấn đề là mảng đối tượng rất nhỏ (1056 byte) bạn có thể thấy trong kết xuất ở trên mà tôi không thể thấy trong mã đang được tạo và vẫn được root bằng cách nào đó.

Cũng lưu ý rằng CDB không báo cáo loại khi phân đoạn heap bị kết xuất: Tôi không chắc liệu điều này có liên quan hay không. Nếu tôi kết xuất đối tượng được đánh dấu (<-), CDB / SOS báo cáo nó tốt:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Các phần tử của mảng đối tượng là tất cả các chuỗi và các chuỗi có thể nhận dạng được từ mã ứng dụng của chúng tôi.

Ngoài ra, tôi không thể tìm thấy gốc GC của chúng vì lệnh! GCRoot bị treo và không bao giờ quay trở lại (tôi thậm chí đã thử để nó qua đêm).

Vì vậy, tôi sẽ đánh giá rất cao nếu ai đó có thể làm sáng tỏ tại sao các mảng đối tượng nhỏ (<85k) này lại kết thúc trên LOH: .NET sẽ đưa một mảng đối tượng nhỏ vào đó trong những tình huống nào? Ngoài ra, có ai tình cờ biết về một cách khác để xác định nguồn gốc của những vật thể này không?


Cập nhật 1

Một giả thuyết khác mà tôi đưa ra vào cuối ngày hôm qua là các mảng đối tượng này bắt đầu lớn nhưng đã bị thu hẹp lại để lại các khối bộ nhớ trống hiện rõ trong các bãi chứa bộ nhớ. Điều khiến tôi nghi ngờ là các mảng đối tượng luôn có vẻ dài 1056 byte (128 phần tử), 128 * 8 cho các tham chiếu và 32 byte cho chi phí.

Ý tưởng là có lẽ một số mã không an toàn trong thư viện hoặc trong CLR đang làm hỏng trường số phần tử trong tiêu đề mảng. Hơi dài mà tôi biết ...


Cập nhật 2

Nhờ Brian Rasmussen (xem câu trả lời được chấp nhận), vấn đề đã được xác định là sự phân mảnh của LOH gây ra bởi bảng thực tập chuỗi! Tôi đã viết một ứng dụng thử nghiệm nhanh để xác nhận điều này:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

Đầu tiên, ứng dụng tạo và bỏ tham chiếu các chuỗi duy nhất trong một vòng lặp. Điều này chỉ để chứng minh rằng bộ nhớ không bị rò rỉ trong trường hợp này. Rõ ràng là nó không nên và nó không.

Trong vòng lặp thứ hai, các chuỗi duy nhất được tạo và thực hiện. Hành động này bắt nguồn từ bảng thực tập. Điều tôi không nhận ra là bảng thực tập được thể hiện như thế nào. Có vẻ như nó bao gồm một tập hợp các trang - mảng đối tượng gồm 128 phần tử chuỗi - được tạo trong LOH. Điều này rõ ràng hơn trong CDB / SOS:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

Lấy một đoạn LOH kết xuất cho thấy mô hình tôi đã thấy trong ứng dụng bị rò rỉ:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Lưu ý rằng kích thước mảng đối tượng là 528 (thay vì 1056) vì máy trạm của tôi là 32 bit và máy chủ ứng dụng là 64 bit. Mảng đối tượng vẫn dài 128 phần tử.

Vì vậy, đạo lý của câu chuyện này là phải thực tập rất cẩn thận. Nếu chuỗi bạn đang thực hiện không được biết là thành viên của một tập hợp hữu hạn thì ứng dụng của bạn sẽ bị rò rỉ do sự phân mảnh của LOH, ít nhất là trong phiên bản 2 của CLR.

Trong trường hợp ứng dụng của chúng tôi, có mã chung trong đường dẫn mã giải mã hóa thực tập nhận dạng thực thể trong quá trình giải phóng: Tôi thực sự nghi ngờ đây là thủ phạm. Tuy nhiên, ý định của nhà phát triển rõ ràng là tốt vì họ muốn đảm bảo rằng nếu cùng một thực thể được giải mã nhiều lần thì chỉ một phiên bản của chuỗi định danh sẽ được duy trì trong bộ nhớ.


2
Câu hỏi hay - Tôi đã nhận thấy điều tương tự trong ứng dụng của mình. Các vật thể nhỏ còn lại trong LOH sau khi các khối lớn được làm sạch, và nó gây ra các vấn đề phân mảnh.
Reed Copsey

2
Tôi đồng ý, câu hỏi tuyệt vời. Tôi sẽ theo dõi để có câu trả lời.
Charlie Flowers

2
Rất thú vị. Có vẻ như nó là một vấn đề khá khó khăn để gỡ lỗi!
Matt Jordan

Câu trả lời:


46

CLR sử dụng LOH để định vị trước một vài đối tượng (chẳng hạn như mảng được sử dụng cho các chuỗi đan xen ). Một số trong số này nhỏ hơn 85000 byte và do đó thường sẽ không được phân bổ trên LOH.

Đó là một chi tiết triển khai, nhưng tôi cho rằng lý do của điều này là để tránh việc thu thập rác không cần thiết của các phiên bản được cho là tồn tại miễn là quá trình tự nó diễn ra.

Cũng do tối ưu hóa hơi bí truyền, bất kỳ double[]phần tử nào trong số 1000 phần tử trở lên cũng được phân bổ trên LOH.


Các đối tượng có vấn đề là đối tượng [] chứa các tham chiếu đến chuỗi mà tôi biết đang được tạo bởi mã ứng dụng. Điều này ngụ ý rằng ứng dụng đang tạo đối tượng [] (tôi không thể thấy bằng chứng về điều này) hoặc một số phần của CLR (chẳng hạn như tuần tự hóa) đang sử dụng chúng để hoạt động trên các đối tượng ứng dụng.
Paul Ruane

1
Đó có thể là cấu trúc bên trong được sử dụng cho các chuỗi xen kẽ. Vui lòng kiểm tra câu trả lời của tôi cho câu hỏi này để biết thêm chi tiết: stackoverflow.com/questions/372547/…
Brian Rasmussen

Ah, đây là một lời dẫn rất thú vị, cảm ơn. Hoàn toàn quên mất bảng thực tập. Tôi biết một trong những nhà phát triển của chúng tôi là một người thực tập nhạy bén nên đây chắc chắn là điều tôi sẽ điều tra.
Paul Ruane

1
85000 byte hay 84 * 1024 = 87040 byte?
Peter Mortensen

5
85000 byte. Bạn có thể xác minh điều này bằng cách tạo một mảng byte 85000-12 (kích thước chiều dài, MT, khối đồng bộ) và gọi GC.GetGenerationphiên bản. Điều này sẽ trả về Gen2 - API không phân biệt giữa Gen2 và LOH. Làm cho mảng nhỏ hơn một byte và API sẽ trả về Gen0.
Brian Rasmussen

13

.NET Framework 4.5.1, có khả năng nén rõ ràng đống đối tượng lớn (LOH) trong quá trình thu gom rác.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

Xem thêm thông tin trong GCSettings.LargeObjectHeapCompactionMode


2

Khi đọc mô tả về cách GC hoạt động và phần về cách các vật thể tồn tại lâu dài kết thúc ở thế hệ 2 và việc thu thập các đối tượng LOH chỉ diễn ra ở bộ sưu tập đầy đủ - cũng như bộ sưu tập ở thế hệ 2, ý tưởng xuất hiện trong đầu. .. tại sao không chỉ giữ các đối tượng thế hệ 2 và lớn trong cùng một đống, vì chúng sẽ được thu thập cùng nhau?

Nếu đó là những gì thực sự xảy ra thì nó sẽ giải thích cách các vật thể nhỏ kết thúc ở cùng một nơi với LOH - nếu chúng sống đủ lâu để kết thúc ở thế hệ 2.

Và do đó, vấn đề của bạn có vẻ là một phản bác khá tốt đối với ý tưởng xảy ra với tôi - nó sẽ dẫn đến sự phân mảnh của LOH.

Tóm tắt: vấn đề của bạn có thể được giải thích bởi LOH và thế hệ 2 chia sẻ cùng một vùng heap, mặc dù điều đó không có nghĩa là bằng chứng rằng đây là lời giải thích.

Cập nhật: đầu ra của !dumpheap -statkhá nhiều điều đã thổi bay lý thuyết này! Thế hệ 2 và LOH có các vùng riêng.


Sử dụng! Eeheap để hiển thị các phân đoạn tạo nên mỗi heap. Gen 0 và gen 1 sống trong một phân đoạn (cùng một phân đoạn), gen 2 và LOH đều có thể phân bổ nhiều phân đoạn nhưng các phân đoạn đó cho mỗi heap vẫn riêng biệt.
Paul Ruane

Vâng, đã thấy điều đó, cảm ơn. Chỉ muốn đề cập đến lệnh! Eeheaps vì nó thể hiện hành vi này một cách rõ ràng hơn nhiều.
Paul Ruane

Hiệu quả của GC chính phần lớn bắt nguồn từ thực tế là nó có thể định vị lại các đối tượng nên sẽ chỉ có một số lượng nhỏ các vùng bộ nhớ trống trên heap chính. Nếu một đối tượng trên heap chính được ghim trong một bộ sưu tập, không gian bên trên và bên dưới đối tượng được ghim có thể phải được theo dõi riêng biệt, nhưng vì số lượng đối tượng được ghim thường rất nhỏ, do đó, số vùng riêng biệt GC phải theo dõi. Trộn các đối tượng có thể di dời và không thể di chuyển (lớn) trong cùng một đống sẽ làm giảm hiệu suất.
supercat

Một câu hỏi thú vị hơn là tại sao .NET đặt doublecác mảng lớn hơn 1000 phần tử trên LOH, thay vì điều chỉnh GC để đảm bảo rằng chúng được căn chỉnh trên ranh giới 8 byte. Trên thực tế, ngay cả trên hệ thống 32 bit, tôi mong đợi rằng do hành vi bộ nhớ cache, việc áp đặt căn chỉnh 8 byte trên tất cả các đối tượng có kích thước được phân bổ là bội số của 8 byte có thể sẽ là một chiến thắng về hiệu suất. Nếu không, mặc dù hiệu suất của một thiết bị được sử dụng nhiều được double[]căn chỉnh trong bộ nhớ cache sẽ tốt hơn so với một thiết bị không được sử dụng, tôi không biết tại sao kích thước lại tương quan với việc sử dụng.
supercat

@supercat Ngoài ra, hai heap cũng hoạt động rất khác nhau trong phân bổ. Heap chính (tại thời điểm này) về cơ bản là một ngăn xếp trong các mẫu phân bổ - nó luôn phân bổ ở trên cùng, bỏ qua bất kỳ không gian trống nào - khi nén xong, các không gian trống sẽ bị vắt kiệt. Điều này làm cho việc phân bổ gần như không phải chọn và giúp định vị dữ liệu. Mặt khác, phân bổ trên LOH tương tự như cách hoạt động của malloc - nó sẽ tìm vị trí trống đầu tiên có thể chứa những gì bạn đang phân bổ và phân bổ ở đó. Vì nó dành cho các đối tượng lớn, vị trí dữ liệu là một điều kiện nhất định và hình phạt cho việc phân bổ không quá tệ.
Luaan

1

Nếu định dạng có thể được nhận dạng là ứng dụng của bạn, tại sao bạn vẫn chưa xác định được mã đang tạo ra định dạng chuỗi này? Nếu có một số khả năng, hãy thử thêm dữ liệu duy nhất để tìm ra đường dẫn mã nào là thủ phạm.

Thực tế là các mảng được xen kẽ với các mục lớn được giải phóng khiến tôi đoán rằng ban đầu chúng được ghép nối hoặc ít nhất là có liên quan. Cố gắng xác định các đối tượng được giải phóng để tìm ra những gì đã tạo ra chúng và các chuỗi liên quan.

Khi bạn xác định được điều gì đang tạo ra các chuỗi này, hãy cố gắng tìm ra điều gì sẽ giữ chúng không bị GCed. Có lẽ chúng đang được đưa vào một danh sách bị lãng quên hoặc không được sử dụng cho mục đích ghi nhật ký hoặc điều gì đó tương tự.


CHỈNH SỬA: Bỏ qua vùng bộ nhớ và kích thước mảng cụ thể tại thời điểm này: chỉ cần tìm ra những gì đang được thực hiện với các chuỗi này để gây ra rò rỉ. Hãy thử! GCRoot khi chương trình của bạn đã tạo hoặc thao tác các chuỗi này chỉ một hoặc hai lần, khi có ít đối tượng hơn để theo dõi.


Các chuỗi là sự kết hợp của Guid (mà chúng tôi sử dụng) và các khóa chuỗi có thể nhận dạng dễ dàng. Tôi có thể thấy nơi chúng được tạo, nhưng chúng không bao giờ được thêm (trực tiếp) vào mảng đối tượng và chúng tôi không tạo mảng 128 phần tử một cách rõ ràng. Mặc dù vậy, những mảng nhỏ này không nên nằm trong LOH.
Paul Ruane

1

Câu hỏi hay, tôi đã học được bằng cách đọc các câu hỏi.

Tôi nghĩ rằng các bit khác của đường dẫn mã deserialisation cũng đang sử dụng đống đối tượng lớn, do đó phân mảnh. Nếu tất cả các chuỗi được thực hiện vào cùng một thời điểm, tôi nghĩ bạn sẽ ổn.

Với trình thu gom rác .net tốt như thế nào, chỉ cần cho phép đường dẫn mã deserialisation tạo đối tượng chuỗi bình thường là đủ tốt. Đừng làm bất cứ điều gì phức tạp hơn cho đến khi nhu cầu được chứng minh.

Hầu hết tôi sẽ xem xét việc giữ một bảng băm của một vài chuỗi cuối cùng bạn đã thấy và sử dụng lại những chuỗi này. Bằng cách giới hạn kích thước bảng băm và chuyển kích thước vào khi tạo bảng, bạn có thể ngăn chặn hầu hết các phân mảnh. Sau đó, bạn cần một cách để loại bỏ các chuỗi mà bạn không thấy gần đây khỏi bảng băm để giới hạn kích thước của nó. Nhưng nếu các chuỗi mà đường dẫn mã deserialisation tạo ra chỉ tồn tại trong thời gian ngắn, bạn sẽ không thu được gì nhiều nếu có.


1

Dưới đây là một số cách để xác định nhóm gọi chính xác của LOH phân bổ.

Và để tránh phân mảnh LOH Phân bổ trước một mảng lớn các đối tượng và ghim chúng. Sử dụng lại những đồ vật này khi cần thiết. Đây là bài đăng trên LOH Fragmentation. Một cái gì đó như thế này có thể giúp tránh phân mảnh LOH.


Tôi không thể hiểu tại sao ghim ở đây sẽ giúp ích? BTW các đối tượng lớn trên LOH không bị GC di chuyển. Nó là một chi tiết thực hiện mặc dù.
user492238

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.