2012-03-06 32 views
9

Tôi đang cố gắng đọc một văn bản lớn văn bản vào bộ nhớ bằng Java. Tại một số điểm nó chạm vào một bức tường và chỉ rác thu thập liên tục. Tôi muốn biết nếu có ai có kinh nghiệm đánh bại GC của Java để gửi với các tập dữ liệu lớn.Hiệu suất kém với danh sách Java lớn

Tôi đang đọc một tệp 8 GB văn bản tiếng Anh, bằng UTF-8, với một câu trong một dòng. Tôi muốn split() mỗi dòng trên khoảng trắng và lưu trữ các mảng Chuỗi kết quả trong một ArrayList<String[]> để xử lý thêm. Đây là một chương trình được đơn giản hóa thể hiện sự cố:

/** Load whitespace-delimited tokens from stdin into memory. */ 
public class LoadTokens { 
    private static final int INITIAL_SENTENCES = 66000000; 

    public static void main(String[] args) throws IOException { 
     List<String[]> sentences = new ArrayList<String[]>(INITIAL_SENTENCES); 
     BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 
     long numTokens = 0; 
     String line; 

     while ((line = stdin.readLine()) != null) { 
      String[] sentence = line.split("\\s+"); 
      if (sentence.length > 0) { 
       sentences.add(sentence); 
       numTokens += sentence.length; 
      } 
     } 
     System.out.println("Read " + sentences.size() + " sentences, " + numTokens + " tokens."); 
    } 
} 

Dường như đã cắt và phơi khô phải không? Bạn sẽ nhận thấy tôi thậm chí trước khi kích thước của tôi ArrayList; Tôi có ít hơn 66 triệu câu và 1,3 tỷ thẻ. Bây giờ nếu bạn whip ra Java object sizes tham chiếu và bút chì của bạn, bạn sẽ thấy rằng nên yêu cầu về:

  • 66e6 tài liệu tham khảo String[] @ 8 byte ea = 0,5 GB
  • 66e6 String[] đối tượng @ 32 byte ea = 2 GB
  • 66e6 char[] đối tượng @ 32 byte ea = 2 GB
  • 1.3e9 String tài liệu tham khảo @ 8 byte ea = 10 GB
  • 1.3e9 String s @ 44 byte ea = 53 GB
  • 8e9 char s @ 2 byte ea = 15 GB

83 GB. (Bạn sẽ nhận thấy tôi thực sự cần phải sử dụng kích thước đối tượng 64-bit, vì Compressed OOPs không thể giúp tôi với> 32 GB đống.) Chúng tôi may mắn có một máy RedHat 6 với RAM 128 GB, vì vậy tôi khởi động Máy chủ Java 64 bit của tôi (TM) 64 bit (xây dựng 20.4-b02, chế độ hỗn hợp) từ bộ Java SE 1.6.0_29 của tôi với pv giant-file.txt | java -Xmx96G -Xms96G LoadTokens chỉ để an toàn và quay lại trong khi tôi xem top.

Một nơi nào đó ít hơn một nửa thông qua đầu vào, vào khoảng 50-60 GB RSS, bộ thu gom rác song song khởi động lên tới 1300% CPU (hộp 16 proc) và tiến trình đọc dừng. Sau đó, nó đi thêm một vài GB, sau đó tiến trình dừng lại lâu hơn nữa. Nó lấp đầy 96 GB và chưa hoàn thành. Tôi đã để cho nó đi trong một giờ rưỡi, và nó chỉ cháy ~ 90% thời gian hệ thống làm GC. Điều đó có vẻ cực đoan.

Để đảm bảo rằng tôi không bị điên, tôi đã đánh lừa Python tương đương (tất cả hai dòng;) và nó chạy để hoàn thành trong khoảng 12 phút và 70 GB RSS.

Vì vậy: tôi đang làm điều gì đó ngu ngốc? (Ngoài những thứ không hiệu quả, mọi thứ đang được lưu trữ, mà tôi thực sự không thể giúp - và thậm chí nếu cấu trúc dữ liệu của tôi có chất béo, miễn là chúng phù hợp, Java không nên chỉ nghẹt thở.) GC tư vấn cho đống thực sự lớn? Tôi đã thử -XX:+UseParNewGC và có vẻ như còn tồi tệ hơn.

+0

Đối tượng 'char []' sao lưu các chuỗi? –

+0

Trong các đối tượng 'String': tiêu đề đối tượng 24 byte + 8 byte' char [] 'con trỏ + bắt đầu 4 byte, bù đắp và hashcode, nếu tính toán của tôi là chính xác. –

+0

Đó là tham chiếu 'char []' * * - nhưng về bản thân 'char []' * thì sao? Một mảng 'char []' có một đối tượng trên không ... –

Trả lời

3

-XX:+UseConcMarkSweepGC: hoàn thành trong 78 GB và ~ 12 phút. (Hầu như tốt bằng Python!) Cảm ơn sự giúp đỡ của mọi người.

+0

Tôi thường sử dụng CMS cho máy chủ java với đống lớn để giảm tác động của gc đến thời gian phản hồi. Tôi không tin rằng việc thay đổi chính sách sẽ giúp mã của bạn trong một nhiệm vụ như vậy. Tôi đoán bằng cách sử dụng CMS đã thay đổi cách đống được chia thành các phần và JVM của bạn nhận được một OldGen lớn hơn. –

2

Idea 1

Bắt đầu bằng cách xem xét này:

while ((line = stdin.readLine()) != null) { 

Nó ít nhất sử dụng phải là trường hợp mà readLine sẽ trả về một String với sự ủng hộ char[] ít nhất 80 ký tự.Cho dù đó sẽ trở thành một vấn đề hay không phụ thuộc vào những gì các dòng tiếp theo không:

String[] sentence = line.split("\\s+"); 

Bạn nên xác định xem chuỗi trả về bởi split giữ cùng sự ủng hộ char[].

Nếu họ làm (và giả sử đường dây của bạn thường ngắn hơn 80 ký tự), bạn nên sử dụng:

line = new String(line); 

này sẽ tạo ra một bản sao của bản sao của chuỗi với một mảng chuỗi "đúng kích cỡ"

Nếu họ không, sau đó bạn nên có khả năng làm việc ra một số cách để tạo ra những hành vi tương tự nhưng thay đổi nó để họ làm sử dụng cùng sự ủng hộ char[] (tức là họ đang chuỗi con của dòng gốc) - và thực hiện thao tác nhân bản giống nhau ation, tất nhiên rồi. Bạn không muốn char[] riêng biệt cho mỗi từ, vì điều đó sẽ lãng phí nhiều bộ nhớ hơn so với không gian.

Idea 2

cuộc đàm phán Tiêu đề của bạn về sự yếu kém của danh sách - nhưng tất nhiên bạn có thể dễ dàng đưa danh sách ra khỏi phương trình ở đây bằng cách đơn giản tạo ra một String[][], ít nhất là cho mục đích thử nghiệm. Có vẻ như bạn đã biết kích thước của tệp - và nếu không, bạn có thể chạy nó qua số wc để kiểm tra trước. Chỉ để xem bạn có thể tránh được sự cố đó để bắt đầu với hay không.

Idea 3

bao nhiêu biệt lời là có ở corpus của bạn? Bạn đã cân nhắc việc giữ một số HashSet<String> và thêm mỗi từ vào nó khi bạn bắt gặp nó? Bằng cách đó bạn có thể kết thúc với chuỗi xa ít. Tại thời điểm này, bạn có lẽ sẽ muốn từ bỏ "sự ủng hộ đơn char[] mỗi dòng" từ ý tưởng đầu tiên - bạn muốn muốn mỗi chuỗi được hỗ trợ bởi mảng char riêng của mình, vì nếu không một dòng với một từ mới duy nhất trong là vẫn sẽ đòi hỏi rất nhiều nhân vật. (Ngoài ra, đối với thực tinh chỉnh, bạn có thể xem có bao nhiêu "từ mới" có trong một dòng và sao chép mỗi chuỗi hay không.)

+0

Re: Idea 3, bạn có thể xem xét sử dụng 'String.intern()'? –

+0

@LouisWasserman: Có khả năng - nhưng chỉ khi quá trình này sẽ không làm bất cứ điều gì khác. Tôi thường thích có bộ thực tập của riêng mình, để tránh "gây ô nhiễm" cho toàn bộ quy trình. (Mặc dù có thể có những điều thú vị để có nghĩa là đó không phải là một vấn đề những ngày này. Nó chỉ * cảm thấy * sạch hơn.) –

+2

Hmmm. Đề xuất thay thế - ['Interners.newWeakInterner'] của Guava (http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Interners.html#newWeakInterner()) để làm điều đó với tài liệu tham khảo yếu, chỉ để các chuỗi interned có thể nhận được GC'd khi bạn đang thực hiện. –

2

Bạn nên sử dụng các thủ thuật sau đây:

  • Trợ giúp JVM để thu thập cùng một mã thông báo vào một tham chiếu String đơn lẻ nhờ sentences.add(sentence.intern()). Xem String.intern để biết chi tiết. Theo như tôi biết, nó cũng nên có hiệu ứng Jon Skeet nói về, nó cắt mảng char thành từng miếng nhỏ.

  • Sử dụng experimental HotSpot options để String nhỏ gọn và char [] triển khai và những người liên quan:

    -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat 
    

Với lượng bộ nhớ như vậy, bạn nên cấu hình hệ thống và JVM của bạn để use large pages.

Đó là thực sự khó khăn để cải thiện hiệu suất với điều chỉnh GC mình và hơn 5%.Trước tiên, bạn nên giảm mức tiêu thụ bộ nhớ ứng dụng của mình nhờ vào hồ sơ.

Nhân tiện, tôi tự hỏi nếu bạn thực sự cần có nội dung đầy đủ của một cuốn sách trong bộ nhớ - Tôi không biết mã của bạn làm gì tiếp theo với tất cả các câu nhưng bạn nên xem xét một tùy chọn thay thế như Lucene indexing tool để đếm từ hoặc trích xuất bất kỳ thông tin nào khác từ văn bản của bạn.

+0

Cảm ơn các đề xuất. Tôi đã thử thực hiện chuỗi trong các ứng dụng trước đó; nó rất chậm với rất nhiều dữ liệu, và nó đòi hỏi một PermGen rất lớn, điều này thực sự gây nhầm lẫn cho GC. Tôi đã thử các tùy chọn tối ưu hóa String của bạn, và nó có thể đã giảm sử dụng bộ nhớ một chút, nhưng nó vẫn dần dần lấp đầy bộ nhớ và borks. Ý tưởng trang lớn là ý tưởng hay; Thật không may, bạn thực sự phải khởi động lại để có đủ bộ nhớ miễn phí liền kề (đây là gì, DOS?;), và bộ nhớ đó không thể được sử dụng cho bất cứ điều gì khác. Tôi đang đọc trên GC chỉnh, và tôi nghĩ rằng tôi sẽ cố gắng thu gom đồng thời tiếp theo. –

0

Bạn nên kiểm tra cách thức không gian heap của bạn được chia thành nhiều phần (PermGen, OldGen, Eden và Survivors) nhờ VisualGC hiện là plugin cho VisualVM.

Trong trường hợp của bạn, bạn có thể muốn giảm bớt Eden và người sống sót để tăng OldGen để GC của bạn không quay vào thu thập một OldGen đầy đủ ...

Để làm như vậy, bạn phải sử dụng tùy chọn nâng cao như :

-XX:NewRatio=2 -XX:SurvivorRatio=8 

Hãy coi chừng những vùng này và chính sách phân bổ mặc định tùy thuộc vào bộ thu bạn sử dụng. Vì vậy, thay đổi một tham số tại một thời điểm và kiểm tra lại. Nếu tất cả các chuỗi đó phải sống trong bộ nhớ tất cả thời gian sống của JVM, thì tốt nhất là nội bộ hóa chúng trong PermGen được xác định đủ lớn với -XX:MaxPermSize và để tránh thu thập trên vùng đó nhờ -Xnoclassgc.

Tôi khuyên bạn nên bật các tùy chọn gỡ lỗi này (không có phí dự kiến) và cuối cùng đăng nhật ký gc để chúng tôi có thể có ý tưởng về hoạt động GC của bạn.

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log 
+0

Tôi đã xem xét điều này và tôi có thể thử. Cám ơn vì sự gợi ý. –

Các vấn đề liên quan