Phương pháp hay nhất để tạo hàng triệu đối tượng nhỏ tạm thời


109

"Các phương pháp hay nhất" để tạo (và giải phóng) hàng triệu đối tượng nhỏ là gì?

Tôi đang viết một chương trình cờ vua bằng Java và thuật toán tìm kiếm tạo ra một đối tượng "Di chuyển" duy nhất cho mỗi nước đi có thể có và một tìm kiếm danh nghĩa có thể dễ dàng tạo ra hơn một triệu đối tượng di chuyển mỗi giây. JVM GC đã có thể xử lý tải trên hệ thống phát triển của tôi, nhưng tôi quan tâm đến việc khám phá các phương pháp tiếp cận thay thế sẽ:

  1. Giảm thiểu chi phí thu gom rác và
  2. giảm dung lượng bộ nhớ tối đa cho các hệ thống cấp thấp hơn.

Phần lớn các đối tượng có thời gian tồn tại rất ngắn, nhưng khoảng 1% số lần di chuyển được tạo ra vẫn tồn tại và được trả về dưới dạng giá trị lâu dài, vì vậy bất kỳ kỹ thuật gộp hoặc bộ nhớ đệm nào sẽ phải cung cấp khả năng loại trừ các đối tượng cụ thể được sử dụng lại .

Tôi không mong đợi mã ví dụ đầy đủ, nhưng tôi đánh giá cao các đề xuất để đọc / nghiên cứu thêm hoặc các ví dụ nguồn mở có tính chất tương tự.


11
Mẫu Flyweight có phù hợp với trường hợp của bạn không? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
Bạn có cần phải gói gọn nó trong một đối tượng không?
nhahtdh

1
Mẫu Flyweight không thích hợp, vì các đối tượng không chia sẻ dữ liệu chung quan trọng. Đối với việc đóng gói dữ liệu trong một đối tượng, nó quá lớn để được đóng gói vào một nguyên thủy, đó là lý do tại sao tôi đang tìm kiếm các lựa chọn thay thế cho POJO.
Humble Programmer

2
rkj

Câu trả lời:


47

Chạy ứng dụng với bộ sưu tập rác chi tiết:

java -verbose:gc

Và nó sẽ cho bạn biết khi nào nó thu thập. Sẽ có hai kiểu quét, quét nhanh và quét toàn bộ.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

Mũi tên có kích thước trước và sau.

Miễn là nó chỉ là làm GC và không phải là một GC đầy đủ, bạn đã an toàn ở nhà. GC thông thường là một bộ sưu tập sao chép trong 'thế hệ trẻ', vì vậy các đối tượng không còn được tham chiếu chỉ đơn giản là bị lãng quên, đó là chính xác những gì bạn muốn.

Đọc Điều chỉnh thu gom rác trên máy ảo HotSpot của Java SE 6 có lẽ hữu ích.


Thử nghiệm với kích thước đống Java để cố gắng tìm ra điểm hiếm khi thu thập đầy đủ rác. Trong Java 7, G1 GC mới trong một số trường hợp nhanh hơn (và chậm hơn ở những trường hợp khác).
Michael Shopsin

21

Kể từ phiên bản 6, chế độ máy chủ của JVM sử dụng kỹ thuật phân tích thoát . Sử dụng nó, bạn có thể tránh tất cả GC cùng nhau.


1
Phân tích thoát thường gây thất vọng, nên kiểm tra xem JVM đã tìm ra những gì bạn đang làm hay chưa.
Nitsan Wakart

2
Nếu bạn có kinh nghiệm sử dụng tùy chọn này: -XX: + PrintEscapeAnalysis và -XX: + PrintEliminateAllocations. Điều đó sẽ là tuyệt vời để chia sẻ. Bởi vì tôi không, nói một cách trung thực.
Mikhail

xem stackoverflow.com/questions/9032519/… bạn sẽ cần phải có một bản dựng gỡ lỗi cho JDK 7, tôi thừa nhận là tôi chưa làm được điều đó nhưng với JDK 6 thì nó đã thành công.
Nitsan Wakart

19

Vâng, có một số câu hỏi trong một ở đây!

1 - Các đối tượng tồn tại trong thời gian ngắn được quản lý như thế nào?

Như đã nói trước đây, JVM hoàn toàn có thể đối phó với một lượng lớn vật thể tồn tại trong thời gian ngắn, vì nó tuân theo Giả thuyết Thế hệ Yếu .

Lưu ý rằng chúng ta đang nói về các đối tượng đã đạt đến bộ nhớ chính (heap). Đây không phải là luôn luôn như vậy. Rất nhiều đối tượng bạn tạo thậm chí không để lại thanh ghi CPU. Ví dụ: hãy xem xét vòng lặp for này

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Đừng nghĩ về việc bỏ cuộn vòng lặp (một cách tối ưu mà JVM thực hiện rất nhiều trên mã của bạn). Nếu maxbằng Integer.MAX_VALUE, vòng lặp của bạn có thể mất một khoảng thời gian để thực thi. Tuy nhiên, ibiến sẽ không bao giờ thoát khỏi khối lặp. Do đó, JVM sẽ đưa biến đó vào thanh ghi CPU, thường xuyên tăng nó lên nhưng sẽ không bao giờ gửi biến trở lại bộ nhớ chính.

Vì vậy, việc tạo ra hàng triệu đối tượng không phải là vấn đề lớn nếu chúng chỉ được sử dụng cục bộ. Họ sẽ chết trước khi được cất giữ trong Eden, vì vậy GC thậm chí sẽ không nhận thấy họ.

2 - Nó có hữu ích để giảm chi phí của GC không?

Như thường lệ, nó phụ thuộc.

Đầu tiên, bạn nên bật tính năng ghi nhật ký GC để có cái nhìn rõ ràng về những gì đang diễn ra. Bạn có thể kích hoạt nó với -Xloggc:gc.log -XX:+PrintGCDetails.

Nếu ứng dụng của bạn đang dành nhiều thời gian trong chu kỳ GC, thì, vâng, hãy điều chỉnh GC, nếu không, nó có thể không thực sự đáng giá.

Ví dụ: nếu bạn có một GC trẻ cứ sau 100ms mất 10ms, bạn dành 10% thời gian của mình trong GC và bạn có 10 lần thu thập mỗi giây (đó là huuuuuge). Trong trường hợp như vậy, tôi sẽ không dành bất kỳ thời gian nào để điều chỉnh GC, vì 10 GC / s đó sẽ vẫn ở đó.

3 - Một số kinh nghiệm

Tôi đã gặp sự cố tương tự trên một ứng dụng đang tạo ra một lượng lớn lớp nhất định. Trong nhật ký GC, tôi nhận thấy rằng tốc độ tạo của ứng dụng là khoảng 3 GB / s, quá nhiều (cứ đến ... 3 gigabyte dữ liệu mỗi giây ?!).

Vấn đề: Quá nhiều GC thường xuyên gây ra bởi quá nhiều đối tượng được tạo.

Trong trường hợp của tôi, tôi đã đính kèm một trình biên dịch bộ nhớ và nhận thấy rằng một lớp đại diện cho một tỷ lệ phần trăm lớn tất cả các đối tượng của tôi. Tôi đã theo dõi các khởi tạo để phát hiện ra rằng lớp này về cơ bản là một cặp boolean được bao bọc trong một đối tượng. Trong trường hợp đó, có hai giải pháp:

  • Làm lại thuật toán để tôi không trả về một cặp boolean mà thay vào đó tôi có hai phương thức trả về từng boolean riêng biệt

  • Lưu vào bộ nhớ cache các đối tượng, biết rằng chỉ có 4 trường hợp khác nhau

Tôi đã chọn cái thứ hai, vì nó ít ảnh hưởng nhất đến ứng dụng và dễ giới thiệu. Tôi đã mất vài phút để đặt một nhà máy với bộ nhớ cache không an toàn cho luồng (tôi không cần an toàn luồng vì cuối cùng tôi chỉ có 4 trường hợp khác nhau).

Tỷ lệ phân bổ giảm xuống 1 GB / s, và tần suất GC trẻ (chia cho 3) cũng vậy.

Hy vọng rằng sẽ giúp!


11

Nếu bạn chỉ có các đối tượng giá trị (nghĩa là không có tham chiếu đến các đối tượng khác) và thực sự nhưng ý tôi là thực sự rất nhiều và rất nhiều chúng, bạn có thể sử dụng trực tiếp ByteBuffersvới thứ tự byte gốc [cái sau là quan trọng] và bạn cần vài trăm dòng mã cấp phát / tái sử dụng + getter / setters. Getters trông tương tự nhưlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Điều đó sẽ giải quyết vấn đề GC gần như hoàn toàn miễn là bạn phân bổ một lần duy nhất, tức là, một phần lớn và sau đó tự quản lý các đối tượng. Thay vì các tham chiếu, bạn chỉ có chỉ mục (nghĩa là int) vào tài ByteBufferliệu phải được chuyển đến. Bạn cũng có thể cần phải tự căn chỉnh bộ nhớ.

Kỹ thuật này sẽ giống như sử dụng C and void*, nhưng với một số gói, nó có thể chịu được. Một nhược điểm về hiệu suất có thể là kiểm tra giới hạn nếu trình biên dịch không loại bỏ được nó. Một ưu điểm chính là cục bộ nếu bạn xử lý các bộ giá trị như vectơ, việc thiếu tiêu đề đối tượng cũng làm giảm dung lượng bộ nhớ.

Ngoài ra, có thể bạn sẽ không cần một cách tiếp cận như vậy vì thế hệ trẻ của hầu như tất cả các JVM đều chết một cách đáng kể và chi phí phân bổ chỉ là một mấu chốt. Chi phí phân bổ có thể cao hơn một chút nếu bạn sử dụng finalcác trường vì chúng yêu cầu hàng rào bộ nhớ trên một số nền tảng (cụ thể là ARM / Power), tuy nhiên, trên x86 thì miễn phí.


8

Giả sử bạn thấy GC là một vấn đề (như những người khác chỉ ra nó có thể không phải vậy), bạn sẽ thực hiện quản lý bộ nhớ của riêng mình cho trường hợp đặc biệt, tức là một lớp bị xáo trộn lớn. Hãy thử tính năng tổng hợp đối tượng, tôi đã thấy những trường hợp nó hoạt động khá tốt. Triển khai các nhóm đối tượng là một con đường tốt, vì vậy không cần phải truy cập lại ở đây, hãy chú ý:

  • đa luồng: sử dụng nhóm cục bộ luồng có thể phù hợp với trường hợp của bạn
  • sao lưu cấu trúc dữ liệu: xem xét sử dụng ArrayDeque vì nó hoạt động tốt khi gỡ bỏ và không có chi phí phân bổ
  • giới hạn kích thước hồ bơi của bạn :)

Đo trước / sau, v.v., v.v.


6

Tôi đã gặp một vấn đề tương tự. Trước hết, hãy cố gắng giảm kích thước của các đối tượng nhỏ. Chúng tôi đã giới thiệu một số giá trị trường mặc định tham chiếu đến chúng trong mỗi trường hợp đối tượng.

Ví dụ, MouseEvent có một tham chiếu đến lớp Point. Chúng tôi đã lưu vào bộ nhớ cache các Điểm và tham chiếu chúng thay vì tạo các phiên bản mới. Tương tự đối với các chuỗi rỗng chẳng hạn.

Một nguồn khác là nhiều boolean được thay thế bằng một int và đối với mỗi boolean, chúng tôi chỉ sử dụng một byte của int.


Chỉ vì quan tâm: Nó đã mua cho bạn điều gì về hiệu suất khôn ngoan? Bạn có lập hồ sơ ứng dụng của mình trước và sau khi thay đổi không, và nếu có, kết quả là gì?
Axel

@Axel các đối tượng sử dụng ít bộ nhớ hơn nhiều nên GC không được gọi thường xuyên. Chắc chắn là chúng tôi đã cấu hình ứng dụng của mình nhưng thậm chí còn có hiệu ứng hình ảnh về tốc độ được cải thiện.
StanislavL

6

Tôi đã xử lý tình huống này bằng một số mã xử lý XML một thời gian trước. Tôi thấy mình đã tạo ra hàng triệu đối tượng thẻ XML rất nhỏ (thường chỉ là một chuỗi) và cực kỳ tồn tại trong thời gian ngắn (không kiểm tra được XPath có nghĩa là không khớp nên bị loại bỏ).

Tôi đã thực hiện một số thử nghiệm nghiêm túc và đi đến kết luận rằng tôi chỉ có thể cải thiện khoảng 7% tốc độ bằng cách sử dụng danh sách các thẻ bị loại bỏ thay vì tạo thẻ mới. Tuy nhiên, sau khi triển khai, tôi nhận thấy rằng hàng đợi miễn phí cần một cơ chế được thêm vào để cắt bớt nó nếu nó quá lớn - điều này hoàn toàn vô hiệu hóa tối ưu hóa của tôi, vì vậy tôi đã chuyển nó sang một tùy chọn.

Tóm lại - có lẽ không đáng - nhưng tôi rất vui khi thấy bạn đang nghĩ về nó, điều đó cho thấy bạn quan tâm.


2

Cho rằng bạn đang viết một chương trình cờ vua, có một số kỹ thuật đặc biệt bạn có thể sử dụng để đạt được hiệu suất tốt. Một cách tiếp cận đơn giản là tạo một mảng lớn dài (hoặc byte) và coi nó như một ngăn xếp. Mỗi khi trình tạo chuyển động của bạn tạo ra các chuyển động, nó sẽ đẩy một vài số lên ngăn xếp, ví dụ: chuyển từ ô vuông và chuyển sang ô vuông. Khi bạn đánh giá cây tìm kiếm, bạn sẽ thực hiện các bước di chuyển và cập nhật biểu diễn bảng.

Nếu bạn muốn biểu đạt sức mạnh sử dụng các đối tượng. Nếu bạn muốn tốc độ (trong trường hợp này), hãy chuyển sang bản địa.


1

Một giải pháp mà tôi đã sử dụng cho các thuật toán tìm kiếm như vậy là chỉ tạo một đối tượng Move, thay đổi nó bằng bước di chuyển mới, sau đó hoàn tác việc di chuyển trước khi rời khỏi phạm vi. Bạn có thể chỉ đang phân tích một nước đi tại một thời điểm, và sau đó chỉ cần lưu trữ nước đi tốt nhất ở đâu đó.

Nếu điều đó không khả thi vì một lý do nào đó và bạn muốn giảm mức sử dụng bộ nhớ tối đa, thì có một bài viết hay về hiệu quả bộ nhớ tại đây: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-enough-java- tutorial.pdf


Liên kết chết. Có nguồn nào khác cho bài báo đó không?
dnault

0

Chỉ cần tạo hàng triệu đối tượng của bạn và viết mã của bạn theo cách thích hợp: không giữ các tham chiếu không cần thiết đến các đối tượng này. GC sẽ làm công việc bẩn thỉu cho bạn. Bạn có thể thử với GC chi tiết như đã đề cập để xem liệu họ có thực sự là GC'd hay không. Java IS về việc tạo và giải phóng các đối tượng. :)


1
Xin lỗi anh bạn, tôi không đồng ý với cách tiếp cận của bạn ... Java, giống như bất kỳ ngôn ngữ lập trình nào, là giải quyết một vấn đề trong những ràng buộc của nó, nếu OP bị hạn chế bởi GC, bạn sẽ giúp anh ta như thế nào?
Nitsan Wakart

1
Tôi đang nói với anh ấy cách Java thực sự hoạt động. Nếu anh ta không thể tránh khỏi tình huống có hàng triệu đối tượng tạm thời, lời khuyên tốt nhất có thể là, lớp tạm thời nên nhẹ và anh ta phải đảm bảo rằng anh ta phát hành các tham chiếu càng sớm càng tốt, không phải là một bước nữa. Tui bỏ lỡ điều gì vậy?
gyorgyabraham

Java hỗ trợ tạo rác và sẽ dọn dẹp nó cho bạn, điều đó đúng. Nếu OP không thể né tránh việc tạo ra các đối tượng, và anh ta không hài lòng với thời gian ở GC thì đó là một kết cục đáng buồn. Sự phản đối của tôi là đề xuất bạn đưa ra để tạo ra nhiều công việc hơn cho GC vì đó là một Java phù hợp.
Nitsan Wakart

0

Tôi nghĩ bạn nên đọc về phân bổ ngăn xếp trong Java và phân tích thoát.

Bởi vì nếu bạn đi sâu hơn vào chủ đề này, bạn có thể thấy rằng các đối tượng của bạn thậm chí không được phân bổ trên heap và chúng không được GC thu thập như cách các đối tượng trên heap.

Có một giải thích trên wikipedia về phân tích thoát, với ví dụ về cách hoạt động của nó trong Java:

http://en.wikipedia.org/wiki/Escape_analysis


0

Tôi không phải là một fan hâm mộ lớn của GC, vì vậy tôi luôn cố gắng tìm mọi cách để giải quyết vấn đề đó. Trong trường hợp này, tôi khuyên bạn nên sử dụng mẫu Object Pool :

Ý tưởng là tránh tạo các đối tượng mới bằng cách lưu trữ chúng trong một ngăn xếp để bạn có thể sử dụng lại sau này.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
Sử dụng pool cho các đối tượng nhỏ là một ý tưởng khá tồi, bạn cần một pool trên mỗi luồng để khởi động (hoặc quyền truy cập được chia sẻ sẽ giết chết bất kỳ hiệu suất nào). Các hồ bơi như vậy cũng hoạt động kém hơn một bộ thu gom rác tốt. Cuối cùng: GC là ơn trời khi xử lý mã / cấu trúc đồng thời - nhiều thuật toán dễ thực hiện hơn đáng kể vì tự nhiên không có vấn đề gì về ABA. Tham khảo đếm trong môi trường đồng thời yêu cầu ít nhất một hoạt động nguyên tử + hàng rào bộ nhớ (LOCK ADD hoặc CAS trên x86)
bestsss

1
Việc quản lý các đối tượng trong các hồ bơi có thể nhiều tốn kém hơn để cho thời gian thu gom rác thải.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen Nói chung tôi đồng ý với bạn, nhưng lưu ý rằng việc phát hiện ra sự khác biệt đó là một thách thức khá lớn và khi bạn đi đến kết luận rằng GC hoạt động tốt hơn trong trường hợp của bạn, đó phải là một trường hợp rất độc đáo nếu sự khác biệt đó quan trọng. Nhưng ngược lại, có thể là Object pool sẽ lưu ứng dụng của bạn.
Ilya Gazman

1
Tôi chỉ đơn giản là không hiểu lập luận của bạn? Rất khó phát hiện GC có nhanh hơn đối tượng gộp chung không? Và do đó bạn nên sử dụng đối tượng gộp? JVM được tối ưu hóa cho mã hóa sạch và các đối tượng có tuổi thọ ngắn. Nếu đó là những gì câu hỏi này là về (mà tôi hy vọng nếu OP tạo ra một triệu trong số chúng pr giây) thì nó chỉ nên xảy ra nếu có một lợi thế có thể chứng minh để chuyển sang một sơ đồ phức tạp hơn và dễ xảy ra lỗi như bạn đề xuất. Nếu điều này là quá khó để chứng minh, thì tại sao phải bận tâm.
Thorbjørn Ravn Andersen

0

Nhóm đối tượng cung cấp những cải tiến to lớn (đôi khi 10 lần) đối với việc phân bổ đối tượng trên đống. Nhưng việc triển khai ở trên bằng cách sử dụng danh sách liên kết vừa ngây thơ vừa sai lầm! Danh sách liên kết tạo ra các đối tượng để quản lý cấu trúc bên trong của nó, vô hiệu hóa nỗ lực. Bộ đệm chuông sử dụng một mảng đối tượng hoạt động tốt. Trong ví dụ đưa ra (một chương trình cờ vua quản lý các nước đi), Ringbuffer nên được bao bọc thành một đối tượng giữ cho danh sách tất cả các nước đi được tính toán. Sau đó, chỉ các tham chiếu đối tượng chủ động mới được chuyển xung quanh.

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.