Làm cách nào để tạo rò rỉ bộ nhớ trong Java?


3223

Tôi vừa có một cuộc phỏng vấn và tôi được yêu cầu tạo rò rỉ bộ nhớ với Java.
Không cần phải nói, tôi cảm thấy khá ngu ngốc khi không có manh mối về cách thậm chí bắt đầu tạo một cái.

Một ví dụ sẽ là gì?


275
Tôi sẽ nói với họ rằng Java sử dụng một bộ thu rác, và yêu cầu họ phải được cụ thể thêm một chút về định nghĩa của họ về "bộ nhớ bị rò rỉ", giải thích rằng - chặn lỗi JVM - Java không thể rò rỉ bộ nhớ trong khá giống C / C ++ có thể. Bạn phải có một tài liệu tham khảo đến đối tượng ở đâu đó .
Darien

371
Tôi thấy buồn cười khi trên hầu hết các câu trả lời, mọi người đang tìm kiếm những trường hợp và mánh khóe đó và dường như hoàn toàn thiếu quan điểm (IMO). Họ chỉ có thể hiển thị mã giữ các tham chiếu vô dụng đến các đối tượng sẽ không bao giờ sử dụng lại, đồng thời không bao giờ bỏ các tham chiếu đó; người ta có thể nói rằng những trường hợp đó không phải là rò rỉ bộ nhớ "thật" bởi vì vẫn có các tham chiếu đến các đối tượng xung quanh, nhưng nếu chương trình không bao giờ sử dụng các tham chiếu đó nữa và cũng không bao giờ bỏ chúng, thì nó hoàn toàn tương đương với (và tệ như) a " rò rỉ bộ nhớ thật ".
ehabkost

62
Thành thật mà nói tôi không thể tin được câu hỏi tương tự mà tôi đã hỏi về "Đi" đã bị hạ xuống -1. Ở đây: stackoverflow.com/questions/4400311/ về cơ bản các rò rỉ bộ nhớ mà tôi đang nói đến là những người đã nhận được hơn 200 lượt nâng cấp lên OP và tôi đã bị tấn công và bị xúc phạm khi hỏi liệu "Go" có gặp vấn đề tương tự không. Bằng cách nào đó tôi không chắc chắn rằng tất cả các trang wiki đang hoạt động tuyệt vời như vậy.
Cú phápT3rr0r

74
@ SyntaxT3rr0r - Câu trả lời của darien không phải là sự cuồng tín. ông thừa nhận rõ ràng rằng một số JVM nhất định có thể có lỗi có nghĩa là bộ nhớ bị rò rỉ. điều này khác với thông số ngôn ngữ cho phép rò rỉ bộ nhớ.
Peter Recore

31
@ehabkost: Không, chúng không tương đương. (1) Bạn có khả năng lấy lại bộ nhớ, trong khi trong "rò rỉ thực sự", chương trình C / C ++ của bạn quên đi phạm vi được phân bổ, không có cách nào an toàn để khôi phục. (2) Bạn có thể rất dễ dàng phát hiện vấn đề với hồ sơ, bởi vì bạn có thể thấy những đối tượng mà "sự phình to" liên quan. (3) "Rò rỉ thực sự" là một lỗi không rõ ràng, trong khi một chương trình giữ nhiều đối tượng xung quanh cho đến khi chấm dứt có thể là một phần có chủ ý trong cách hoạt động của nó.
Darien

Câu trả lời:


2309

Đây là một cách tốt để tạo rò rỉ bộ nhớ thực (các đối tượng không thể truy cập bằng cách chạy mã nhưng vẫn được lưu trong bộ nhớ) trong Java thuần túy:

  1. Ứng dụng tạo ra một luồng chạy dài (hoặc sử dụng nhóm luồng để rò rỉ nhanh hơn nữa).
  2. Các luồng tải một lớp thông qua một (tùy chọn tùy chọn) ClassLoader.
  3. Lớp phân bổ một khối lớn bộ nhớ (ví dụ new byte[1000000] ), lưu trữ một tham chiếu mạnh đến nó trong một trường tĩnh và sau đó lưu trữ một tham chiếu đến chính nó trong a ThreadLocal. Phân bổ bộ nhớ thêm là tùy chọn (rò rỉ thể hiện lớp là đủ), nhưng nó sẽ làm cho việc rò rỉ hoạt động nhanh hơn nhiều.
  4. Ứng dụng xóa tất cả các tham chiếu đến lớp tùy chỉnh hoặc ClassLoader nó đã được tải từ đó.
  5. Nói lại.

Do cách ThreadLocal được triển khai trong JDK của Oracle, điều này tạo ra rò rỉ bộ nhớ:

  • Mỗi Threadlĩnh vực có một lĩnh vực riêngthreadLocals , thực sự lưu trữ các giá trị luồng-cục bộ.
  • Mỗi khóa trong bản đồ này là một tham chiếu yếu đến một ThreadLocalđối tượng, vì vậy sau đóThreadLocal đối tượng được thu gom rác, mục nhập của nó sẽ bị xóa khỏi bản đồ.
  • Nhưng mỗi giá trị là một tham chiếu mạnh, vì vậy khi một giá trị (trực tiếp hoặc gián tiếp) trỏ đến ThreadLocalđối tượng là khóa của nó , đối tượng đó sẽ không bị thu gom rác cũng như bị xóa khỏi bản đồ miễn là luồng tồn tại.

Trong ví dụ này, chuỗi các tài liệu tham khảo mạnh mẽ trông như thế này:

Threadđối tượng → threadLocalsánh xạ → thể hiện của lớp ví dụ → lớp ví dụ → ThreadLocaltrường tĩnh →ThreadLocal đối tượng.

(Việc ClassLoadernày không thực sự đóng vai trò tạo ra rò rỉ, nó chỉ làm cho rò rỉ trở nên tồi tệ hơn do chuỗi tham chiếu bổ sung này: lớp ví dụ → ClassLoader→ tất cả các lớp mà nó đã tải. Nó thậm chí còn tồi tệ hơn trong nhiều triển khai JVM, đặc biệt là trước Java 7, vì các lớp và ClassLoaders được phân bổ thẳng vào permgen và không bao giờ được thu gom rác.)

Một biến thể trên mẫu này là lý do tại sao các thùng chứa ứng dụng (như Tomcat) có thể rò rỉ bộ nhớ như một cái sàng nếu bạn thường xuyên triển khai các ứng dụng xảy ra để sử dụng ThreadLocal chúng theo cách nào đó quay trở lại chính chúng. Điều này có thể xảy ra vì một số lý do tinh tế và thường khó gỡ lỗi và / hoặc sửa chữa.

Cập nhật : Vì nhiều người tiếp tục yêu cầu, đây là một số mã ví dụ cho thấy hành vi này đang hoạt động .


186
Rò rỉ +1 ClassLoader là một số rò rỉ bộ nhớ đau đớn nhất trong thế giới JEE, thường được gây ra bởi các lib bên thứ 3 làm biến đổi dữ liệu (BeanUtils, codec XML / JSON). Điều này có thể xảy ra khi lib được tải bên ngoài trình nạp lớp gốc của ứng dụng của bạn nhưng chứa các tham chiếu đến các lớp của bạn (ví dụ: bằng cách lưu vào bộ đệm). Khi bạn không triển khai / triển khai lại ứng dụng của mình, JVM không thể dọn rác thu thập trình nạp lớp của ứng dụng (và do đó tất cả các lớp được tải bởi nó), do đó, việc lặp lại sẽ triển khai máy chủ ứng dụng cuối cùng. Nếu may mắn, bạn có được manh mối với ClassCastException zxyAbc không thể được chuyển thành zxyAbc
Earcam

7
tomcat sử dụng các thủ thuật và nils TẤT CẢ các biến tĩnh trong TẤT CẢ các lớp được tải, tomcat có rất nhiều cơ sở dữ liệu và mã hóa xấu (cần phải có thời gian và gửi các bản sửa lỗi), cộng với tất cả các concuxLinkedQueue gây chú ý như bộ đệm cho các đối tượng bên trong (nhỏ), nhỏ đến mức ngay cả ConcurrencyLinkedQueue.Node chiếm nhiều bộ nhớ hơn.
bestsss

57
+1: Rò rỉ classloader là một cơn ác mộng. Tôi đã dành nhiều tuần cố gắng để tìm ra chúng. Điều đáng buồn là, như những gì @earcam đã nói, chúng chủ yếu được gây ra bởi libs của bên thứ 3 và hầu hết các trình duyệt không thể phát hiện ra những rò rỉ này. Có một lời giải thích tốt và rõ ràng trên blog này về rò rỉ Classloader. blog.oracle.com/fkieviet/entry/ từ
Adrian M

4
@Nicolas: Bạn có chắc không? Theo mặc định, JRockit thực hiện các đối tượng Lớp GC và HotSpot thì không, nhưng AFAIK JRockit vẫn không thể GC một Class hoặc ClassLoader được tham chiếu bởi ThreadLocal.
Daniel Pryden

6
Tomcat sẽ cố gắng phát hiện những rò rỉ này cho bạn và cảnh báo về chúng: wiki.apache.org/tomcat/MemoryLeakProtection . Phiên bản gần đây nhất đôi khi thậm chí sẽ sửa lỗi rò rỉ cho bạn.
Matthijs Bierman

1212

Tham chiếu đối tượng giữ trường tĩnh [trường cuối cùng]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Gọi String.intern()trên chuỗi dài

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(Không được tiết lộ) các luồng mở (tệp, mạng, v.v.)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Kết nối không được tiết lộ

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Các khu vực không thể truy cập được từ trình thu gom rác của JVM , chẳng hạn như bộ nhớ được phân bổ thông qua các phương thức gốc

Trong các ứng dụng web, một số đối tượng được lưu trữ trong phạm vi ứng dụng cho đến khi ứng dụng bị dừng hoặc xóa rõ ràng.

getServletContext().setAttribute("SOME_MAP", map);

Các tùy chọn JVM không chính xác hoặc không phù hợp , chẳng hạn như noclassgctùy chọn trên IBM JDK ngăn chặn bộ sưu tập rác lớp không sử dụng

Xem cài đặt jdk của IBM .


178
Tôi không đồng ý rằng các thuộc tính bối cảnh và phiên là "rò rỉ". Chúng chỉ là những biến số tồn tại lâu dài. Và trường cuối cùng tĩnh ít nhiều chỉ là một hằng số. Có lẽ nên tránh các hằng số lớn, nhưng tôi không nghĩ là công bằng khi gọi nó là rò rỉ bộ nhớ.
Ian McLaird

80
(Không được tiết lộ) các luồng mở (tệp, mạng, v.v.) , không bị rò rỉ thực sự, trong quá trình hoàn thiện (sẽ diễn ra sau chu trình GC tiếp theo), close () sẽ được lên lịch ( close()thường không được gọi trong bộ hoàn thiện chủ đề vì có thể là một hoạt động chặn). Đó là một thực tế xấu không đóng cửa, nhưng nó không gây rò rỉ. Không được tiết lộ java.sql. Kết nối là như nhau.
bestsss

33
Trong hầu hết các JVM lành mạnh, nó xuất hiện như thể lớp String chỉ có một tham chiếu yếu về internnội dung có thể băm của nó . Như vậy, nó rác được thu gom đúng cách và không bị rò rỉ. (nhưng IANAJP) mindprod.com/jgloss/iterned.html#GC
Matt B.

42
Làm thế nào tham chiếu đối tượng giữ trường tĩnh [trường cuối cùng] là rò rỉ bộ nhớ ??
Kanagavelu Sugumar

5
@cHao Đúng. Nguy hiểm tôi gặp phải không phải là vấn đề từ bộ nhớ bị rò rỉ bởi Streams. Vấn đề là không đủ bộ nhớ bị rò rỉ bởi chúng. Bạn có thể rò rỉ rất nhiều tay cầm, nhưng vẫn có nhiều bộ nhớ. Bộ sưu tập rác sau đó có thể quyết định không bận tâm thực hiện một bộ sưu tập đầy đủ vì nó vẫn còn nhiều bộ nhớ. Điều này có nghĩa là trình hoàn thiện không được gọi, vì vậy bạn hết xử lý. Vấn đề là, các bộ hoàn thiện sẽ (thường) sẽ được chạy trước khi bạn hết bộ nhớ do rò rỉ các luồng, nhưng nó có thể không được gọi trước khi bạn hết thứ gì khác ngoài bộ nhớ.
Patrick M

460

Một điều đơn giản cần làm là sử dụng Hashset với một lỗi không chính xác (hoặc không tồn tại) hashCode()hoặc equals()sau đó tiếp tục thêm "bản sao". Thay vì bỏ qua các bản sao như mong muốn, bộ sẽ chỉ phát triển và bạn sẽ không thể xóa chúng.

Nếu bạn muốn các khóa / phần tử xấu này treo xung quanh, bạn có thể sử dụng trường tĩnh như

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

68
Trên thực tế, bạn có thể xóa các phần tử khỏi Hashset ngay cả khi lớp phần tử bị hashCode và sai; chỉ cần lấy một iterator cho tập hợp và sử dụng phương thức remove của nó, vì iterator thực sự hoạt động trên chính các mục bên dưới chứ không phải các phần tử. (Lưu ý rằng một hashCode chưa thực hiện / bằng là không đủ để kích hoạt một sự rò rỉ; giá trị mặc định thực hiện nhận dạng đối tượng đơn giản và do đó bạn có thể nhận được các yếu tố và loại bỏ chúng bình thường.)
Donal Fellows

12
@ Tôi nói những gì tôi đang cố nói, tôi đoán, là tôi không đồng ý với định nghĩa của bạn về rò rỉ bộ nhớ. Tôi sẽ xem xét (để tiếp tục tương tự) kỹ thuật loại bỏ vòng lặp của bạn là một cái nhỏ giọt dưới sự rò rỉ; rò rỉ vẫn tồn tại bất kể chảo nhỏ giọt.
corsiKa

94
Tôi đồng ý, đây không phải là "rò rỉ" bộ nhớ, bởi vì bạn chỉ có thể xóa các tham chiếu đến hàm băm và chờ đợi GC khởi động, và thế là xong! ký ức quay trở lại.
dùng541686

12
@ SyntaxT3rr0r, tôi đã giải thích câu hỏi của bạn là hỏi liệu có bất cứ điều gì trong ngôn ngữ tự nhiên dẫn đến rò rỉ bộ nhớ. Câu trả lời là không. Câu hỏi này hỏi liệu có thể tạo ra một tình huống để tạo ra thứ gì đó giống như rò rỉ bộ nhớ. Không có ví dụ nào trong số này là rò rỉ bộ nhớ theo cách mà một lập trình viên C / C ++ sẽ hiểu nó.
Peter Lawrey

11
@Peter Lawrey: bạn cũng nghĩ gì về điều này: "Không có bất cứ thứ gì trong ngôn ngữ C tự nhiên bị rò rỉ bộ nhớ nếu bạn không quên giải phóng bộ nhớ theo cách thủ công" . Làm thế nào mà cho sự không trung thực trí tuệ? Dù sao, tôi mệt mỏi: bạn có thể có từ cuối cùng.
Cú phápT3rr0r

271

Dưới đây sẽ có một trường hợp không rõ ràng khi rò rỉ Java, bên cạnh trường hợp tiêu chuẩn của người nghe bị lãng quên, tài liệu tham khảo tĩnh, khóa không có thật / khóa có thể sửa đổi trong hashmap hoặc chỉ các luồng bị kẹt mà không có cơ hội kết thúc vòng đời của chúng.

  • File.deleteOnExit() - luôn rò rỉ chuỗi, nếu chuỗi là một chuỗi con, rò rỉ thậm chí còn tệ hơn (char [] bên dưới cũng bị rò rỉ)- trong chuỗi con Java 7 cũng sao chép char[], vì vậy, sau này không áp dụng ; @Daniel, mặc dù không cần bình chọn.

Tôi sẽ tập trung vào các chủ đề để thể hiện sự nguy hiểm của các chủ đề không được quản lý, hầu như không muốn chạm vào swing.

  • Runtime.addShutdownHookvà không xóa ... và sau đó ngay cả với removeShutdownHook do một lỗi trong lớp Threadgroup liên quan đến các luồng chưa được khởi động, nó có thể không được thu thập, rò rỉ Threadgroup một cách hiệu quả. Jgroup có rò rỉ trong G RumRouter.

  • Tạo, nhưng không bắt đầu, a Threadđi vào cùng loại như trên.

  • Việc tạo một luồng kế thừa ContextClassLoaderAccessControlContext, cộng với ThreadGroupvà bất kỳ InheritedThreadLocal, tất cả các tham chiếu đó là các rò rỉ tiềm năng, cùng với toàn bộ các lớp được nạp bởi trình nạp lớp và tất cả các tham chiếu tĩnh và ja-ja. Hiệu ứng này đặc biệt rõ ràng với toàn bộ khung jucExecutor có ThreadFactorygiao diện siêu đơn giản , tuy nhiên hầu hết các nhà phát triển không có manh mối về mối nguy hiểm đang rình rập. Ngoài ra rất nhiều thư viện bắt đầu các chủ đề theo yêu cầu (cách quá nhiều thư viện phổ biến trong ngành).

  • ThreadLocalbộ nhớ cache; Đó là những điều xấu xa trong nhiều trường hợp. Tôi chắc chắn rằng tất cả mọi người đã nhìn thấy khá nhiều bộ nhớ cache đơn giản dựa trên ThreadLocal, cũng là tin xấu: nếu luồng tiếp tục diễn ra nhiều hơn mong đợi của ClassLoader, thì đó là một rò rỉ nhỏ. Không sử dụng bộ đệm ThreadLocal trừ khi thực sự cần thiết.

  • Gọi ThreadGroup.destroy()khi Threadgroup không có chủ đề, nhưng nó vẫn giữ Threadgroup con. Một rò rỉ xấu sẽ ngăn chặn Threadgroup loại bỏ khỏi cha mẹ của nó, nhưng tất cả các con trở nên không thể đếm được.

  • Sử dụng WeakHashMap và giá trị (in) trực tiếp tham chiếu khóa. Đây là một thứ khó tìm mà không có một đống. Điều đó áp dụng cho tất cả các phần mở rộng Weak/SoftReferencecó thể giữ một tham chiếu cứng trở lại đối tượng được bảo vệ.

  • Sử dụng java.net.URLvới giao thức HTTP (S) và tải tài nguyên từ (!). Điều này là đặc biệt, việc KeepAliveCachetạo một luồng mới trong hệ thống Threadgroup hệ thống rò rỉ trình nạp lớp ngữ cảnh của luồng hiện tại. Chuỗi được tạo theo yêu cầu đầu tiên khi không có luồng sống, vì vậy bạn có thể gặp may mắn hoặc chỉ bị rò rỉ. Rò rỉ đã được sửa trong Java 7 và mã tạo luồng xử lý loại bỏ đúng trình nạp lớp ngữ cảnh. Có vài trường hợp nữa (như ImageFetcher, cũng đã sửa ) tạo chủ đề tương tự.

  • Sử dụng InflaterInputStreamchuyển new java.util.zip.Inflater()trong hàm tạo ( PNGImageDecoderví dụ) và không gọi end()của biến thể. Chà, nếu bạn truyền vào hàm tạo một cách chính xác new, không có cơ hội nào ... Và vâng, việc gọi close()trên luồng không đóng được bộ lọc nếu nó được truyền thủ công dưới dạng tham số của hàm tạo. Đây không phải là một rò rỉ thực sự vì nó sẽ được phát hành bởi người hoàn thiện ... khi thấy cần thiết. Cho đến lúc đó nó ăn bộ nhớ bản địa rất tệ, nó có thể khiến Linux oom_killer giết chết quá trình với sự trừng phạt. Vấn đề chính là việc hoàn thiện trong Java rất không đáng tin cậy và G1 làm cho nó trở nên tồi tệ hơn cho đến 7.0.2. Đạo đức của câu chuyện: phát hành tài nguyên bản địa càng sớm càng tốt; người hoàn thiện là quá nghèo

  • Trường hợp tương tự với java.util.zip.Deflater. Điều này còn tệ hơn nhiều vì Deflater đang đói bộ nhớ trong Java, tức là luôn sử dụng 15 bit (tối đa) và 8 cấp bộ nhớ (9 là tối đa) phân bổ vài trăm KB bộ nhớ riêng. May mắn thay, Deflaterkhông được sử dụng rộng rãi và theo hiểu biết của tôi, JDK không chứa sự lạm dụng. Luôn gọi end()nếu bạn tự tạo một Deflaterhoặc Inflater. Phần tốt nhất của hai phần cuối: bạn không thể tìm thấy chúng thông qua các công cụ định hình thông thường có sẵn.

(Tôi có thể thêm một số lãng phí thời gian nữa mà tôi đã gặp theo yêu cầu.)

Chúc may mắn và an toàn; rò rỉ là xấu xa!


23
Creating but not starting a Thread...Rất tiếc, tôi đã bị cắn bởi điều này một vài thế kỷ trước! (Java 1.3)
leonbloy

@leonbloy, trước khi nó thậm chí còn tệ hơn khi luồng được thêm thẳng vào nhóm luồng, không bắt đầu có nghĩa là rò rỉ rất khó khăn. Không chỉ tăng unstartedsố lượng mà còn ngăn chặn nhóm chủ đề tiêu diệt (ác ít hơn nhưng vẫn bị rò rỉ)
bestsss

Cảm ơn bạn! "Gọi ThreadGroup.destroy()khi Threadgroup không có chủ đề ..." là một lỗi cực kỳ tinh vi; Tôi đã theo đuổi điều này trong nhiều giờ, dẫn đến lạc lối vì liệt kê chuỗi trong GUI điều khiển của tôi không cho thấy gì, nhưng nhóm luồng và, có lẽ, ít nhất một nhóm trẻ sẽ không biến mất.
Lawrence Dol

1
@bestsss: Tôi tò mò, tại sao bạn lại muốn gỡ bỏ một cái móc tắt máy, vì nó chạy ở chế độ JVM, tắt máy?
Lawrence Dol

203

Hầu hết các ví dụ ở đây là "quá phức tạp". Họ là những trường hợp cạnh. Với các ví dụ này, lập trình viên đã mắc lỗi (như không xác định lại bằng / mã băm) hoặc đã bị cắn bởi một trường hợp góc của JVM / JAVA (tải lớp có tĩnh ...). Tôi nghĩ đó không phải là ví dụ mà người phỏng vấn muốn hoặc thậm chí là trường hợp phổ biến nhất.

Nhưng có những trường hợp thực sự đơn giản hơn cho rò rỉ bộ nhớ. Trình thu gom rác chỉ giải phóng những gì không còn được tham chiếu. Chúng tôi là những nhà phát triển Java không quan tâm đến bộ nhớ. Chúng tôi phân bổ nó khi cần thiết và để nó được giải phóng tự động. Khỏe.

Nhưng bất kỳ ứng dụng tồn tại lâu dài có xu hướng chia sẻ trạng thái. Nó có thể là bất cứ thứ gì, số liệu thống kê, singletons ... Thông thường các ứng dụng không tầm thường có xu hướng tạo ra các đồ thị đối tượng phức tạp. Chỉ cần quên đặt tham chiếu thành null hoặc thường xuyên hơn việc quên xóa một đối tượng khỏi bộ sưu tập là đủ để làm rò rỉ bộ nhớ.

Tất nhiên tất cả các loại trình nghe (như trình nghe UI), bộ nhớ cache hoặc bất kỳ trạng thái chia sẻ tồn tại lâu nào có xu hướng tạo ra rò rỉ bộ nhớ nếu không được xử lý đúng cách. Điều cần hiểu là đây không phải là trường hợp góc Java hoặc là vấn đề với trình thu gom rác. Đây là một vấn đề thiết kế. Chúng tôi thiết kế rằng chúng tôi thêm một người nghe vào một đối tượng tồn tại lâu dài, nhưng chúng tôi không loại bỏ người nghe khi không còn cần thiết. Chúng tôi lưu trữ các đối tượng, nhưng chúng tôi không có chiến lược để loại bỏ chúng khỏi bộ đệm.

Chúng ta có thể có một biểu đồ phức tạp lưu trữ trạng thái trước đó cần thiết cho một tính toán. Nhưng trạng thái trước đó tự nó được liên kết với trạng thái trước và như vậy.

Giống như chúng ta phải đóng các kết nối hoặc tệp SQL. Chúng ta cần đặt tham chiếu thích hợp thành null và xóa các phần tử khỏi bộ sưu tập. Chúng ta sẽ có các chiến lược lưu trữ phù hợp (kích thước bộ nhớ tối đa, số lượng phần tử hoặc bộ định thời). Tất cả các đối tượng cho phép người nghe được thông báo phải cung cấp cả phương thức addListener và removeListener. Và khi các thông báo này không còn được sử dụng, họ phải xóa danh sách người nghe của họ.

Rò rỉ bộ nhớ là thực sự có thể và hoàn toàn có thể dự đoán được. Không cần các tính năng ngôn ngữ đặc biệt hoặc trường hợp góc. Rò rỉ bộ nhớ là một chỉ báo cho thấy có thể thiếu thứ gì đó hoặc thậm chí là có vấn đề về thiết kế.


24
Tôi thấy buồn cười khi trên các câu trả lời khác, mọi người đang tìm kiếm những trường hợp và mánh khóe đó và dường như hoàn toàn thiếu quan điểm. Họ chỉ có thể hiển thị mã giữ các tham chiếu vô dụng đến các đối tượng sẽ không bao giờ sử dụng lại và không bao giờ xóa các tham chiếu đó; người ta có thể nói rằng những trường hợp đó không phải là rò rỉ bộ nhớ "thật" bởi vì vẫn có các tham chiếu đến các đối tượng xung quanh, nhưng nếu chương trình không bao giờ sử dụng các tham chiếu đó nữa và cũng không bao giờ bỏ chúng, thì nó hoàn toàn tương đương với (và tệ như) a " rò rỉ bộ nhớ thật ".
ehabkost

@Nicolas Bousquet: "Rò rỉ bộ nhớ thực sự rất có thể" Cảm ơn bạn rất nhiều. +15 lượt upvote. Đẹp. Tôi đã hét lên ở đây vì đã nêu thực tế đó, vì tiền đề của một câu hỏi về ngôn ngữ Go: stackoverflow.com/questions/4400311 Câu hỏi này vẫn có những phản hồi tiêu cực :(
SyntaxT3rr0r

GC trong Java và .NET theo một nghĩa nào đó được xác định dựa trên biểu đồ giả định của các đối tượng chứa các tham chiếu đến các đối tượng khác giống như biểu đồ của các đối tượng "quan tâm" đến các đối tượng khác. Trong thực tế, có thể các cạnh có thể tồn tại trong biểu đồ tham chiếu không thể hiện quan hệ "quan tâm" và đối tượng có thể quan tâm đến sự tồn tại của đối tượng khác ngay cả khi không có đường dẫn tham chiếu trực tiếp hoặc gián tiếp (thậm chí sử dụng WeakReference) từ người này sang người khác. Nếu một tham chiếu đối tượng có một chút dự phòng, có thể hữu ích khi có chỉ báo "quan tâm đến mục tiêu" ...
supercat

... và yêu cầu hệ thống cung cấp thông báo (thông qua các phương tiện tương tự PhantomReference) nếu một đối tượng được tìm thấy không có ai quan tâm đến nó. WeakReferenceđến gần, nhưng phải được chuyển đổi thành một tài liệu tham khảo mạnh mẽ trước khi nó có thể được sử dụng; nếu một chu trình GC xảy ra trong khi tham chiếu mạnh tồn tại, mục tiêu sẽ được coi là hữu ích.
supercat

Đây là ý kiến ​​của tôi câu trả lời chính xác. Chúng tôi đã viết một mô phỏng nhiều năm trước. Bằng cách nào đó chúng ta vô tình liên kết trạng thái trước với trạng thái hiện tại tạo ra rò rỉ bộ nhớ. Vì thời hạn, chúng tôi không bao giờ giải quyết rò rỉ bộ nhớ mà biến nó thành «tính năng» bằng cách ghi lại nó.
nalply

163

Câu trả lời hoàn toàn phụ thuộc vào những gì người phỏng vấn nghĩ rằng họ đang hỏi.

Trong thực tế có thể làm cho rò rỉ Java? Tất nhiên là có, và có rất nhiều ví dụ trong các câu trả lời khác.

Nhưng có nhiều câu hỏi meta có thể đã được hỏi?

  • Là một triển khai Java "hoàn hảo" về mặt lý thuyết có dễ bị rò rỉ không?
  • Ứng viên có hiểu sự khác biệt giữa lý thuyết và thực tế không?
  • Ứng viên có hiểu cách thức thu gom rác hoạt động không?
  • Hoặc làm thế nào thu gom rác được cho là để làm việc trong một trường hợp lý tưởng?
  • Họ có biết họ có thể gọi các ngôn ngữ khác thông qua giao diện bản địa không?
  • Họ có biết để rò rỉ bộ nhớ trong các ngôn ngữ khác không?
  • Ứng viên thậm chí có biết quản lý bộ nhớ là gì và những gì đang diễn ra đằng sau hậu trường trong Java không?

Tôi đang đọc câu hỏi meta của bạn là "Câu trả lời nào tôi có thể sử dụng trong tình huống phỏng vấn này". Và do đó, tôi sẽ tập trung vào các kỹ năng phỏng vấn thay vì Java. Tôi tin rằng bạn có nhiều khả năng lặp lại tình huống không biết câu trả lời cho một câu hỏi trong một cuộc phỏng vấn hơn là bạn đang ở một nơi cần biết cách làm cho Java bị rò rỉ. Vì vậy, hy vọng, điều này sẽ giúp.

Một trong những kỹ năng quan trọng nhất bạn có thể phát triển khi phỏng vấn là học cách chủ động lắng nghe các câu hỏi và làm việc với người phỏng vấn để rút ra ý định của họ. Điều này không chỉ cho phép bạn trả lời câu hỏi của họ theo cách họ muốn, mà còn cho thấy rằng bạn có một số kỹ năng giao tiếp quan trọng. Và khi có sự lựa chọn giữa nhiều nhà phát triển tài năng không kém, tôi sẽ thuê người lắng nghe, suy nghĩ và hiểu trước khi họ trả lời mỗi lần.


22
Bất cứ khi nào tôi hỏi câu hỏi đó, tôi đều tìm kiếm một câu trả lời khá đơn giản - tiếp tục phát triển một hàng đợi, cuối cùng không đóng db, không phải là chi tiết chủ đề / trình tải lớp lẻ, ngụ ý họ hiểu những gì gc có thể và không thể làm cho bạn. Tôi phụ thuộc vào công việc bạn đang phỏng vấn cho tôi đoán.
DaveC

Xin hãy xem câu hỏi của tôi, cảm ơn stackoverflow.com/questions/31108772/iêu
Daniel Newtown

130

Sau đây là một ví dụ khá vô nghĩa, nếu bạn không hiểu JDBC . Hoặc ít nhất là như thế nào JDBC hy vọng một nhà phát triển để gần gũi Connection, StatementResultSettrường hợp trước khi vứt bỏ chúng hoặc mất tài liệu tham khảo đối với họ, thay vì dựa vào việc thực hiện finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

Vấn đề với ở trên là Connectionđối tượng không được đóng, và do đó kết nối vật lý sẽ vẫn mở, cho đến khi trình thu gom rác xuất hiện và thấy rằng nó không thể truy cập được. GC sẽ gọi finalizephương thức này, nhưng có các trình điều khiển JDBC không thực hiện finalize, ít nhất là không giống như cách Connection.closeđược thực hiện. Hành vi kết quả là trong khi bộ nhớ sẽ được thu hồi do các đối tượng không thể truy cập được thu thập, các tài nguyên (bao gồm cả bộ nhớ) được liên kết với Connectionđối tượng có thể đơn giản không được lấy lại.

Trong một sự kiện như vậy mà Connection's finalizephương pháp không sạch tất cả mọi thứ, người ta có thể thực sự thấy rằng các kết nối vật lý đến máy chủ cơ sở dữ liệu sẽ kéo dài vài chu kỳ thu gom rác thải, cho đến khi máy chủ cơ sở dữ liệu cuối cùng hiểu ra rằng kết nối là không còn sống (nếu nó không), và nên được đóng lại.

Ngay cả khi trình điều khiển JDBC được triển khai finalize, các ngoại lệ có thể bị ném trong quá trình hoàn thiện. Hành vi kết quả là bất kỳ bộ nhớ nào liên quan đến đối tượng "không hoạt động" hiện tại sẽ không được lấy lại, vì finalizeđược đảm bảo chỉ được gọi một lần.

Kịch bản trên gặp phải các ngoại lệ trong quá trình hoàn thiện đối tượng có liên quan đến một kịch bản khác có thể dẫn đến rò rỉ bộ nhớ - phục hồi đối tượng. Sự phục sinh đối tượng thường được thực hiện có chủ ý bằng cách tạo ra một tham chiếu mạnh mẽ đến đối tượng khỏi bị hoàn thiện, từ một đối tượng khác. Khi phục hồi đối tượng bị lạm dụng, nó sẽ dẫn đến rò rỉ bộ nhớ kết hợp với các nguồn rò rỉ bộ nhớ khác.

Còn rất nhiều ví dụ nữa mà bạn có thể gợi lên - như

  • Quản lý một Listtrường hợp mà bạn chỉ thêm vào danh sách và không xóa khỏi danh sách đó (mặc dù bạn sẽ loại bỏ các yếu tố bạn không còn cần nữa), hoặc
  • Mở Sockets hoặc Files, nhưng không đóng chúng khi không còn cần thiết (tương tự như ví dụ trên liên quan đến Connectionlớp).
  • Không dỡ Singletons khi đưa xuống ứng dụng Java EE. Rõ ràng, Trình nạp lớp đã tải lớp đơn sẽ giữ lại một tham chiếu đến lớp và do đó, cá thể đơn sẽ không bao giờ được thu thập. Khi một phiên bản mới của ứng dụng được triển khai, trình nạp lớp mới thường được tạo và trình nạp lớp cũ sẽ tiếp tục tồn tại do singleton.

98
Bạn sẽ đạt đến giới hạn kết nối mở tối đa trước khi bạn đạt giới hạn bộ nhớ thường. Đừng hỏi tôi tại sao tôi biết ...
Phần cứng

Trình điều khiển JDBC của Oracle nổi tiếng với việc này.
chotchki

@Hardwareguy Tôi đã đạt đến giới hạn kết nối của cơ sở dữ liệu SQL rất nhiều cho đến khi tôi đưa Connection.closevào khối cuối cùng của tất cả các cuộc gọi SQL của mình. Để vui hơn, tôi đã gọi một số thủ tục lưu trữ lâu dài của Oracle yêu cầu khóa ở phía Java để ngăn quá nhiều cuộc gọi đến cơ sở dữ liệu.
Michael Storesin

@Hardwareguy Điều đó thật thú vị nhưng không thực sự cần thiết rằng các giới hạn kết nối thực tế sẽ bị ảnh hưởng cho tất cả các môi trường. Ví dụ, đối với một ứng dụng được triển khai trên máy chủ ứng dụng weblogic 11g, tôi đã thấy rò rỉ kết nối ở quy mô lớn. Nhưng do tùy chọn thu hoạch kết nối cơ sở dữ liệu bị rò rỉ vẫn có sẵn trong khi rò rỉ bộ nhớ đang được giới thiệu. Tôi không chắc chắn về tất cả các môi trường.
Aseem Bansal

Theo kinh nghiệm của tôi, bạn bị rò rỉ bộ nhớ ngay cả khi bạn đóng kết nối. Trước tiên, bạn cần đóng Kết quả và PreparedStatement. Có một máy chủ bị sập liên tục sau nhiều giờ hoặc thậm chí nhiều ngày hoạt động tốt, do OutOfMemoryErrors, cho đến khi tôi bắt đầu làm điều đó.
Bjørn Stenfeldt

119

Có lẽ một trong những ví dụ đơn giản nhất về rò rỉ bộ nhớ tiềm năng và cách tránh nó là việc triển khai ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Nếu bạn đang tự thực hiện nó, bạn có nghĩ sẽ xóa phần tử mảng không còn được sử dụng ( elementData[--size] = null) không? Tài liệu tham khảo đó có thể giữ cho một vật thể khổng lồ còn sống ...


5
Và rò rỉ bộ nhớ ở đây là ở đâu?
rds

28
@maniek: Tôi không có ý ám chỉ rằng mã này thể hiện sự rò rỉ bộ nhớ. Tôi đã trích dẫn nó để chỉ ra rằng đôi khi mã không rõ ràng là cần thiết để tránh việc giữ đối tượng vô tình.
meriton

RangeCheck (chỉ mục) là gì; ?
Koray Tugay

6
Joshua Bloch đã đưa ra ví dụ này trong Java hiệu quả cho thấy việc triển khai Stacks đơn giản. Một câu trả lời rất hay.
thuê

Nhưng đó sẽ không phải là một rò rỉ bộ nhớ THỰC, ngay cả khi quên. Phần tử vẫn sẽ được truy cập SAFELLY bằng Reflection, nó sẽ không rõ ràng và có thể truy cập trực tiếp thông qua giao diện Danh sách, nhưng đối tượng và tham chiếu vẫn ở đó và có thể được truy cập an toàn.
DGoiko

68

Bất cứ khi nào bạn giữ các tài liệu tham khảo xung quanh các đối tượng mà bạn không còn cần bạn bị rò rỉ bộ nhớ. Xem Xử lý rò rỉ bộ nhớ trong các chương trình Java để biết ví dụ về cách rò rỉ bộ nhớ thể hiện trong Java và những gì bạn có thể làm về nó.


14
Tôi không tin đây là một "rò rỉ". Đó là một lỗi, và đó là do thiết kế chương trình và ngôn ngữ. Một rò rỉ sẽ là một đối tượng treo xung quanh mà không có bất kỳ tham chiếu đến nó.
dùng541686

29
@Mehrdad: Đó chỉ là một định nghĩa hẹp không áp dụng đầy đủ cho tất cả các ngôn ngữ. Tôi cho rằng bất kỳ rò rỉ bộ nhớ nào cũng là lỗi do thiết kế chương trình kém.
Bill the Lizard

9
@Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. Tôi không thấy cách bạn rút ra kết luận đó. Có ít cách để tạo rò rỉ bộ nhớ trong Java theo bất kỳ định nghĩa nào. Đó chắc chắn vẫn là một câu hỏi hợp lệ.
Bill the Lizard

7
@ 31eee384: Nếu chương trình của bạn giữ các đối tượng trong bộ nhớ mà nó không bao giờ có thể sử dụng, thì về mặt kỹ thuật, đó là bộ nhớ bị rò rỉ. Thực tế là bạn có vấn đề lớn hơn không thực sự thay đổi điều đó.
Bill Lizard

8
@ 31eee384: Nếu bạn biết thực tế là nó sẽ không như vậy, thì không thể. Chương trình, như được viết, sẽ không bao giờ truy cập dữ liệu.
Bill Lizard

51

Bạn có thể làm rò rỉ bộ nhớ với lớp sun.misc.Unsafe . Trong thực tế, lớp dịch vụ này được sử dụng trong các lớp tiêu chuẩn khác nhau (ví dụ trong các lớp java.nio ). Bạn không thể tạo cá thể của lớp này trực tiếp , nhưng bạn có thể sử dụng sự phản chiếu để làm điều đó .

Mã không biên dịch trong IDE Eclipse - biên dịch nó bằng lệnh javac(trong quá trình biên dịch, bạn sẽ nhận được cảnh báo)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

1
Bộ nhớ được phân bổ là vô hình cho người thu gom rác
thân cây

4
Bộ nhớ được phân bổ cũng không thuộc về Java.
bestsss

Là mặt trời / oracle jvm cụ thể? Ví dụ, điều này sẽ làm việc trên IBM?
Berlin Brown

2
Bộ nhớ chắc chắn "thuộc về Java", ít nhất là theo nghĩa i) nó không có sẵn cho bất kỳ ai khác, ii) khi ứng dụng Java thoát ra, nó sẽ được trả về hệ thống. Nó chỉ nằm ngoài JVM.
Michael Anderson

3
Điều này sẽ xây dựng trong nhật thực (ít nhất là trong các phiên bản gần đây) nhưng bạn sẽ cần thay đổi cài đặt trình biên dịch: trong Window> Preferences> Java> Trình biên dịch> Lỗi / Cảnh báo> Không dùng API và giới hạn đặt tham chiếu Cấm (quy tắc truy cập) thành "Cảnh báo ".
Michael Anderson

43

Tôi có thể sao chép câu trả lời của mình từ đây: Cách dễ nhất để gây rò rỉ bộ nhớ trong Java?

"Rò rỉ bộ nhớ, trong khoa học máy tính (hoặc rò rỉ, trong bối cảnh này), xảy ra khi một chương trình máy tính tiêu thụ bộ nhớ nhưng không thể giải phóng nó trở lại hệ điều hành." (Wikipedia)

Câu trả lời dễ dàng là: Bạn không thể. Java thực hiện quản lý bộ nhớ tự động và sẽ giải phóng các tài nguyên không cần thiết cho bạn. Bạn không thể ngăn điều này xảy ra. Nó sẽ luôn luôn có thể phát hành các tài nguyên. Trong các chương trình với quản lý bộ nhớ thủ công, điều này là khác nhau. Bạn không thể lấy một số bộ nhớ trong C bằng cách sử dụng malloc (). Để giải phóng bộ nhớ, bạn cần con trỏ mà malloc trả về và gọi free () trên nó. Nhưng nếu bạn không có con trỏ nữa (bị ghi đè hoặc vượt quá tuổi thọ), thì thật không may, bạn không có khả năng giải phóng bộ nhớ này và do đó bạn bị rò rỉ bộ nhớ.

Tất cả các câu trả lời khác cho đến nay là trong định nghĩa của tôi không thực sự rò rỉ bộ nhớ. Tất cả đều nhằm mục đích lấp đầy bộ nhớ với những thứ vô nghĩa thực sự nhanh chóng. Nhưng bất cứ lúc nào bạn vẫn có thể hủy đăng ký các đối tượng bạn đã tạo và do đó giải phóng bộ nhớ -> KHÔNG LEAK. Câu trả lời của acconrad khá gần mặc dù tôi phải thừa nhận vì giải pháp của anh ấy thực sự là "đánh sập" người thu gom rác bằng cách buộc nó vào một vòng lặp vô tận).

Câu trả lời dài là: Bạn có thể bị rò rỉ bộ nhớ bằng cách viết thư viện cho Java bằng JNI, có thể quản lý bộ nhớ thủ công và do đó bị rò rỉ bộ nhớ. Nếu bạn gọi thư viện này, quá trình java của bạn sẽ bị rò rỉ bộ nhớ. Hoặc, bạn có thể có lỗi trong JVM, do đó JVM mất bộ nhớ. Có thể có các lỗi trong JVM, thậm chí có thể có một số lỗi đã biết do bộ sưu tập rác không phải là chuyện nhỏ, nhưng sau đó nó vẫn là một lỗi. Theo thiết kế này là không thể. Bạn có thể yêu cầu một số mã java bị ảnh hưởng bởi một lỗi như vậy. Xin lỗi tôi không biết một cái và nó có thể không còn là một lỗi nữa trong phiên bản Java tiếp theo.


12
Đó là một định nghĩa cực kỳ hạn chế (và không hữu ích) về rò rỉ bộ nhớ. Định nghĩa duy nhất có ý nghĩa cho các mục đích thực tế là "rò rỉ bộ nhớ là bất kỳ điều kiện nào trong đó chương trình tiếp tục giữ bộ nhớ được phân bổ sau khi dữ liệu chứa nó không còn cần thiết nữa".
Mason Wheeler

1
Câu trả lời của acconrad đã bị xóa?
Tomáš Zato - Phục hồi Monica

1
@ TomášZato: Không, không phải vậy. Tôi đã chuyển tham chiếu ở trên để liên kết bây giờ, vì vậy bạn có thể dễ dàng tìm thấy nó.
yankee

Phục sinh đối tượng là gì? Bao nhiêu lần một hàm hủy được gọi? Làm thế nào để những câu hỏi từ chối câu trả lời này?
tự kỷ

1
Chà, chắc chắn rằng bạn có thể tạo rò rỉ bộ nhớ trong Java, mặc dù có GC và vẫn hoàn thành định nghĩa trên. Chỉ cần có cấu trúc dữ liệu chỉ nối thêm, được bảo vệ chống lại truy cập bên ngoài để không có mã nào khác loại bỏ nó - chương trình không thể giải phóng bộ nhớ vì nó không có mã.
toolforger

39

Đây là một cái đơn giản / nham hiểm thông qua http://wiki.eclipse.org/Performance_Bloopers#String.subopes.28,29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Bởi vì chuỗi con đề cập đến biểu diễn bên trong của chuỗi gốc, dài hơn nhiều, nên gốc vẫn ở trong bộ nhớ. Do đó, miễn là bạn có StringLeaker khi chơi, bạn cũng có toàn bộ chuỗi gốc trong bộ nhớ, mặc dù bạn có thể nghĩ rằng mình chỉ đang giữ một chuỗi ký tự đơn.

Cách để tránh lưu trữ một tham chiếu không mong muốn vào chuỗi gốc là làm một cái gì đó như thế này:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Để thêm tính xấu, bạn cũng có thể .intern()là chuỗi con:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Làm như vậy sẽ giữ cả chuỗi dài gốc và chuỗi con dẫn xuất trong bộ nhớ ngay cả sau khi đối tượng StringLeaker bị loại bỏ.


4
Tôi sẽ không gọi đó là một rò rỉ bộ nhớ, cho mỗi gia nhập . Khi muchSmallerStringđược giải phóng (vì StringLeakerđối tượng bị phá hủy), chuỗi dài cũng sẽ được giải phóng. Cái mà tôi gọi là rò rỉ bộ nhớ là bộ nhớ không bao giờ có thể được giải phóng trong trường hợp JVM này. Tuy nhiên, bạn đã chỉ cho mình cách giải phóng bộ nhớ : this.muchSmallerString=new String(this.muchSmallerString). Với một rò rỉ bộ nhớ thực, không có gì bạn có thể làm.
rds

2
@rds, đó là một điểm công bằng. internTrường hợp không phải là trường hợp "bất ngờ về bộ nhớ" hơn là "rò rỉ bộ nhớ". .intern()Mặc dù vậy, chuỗi con chắc chắn tạo ra một tình huống trong đó tham chiếu đến chuỗi dài hơn được bảo tồn và không thể được giải phóng.
Jon Chambers

15
Chuỗi con phương thức () tạo ra một Chuỗi mới trong java7 (đó là một hành vi mới)
anstarovoyt

Thậm chí, bạn không cần phải tự thực hiện chuỗi con (): Sử dụng trình so khớp regex để khớp một phần nhỏ của đầu vào lớn và mang theo chuỗi "trích xuất" trong một thời gian dài. Đầu vào khổng lồ vẫn tồn tại đến Java 6.
Tuneweizen

37

Một ví dụ phổ biến về điều này trong mã GUI là khi tạo một widget / thành phần và thêm một trình nghe vào một đối tượng trong phạm vi tĩnh / ứng dụng và sau đó không xóa trình nghe khi widget bị phá hủy. Bạn không chỉ bị rò rỉ bộ nhớ mà còn bị ảnh hưởng bởi hiệu suất như khi bất cứ điều gì bạn đang nghe các sự kiện cháy nổ, tất cả những người nghe cũ của bạn cũng được gọi.


1
Nền tảng Android cung cấp ví dụ về rò rỉ bộ nhớ được tạo bằng cách lưu trữ Bitmap trong trường tĩnh của Chế độ xem .
rds

36

Lấy bất kỳ ứng dụng web nào đang chạy trong bất kỳ thùng chứa servlet nào (Tomcat, Jetty, Glassfish, bất cứ điều gì ...). Tái triển khai ứng dụng 10 hoặc 20 lần liên tiếp (có thể chỉ cần chạm vào WAR trong thư mục tự động triển khai của máy chủ.

Trừ khi có ai thực sự đã thử nghiệm điều này, rất có thể bạn sẽ nhận được OutOfMemoryError sau một vài lần triển khai, vì ứng dụng không cẩn thận để tự dọn dẹp. Bạn thậm chí có thể tìm thấy một lỗi trong máy chủ của bạn với bài kiểm tra này.

Vấn đề là, thời gian tồn tại của container dài hơn thời gian ứng dụng của bạn. Bạn phải đảm bảo rằng tất cả các tham chiếu mà vùng chứa có thể phải có đối tượng hoặc các lớp trong ứng dụng của bạn có thể được thu gom rác.

Nếu chỉ có một tham chiếu tồn tại trong quá trình triển khai ứng dụng web của bạn, trình nạp lớp tương ứng và do đó, tất cả các lớp của ứng dụng web của bạn không thể được thu gom rác.

Các chủ đề được bắt đầu bởi ứng dụng của bạn, các biến ThreadLocal, các trình nối thêm ghi nhật ký là một số nghi ngờ thông thường gây ra rò rỉ trình nạp lớp.


1
Điều này không phải do rò rỉ bộ nhớ, mà vì trình nạp lớp không hủy tập hợp các lớp trước đó. Do đó, không nên triển khai lại máy chủ ứng dụng mà không khởi động lại máy chủ (không phải máy vật lý mà là máy chủ ứng dụng). Tôi đã thấy vấn đề tương tự với WebSphere.
Sven

35

Có thể bằng cách sử dụng mã gốc bên ngoài thông qua JNI?

Với Java thuần túy, điều đó gần như là không thể.

Nhưng đó là về một loại rò rỉ bộ nhớ "tiêu chuẩn", khi bạn không thể truy cập vào bộ nhớ nữa, nhưng nó vẫn thuộc sở hữu của ứng dụng. Thay vào đó, bạn có thể giữ các tham chiếu đến các đối tượng không sử dụng hoặc mở các luồng mà không đóng chúng sau đó.


22
Điều đó phụ thuộc vào định nghĩa của "rò rỉ bộ nhớ". Nếu "bộ nhớ được giữ, nhưng không còn cần thiết", thì thật dễ dàng thực hiện trong Java. Nếu đó là "bộ nhớ được cấp phát nhưng không thể truy cập được bằng mã", thì nó sẽ khó hơn một chút.
Joachim Sauer

@Joachim Sauer - Ý tôi là loại thứ hai. Việc đầu tiên khá dễ thực hiện :)
Rogach

6
"Với java thuần túy, điều đó gần như là không thể." Vâng, kinh nghiệm của tôi là một đặc biệt khác khi nói đến việc thực hiện lưu trữ bởi những người không nhận thức được những cạm bẫy ở đây.
Fabian Barney

4
@Rogach: về cơ bản có hơn 400 lượt upvote cho các câu trả lời khác nhau của những người có +10 000 đại diện cho thấy rằng trong cả hai trường hợp Joachim Sauer đều nhận xét điều đó rất khả thi. Vì vậy, "gần như không thể" của bạn không có ý nghĩa.
Cú phápT3rr0r

32

Tôi đã có một "rò rỉ bộ nhớ" đẹp liên quan đến phân tích cú pháp PermGen và XML một lần. Trình phân tích cú pháp XML mà chúng tôi đã sử dụng (tôi không thể nhớ đó là trình phân tích cú pháp nào) đã thực hiện String.i INTERN () trên tên thẻ, để so sánh nhanh hơn. Một trong những khách hàng của chúng tôi đã có ý tưởng tuyệt vời để lưu trữ các giá trị dữ liệu không phải trong các thuộc tính hoặc văn bản XML, mà dưới dạng tên thẻ, vì vậy chúng tôi đã có một tài liệu như:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

Trên thực tế, họ không sử dụng số nhưng ID văn bản dài hơn (khoảng 20 ký tự), là số duy nhất và có tốc độ 10 - 15 triệu mỗi ngày. Điều đó tạo ra 200 MB rác mỗi ngày, điều này không bao giờ cần thiết nữa và không bao giờ được xử lý (vì nó nằm trong PermGen). Chúng tôi đã cài đặt permgen thành 512 MB, vì vậy phải mất khoảng hai ngày để ngoại lệ hết bộ nhớ (OOME) đến ...


4
Chỉ cần gõ mã ví dụ của bạn: Tôi nghĩ rằng số (hoặc chuỗi bắt đầu bằng số) không được phép làm tên thành phần trong XML.
Paŭlo Ebermann

Lưu ý rằng điều này không còn đúng với JDK 7+, trong đó việc thực hiện chuỗi xảy ra trên heap. Xem bài viết này để viết chi tiết: java-performance.info/opes-itern-in-java-6-7-8
jmiserez

Vì vậy, tôi nghĩ rằng sử dụng StringBuffer thay cho String sẽ giải quyết vấn đề này? không
anubhs

24

Rò rỉ bộ nhớ là gì:

  • Nó gây ra bởi một lỗi hoặc thiết kế xấu.
  • Thật lãng phí bộ nhớ.
  • Nó trở nên tồi tệ hơn theo thời gian.
  • Người thu gom rác không thể làm sạch nó.

Ví dụ điển hình:

Bộ đệm của các đối tượng là điểm khởi đầu tốt để làm mọi thứ rối tung lên.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Bộ nhớ cache của bạn phát triển và tăng trưởng. Và chẳng mấy chốc, toàn bộ cơ sở dữ liệu bị hút vào bộ nhớ. Một thiết kế tốt hơn sử dụng một LRUMap (Chỉ giữ các đối tượng được sử dụng gần đây trong bộ đệm).

Chắc chắn, bạn có thể làm cho mọi thứ phức tạp hơn nhiều:

  • sử dụng các cấu trúc ThreadLocal .
  • thêm cây tham chiếu phức tạp hơn .
  • hoặc rò rỉ gây ra bởi các thư viện bên thứ 3 .

Điều thường xảy ra:

Nếu đối tượng Thông tin này có tham chiếu đến các đối tượng khác, một lần nữa có tham chiếu đến các đối tượng khác. Theo một cách nào đó, bạn cũng có thể coi đây là một loại rò rỉ bộ nhớ, (gây ra bởi thiết kế xấu).


22

Tôi nghĩ thật thú vị khi không ai sử dụng các ví dụ lớp bên trong. Nếu bạn có một lớp học nội bộ; nó vốn đã duy trì một tham chiếu đến lớp chứa. Tất nhiên, về mặt kỹ thuật nó không bị rò rỉ bộ nhớ vì Java SILL cuối cùng sẽ dọn sạch nó; nhưng điều này có thể khiến các lớp học quanh quẩn lâu hơn dự kiến.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Bây giờ nếu bạn gọi Ví dụ1 và nhận được Ví dụ2 loại bỏ Ví dụ1, bạn sẽ vẫn có một liên kết đến một đối tượng Ví dụ1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

Tôi cũng đã nghe một tin đồn rằng nếu bạn có một biến tồn tại lâu hơn một khoảng thời gian cụ thể; Java giả định rằng nó sẽ luôn tồn tại và thực sự sẽ không bao giờ cố gắng dọn sạch nó nếu không thể đạt được trong mã nữa. Nhưng điều đó là hoàn toàn chưa được xác minh.


2
các lớp bên trong hiếm khi là một vấn đề. Chúng là một trường hợp đơn giản và rất dễ phát hiện. Tin đồn cũng chỉ là tin đồn.
bestsss

2
"Tin đồn" nghe giống như ai đó đã đọc được một nửa về cách thức hoạt động của thế hệ GC. Các vật thể tồn tại lâu nhưng không thể tiếp cận thực sự có thể bám xung quanh và chiếm không gian trong một thời gian, bởi vì JVM đã thúc đẩy chúng ra khỏi thế hệ trẻ để nó có thể ngừng kiểm tra chúng mỗi khi vượt qua. Họ sẽ trốn tránh việc "dọn sạch 5000 chuỗi tạm thời" của tôi, theo thiết kế. Nhưng họ không bất tử. Chúng vẫn đủ điều kiện để thu thập và nếu VM được gắn vào RAM, cuối cùng nó sẽ chạy quét toàn bộ GC và lấy lại bộ nhớ đó.
cHao

22

Gần đây tôi đã gặp một tình huống rò rỉ bộ nhớ do log4j gây ra.

Log4j có cơ chế này được gọi là Bối cảnh chẩn đoán lồng nhau (NDC) , là một công cụ để phân biệt đầu ra nhật ký xen kẽ với các nguồn khác nhau. Độ chi tiết mà tại đó NDC hoạt động là các luồng, do đó, nó phân biệt các đầu ra nhật ký với các luồng khác nhau một cách riêng biệt.

Để lưu trữ các thẻ cụ thể của luồng, lớp NDC của log4j sử dụng Hashtable được khóa bởi chính đối tượng Thread (trái ngược với id của luồng), và do đó cho đến khi thẻ NDC nằm trong bộ nhớ tất cả các đối tượng treo trên luồng đối tượng cũng ở lại trong bộ nhớ. Trong ứng dụng web của chúng tôi, chúng tôi sử dụng NDC để gắn thẻ logoutput với id yêu cầu để phân biệt các bản ghi với một yêu cầu riêng biệt. Container chứa liên kết thẻ NDC với một luồng, cũng loại bỏ nó trong khi trả về phản hồi từ một yêu cầu. Sự cố xảy ra khi trong quá trình xử lý yêu cầu, một luồng con được sinh ra, giống như đoạn mã sau:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Vì vậy, một bối cảnh NDC được liên kết với luồng nội tuyến được sinh ra. Đối tượng luồng là chìa khóa cho bối cảnh NDC này, là luồng nội tuyến có đối tượng HugeList treo trên nó. Do đó, ngay cả sau khi luồng kết thúc thực hiện những gì nó đang làm, tham chiếu đến HugeList vẫn được duy trì bởi bối cảnh NDC Hastable, do đó gây ra rò rỉ bộ nhớ.


Đó là hút. Bạn nên kiểm tra thư viện ghi nhật ký này phân bổ bộ nhớ ZERO trong khi đăng nhập vào một tệp: psychog.soliveirajr.com
TraderJoeChicago

+1 Bạn có biết rõ liệu có vấn đề tương tự với MDC trong slf4j / logback (sản phẩm kế thừa của cùng một tác giả) không? Tôi chuẩn bị lặn sâu vào nguồn nhưng muốn kiểm tra trước. Dù bằng cách nào, cảm ơn vì đã đăng bài này.
sparc_siverse

20

Người phỏng vấn có lẽ đang tìm kiếm một tham chiếu vòng tròn như mã bên dưới (tình cờ chỉ rò rỉ bộ nhớ trong các JVM rất cũ đã sử dụng tính tham chiếu, không còn là trường hợp nữa). Nhưng đó là một câu hỏi khá mơ hồ, vì vậy đây là cơ hội chính để thể hiện sự hiểu biết của bạn về quản lý bộ nhớ JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Sau đó, bạn có thể giải thích rằng với việc đếm tham chiếu, đoạn mã trên sẽ rò rỉ bộ nhớ. Nhưng hầu hết các JVM hiện đại không sử dụng tính năng tham chiếu nữa, hầu hết sử dụng trình thu gom rác quét, trên thực tế sẽ thu thập bộ nhớ này.

Tiếp theo, bạn có thể giải thích việc tạo một Đối tượng có tài nguyên gốc bên dưới, như thế này:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Sau đó, bạn có thể giải thích về mặt kỹ thuật đây là rò rỉ bộ nhớ, nhưng thực sự rò rỉ là do mã gốc trong JVM phân bổ tài nguyên gốc bên dưới, vốn không được giải phóng bởi mã Java của bạn.

Vào cuối ngày, với một JVM hiện đại, bạn cần viết một số mã Java phân bổ tài nguyên riêng ngoài phạm vi nhận thức bình thường của JVM.


19

Mọi người luôn quên tuyến đường mã gốc. Đây là một công thức đơn giản cho một rò rỉ:

  1. Khai báo phương thức riêng.
  2. Trong phương thức bản địa, gọi malloc. Đừng gọifree .
  3. Gọi phương thức bản địa.

Hãy nhớ rằng, cấp phát bộ nhớ trong mã gốc đến từ heap JVM.


1
Dựa trên một câu chuyện có thật.
Reg

18

Tạo một Bản đồ tĩnh và tiếp tục thêm các tham chiếu cứng vào nó. Những người sẽ không bao giờ là GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

87
Làm thế nào là một rò rỉ? Đó là làm chính xác những gì bạn yêu cầu nó làm. Nếu đó là một rò rỉ, tạo và lưu trữ các đối tượng bất cứ nơi nào là một rò rỉ.
Falmarri

3
Tôi đồng ý với @Falmarri. Tôi không thấy rò rỉ ở đó, bạn chỉ đang tạo ra các đối tượng. Bạn chắc chắn có thể 'lấy lại' bộ nhớ mà bạn vừa cấp phát bằng một phương thức khác gọi là 'removeFromCache'. Rò rỉ là khi bạn không thể lấy lại bộ nhớ.
Kyle

3
Quan điểm của tôi là ai đó tiếp tục tạo các đối tượng, có lẽ đặt chúng vào bộ đệm, có thể bị lỗi OOM nếu họ không cẩn thận.
duffymo

8
@duffymo: Nhưng đó không thực sự là câu hỏi đang hỏi. Nó không có gì để làm với chỉ đơn giản là sử dụng hết bộ nhớ của bạn.
Falmarri

3
Hoàn toàn không hợp lệ. Bạn chỉ đang thu thập một loạt các đối tượng trong bộ sưu tập Bản đồ. Tài liệu tham khảo của họ sẽ được lưu giữ vì Bản đồ giữ chúng.
gyorgyabraham

16

Bạn có thể tạo rò rỉ bộ nhớ chuyển động bằng cách tạo một thể hiện mới của một lớp trong phương thức hoàn thiện của lớp đó. Điểm thưởng nếu trình hoàn thiện tạo ra nhiều trường hợp. Đây là một chương trình đơn giản rò rỉ toàn bộ heap trong khoảng thời gian từ vài giây đến vài phút tùy thuộc vào kích thước heap của bạn:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

15

Tôi không nghĩ có ai đã nói điều này chưa: bạn có thể hồi sinh một đối tượng bằng cách ghi đè phương thức Finalize () sao cho việc hoàn tất () lưu trữ một tham chiếu về điều này ở đâu đó. Trình thu gom rác sẽ chỉ được gọi một lần trên đối tượng để sau đó đối tượng sẽ không bao giờ bị phá hủy.


10
Điều này là sai sự thật. finalize()sẽ không được gọi nhưng đối tượng sẽ được thu thập khi không có nhiều tài liệu tham khảo. Người thu gom rác cũng không 'gọi là'.
tốt nhất

1
Câu trả lời này là sai lệch, finalize()phương thức chỉ có thể được gọi bởi JVM một lần, nhưng điều này không có nghĩa là nó không thể được thu gom lại rác nếu đối tượng được phục hồi và sau đó được hủy đăng ký lại. Nếu có mã đóng tài nguyên trong finalize()phương thức thì mã này sẽ không được chạy lại, điều này có thể gây rò rỉ bộ nhớ.
Tom Cammann

15

Tôi đã bắt gặp một loại rò rỉ tài nguyên tinh tế hơn gần đây. Chúng tôi mở tài nguyên thông qua getResourceAsStream của trình tải lớp và điều đó đã xảy ra rằng các xử lý luồng đầu vào không bị đóng.

Uhm, bạn có thể nói, thật là một thằng ngốc.

Chà, điều làm cho điều này thú vị là: theo cách này, bạn có thể rò rỉ bộ nhớ heap của quá trình cơ bản, thay vì từ heap của JVM.

Tất cả những gì bạn cần là một tệp jar có một tệp bên trong sẽ được tham chiếu từ mã Java. Tệp jar càng lớn, bộ nhớ càng nhanh được phân bổ.

Bạn có thể dễ dàng tạo một bình như vậy với lớp sau:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Chỉ cần dán vào một tệp có tên BigJarCreator.java, biên dịch và chạy nó từ dòng lệnh:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: bạn tìm thấy một kho lưu trữ jar trong thư mục làm việc hiện tại của bạn với hai tệp bên trong.

Hãy tạo một lớp thứ hai:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Lớp này về cơ bản không làm gì cả, nhưng tạo các đối tượng InputStream không được ước tính. Những đối tượng đó sẽ được thu gom rác ngay lập tức và do đó, không đóng góp vào kích thước đống. Điều quan trọng đối với ví dụ của chúng tôi là tải tài nguyên hiện có từ tệp jar và kích thước không thành vấn đề ở đây!

Nếu bạn nghi ngờ, hãy thử biên dịch và bắt đầu lớp ở trên, nhưng hãy đảm bảo chọn kích thước heap khá (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

Bạn sẽ không gặp phải lỗi OOM ở đây, vì không có tài liệu tham khảo nào được lưu giữ, ứng dụng sẽ tiếp tục chạy cho dù bạn chọn ITERATION trong ví dụ trên lớn đến mức nào. Mức tiêu thụ bộ nhớ của quy trình của bạn (hiển thị ở trên cùng (RES / RSS) hoặc trình thám hiểm quy trình) tăng lên trừ khi ứng dụng nhận được lệnh chờ. Trong thiết lập ở trên, nó sẽ phân bổ khoảng 150 MB trong bộ nhớ.

Nếu bạn muốn ứng dụng phát an toàn, hãy đóng luồng đầu vào ngay tại nơi nó được tạo:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

và quá trình của bạn sẽ không vượt quá 35 MB, không phụ thuộc vào số lần lặp.

Khá đơn giản và đáng ngạc nhiên.


14

Như nhiều người đã đề xuất, Rò rỉ tài nguyên khá dễ gây ra - như các ví dụ về JDBC. Rò rỉ bộ nhớ thực tế khó hơn một chút - đặc biệt là nếu bạn không dựa vào các bit bị hỏng của JVM để làm điều đó cho bạn ...

Ý tưởng tạo ra các đối tượng có dấu chân rất lớn và sau đó không thể truy cập vào chúng cũng không bị rò rỉ bộ nhớ thực. Nếu không có gì có thể truy cập nó thì đó sẽ là rác được thu thập và nếu có thứ gì đó có thể truy cập thì đó không phải là rò rỉ ...

Một cách đã từng làm việc mặc dù - và tôi không biết liệu nó có còn hay không - là có một chuỗi vòng tròn ba sâu. Như trong Object A có tham chiếu đến Object B, Object B có tham chiếu đến Object C và Object C có tham chiếu đến Object A. GC đủ thông minh để biết rằng một chuỗi sâu hai - như trong A <-> B - có thể được thu thập một cách an toàn nếu A và B không thể truy cập được bằng bất cứ thứ gì khác, nhưng không thể xử lý chuỗi ba chiều ...


7
Đã không phải là trường hợp cho một thời gian bây giờ. Các GC hiện đại biết cách xử lý các tham chiếu tròn.
assylias

13

Một cách khác để tạo rò rỉ bộ nhớ tiềm năng rất lớn là giữ các tham chiếu đến Map.Entry<K,V>a TreeMap.

Thật khó để khẳng định tại sao điều này chỉ áp dụng cho TreeMaps, nhưng bằng cách xem xét việc thực hiện, lý do có thể là: một TreeMap.Entrycửa hàng tham chiếu đến anh chị em của nó, do đó, nếu một TreeMapđã sẵn sàng để được thu thập, nhưng một số lớp khác có tham chiếu đến bất kỳ của nó Map.Entry, thì toàn bộ Bản đồ sẽ được giữ lại vào bộ nhớ.


Kịch bản đời thực:

Hãy tưởng tượng có một truy vấn db trả về một TreeMapcấu trúc dữ liệu lớn . Mọi người thường sử dụng TreeMaps làm thứ tự chèn phần tử được giữ lại.

public static Map<String, Integer> pseudoQueryDatabase();

Nếu truy vấn được gọi nhiều lần và, đối với mỗi truy vấn (vì vậy, đối với mỗi truy vấn được Maptrả về), bạn lưu một Entrynơi nào đó, bộ nhớ sẽ liên tục tăng.

Hãy xem xét lớp bao bọc sau đây:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Ứng dụng:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Sau mỗi pseudoQueryDatabase()cuộc gọi, các mapphiên bản sẽ sẵn sàng để thu thập, nhưng điều đó sẽ không xảy ra, vì ít nhất một cuộc gọi Entryđược lưu trữ ở một nơi khác.

Tùy thuộc vào jvmcài đặt của bạn , ứng dụng có thể bị sập trong giai đoạn đầu do a OutOfMemoryError.

Bạn có thể thấy từ visualvmbiểu đồ này làm thế nào bộ nhớ tiếp tục phát triển.

Kết xuất bộ nhớ - TreeMap

Điều tương tự không xảy ra với cấu trúc dữ liệu băm ( HashMap).

Đây là biểu đồ khi sử dụng a HashMap.

Kết xuất bộ nhớ - HashMap

Giải pháp? Chỉ cần trực tiếp lưu khóa / giá trị (như bạn có thể đã làm) thay vì lưu Map.Entry.


Tôi đã viết một điểm chuẩn rộng rãi hơn ở đây .


11

Chủ đề không được thu thập cho đến khi họ chấm dứt. Họ phục vụ như là rễ của bộ sưu tập rác. Chúng là một trong số ít các đối tượng sẽ không được thu hồi chỉ bằng cách quên chúng hoặc xóa các tham chiếu đến chúng.

Xem xét: mẫu cơ bản để chấm dứt một luồng công nhân là để thiết lập một số biến điều kiện được nhìn thấy bởi luồng. Các luồng có thể kiểm tra biến định kỳ và sử dụng đó như là một tín hiệu để chấm dứt. Nếu biến không được khai báo volatile, thì sự thay đổi của biến có thể không được nhìn thấy bởi luồng, vì vậy nó sẽ không biết chấm dứt. Hoặc tưởng tượng nếu một số chủ đề muốn cập nhật một đối tượng được chia sẻ, nhưng bế tắc trong khi cố gắng khóa nó.

Nếu bạn chỉ có một số chủ đề thì những lỗi này có thể sẽ rõ ràng vì chương trình của bạn sẽ ngừng hoạt động bình thường. Nếu bạn có một nhóm luồng tạo ra nhiều luồng hơn khi cần, thì các luồng bị lỗi thời / bị kẹt có thể không được chú ý và sẽ tích lũy vô thời hạn, gây rò rỉ bộ nhớ. Chủ đề có khả năng sử dụng dữ liệu khác trong ứng dụng của bạn, do đó, cũng sẽ ngăn mọi nội dung mà họ trực tiếp tham khảo được thu thập.

Như một ví dụ về đồ chơi:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Gọi System.gc()tất cả những gì bạn thích, nhưng đối tượng được thông qua leakMesẽ không bao giờ chết.

(* đã chỉnh sửa *)


1
@Spidey Không có gì là "bị mắc kẹt". Phương thức gọi trở lại kịp thời và đối tượng được truyền sẽ không bao giờ được lấy lại. Đó chính xác là một rò rỉ.
Boann

1
Bạn sẽ có một chuỗi "chạy" (hoặc ngủ, bất cứ điều gì) trong suốt chương trình của bạn. Điều đó không được coi là một rò rỉ đối với tôi. Cũng như một hồ bơi không được tính là rò rỉ, ngay cả khi bạn không sử dụng nó hoàn toàn.
Spidey

1
@Spidey "Bạn sẽ có [điều] trong suốt chương trình của mình. Điều đó không được coi là rò rỉ đối với tôi." Bạn có nghe thấy mình không?
Boann

3
@Spidey Nếu bạn tính bộ nhớ mà quá trình biết là không bị rò rỉ, thì tất cả các câu trả lời ở đây đều sai, vì quy trình luôn theo dõi những trang nào trong không gian địa chỉ ảo của nó được ánh xạ. Khi quá trình kết thúc, HĐH sẽ dọn sạch tất cả các rò rỉ bằng cách đưa các trang trở lại vào ngăn xếp trang miễn phí. Để đưa điều đó đến cực điểm tiếp theo, người ta có thể đánh bại mọi rò rỉ tranh cãi bằng cách chỉ ra rằng không có bit vật lý nào trong chip RAM hoặc trong không gian trao đổi trên đĩa bị đặt sai vị trí hoặc bị phá hủy, vì vậy bạn có thể tắt máy tính và một lần nữa để làm sạch bất kỳ rò rỉ.
Boann

1
Định nghĩa thực tế của rò rỉ là bộ nhớ đã bị mất dấu mà chúng ta không biết và do đó không thể thực hiện quy trình cần thiết để chỉ lấy lại nó; chúng ta sẽ phải phá bỏ và xây dựng lại toàn bộ không gian bộ nhớ. Một chủ đề lừa đảo như thế này có thể phát sinh một cách tự nhiên thông qua việc thực hiện chủ đề bế tắc hoặc tinh ranh. Các đối tượng được tham chiếu bởi các luồng như vậy, thậm chí gián tiếp, hiện không được thu thập, vì vậy chúng tôi có bộ nhớ sẽ không được thu hồi hoặc tái sử dụng một cách tự nhiên trong suốt thời gian của chương trình. Tôi gọi đó là một vấn đề; cụ thể đó là rò rỉ bộ nhớ.
Boann

10

Tôi nghĩ rằng một ví dụ hợp lệ có thể đang sử dụng các biến ThreadLocal trong một môi trường nơi các luồng được gộp lại.

Chẳng hạn, sử dụng các biến ThreadLocal trong Servlets để giao tiếp với các thành phần web khác, có các luồng được tạo bởi bộ chứa và duy trì các biến không hoạt động trong một nhóm. Các biến ThreadLocal, nếu không được dọn sạch chính xác, sẽ tồn tại ở đó cho đến khi, có thể, cùng một thành phần web ghi đè lên các giá trị của chúng.

Tất nhiên, một khi được xác định, vấn đề có thể được giải quyết dễ dàng.


10

Người phỏng vấn có thể đang tìm kiếm một giải pháp tham khảo vòng tròn:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Đây là một vấn đề cổ điển với việc thu gom rác tham khảo. Sau đó, bạn sẽ giải thích một cách lịch sự rằng các JVM sử dụng thuật toán phức tạp hơn nhiều mà không có giới hạn này.

-Wes Tarle


12
Đây là một vấn đề cổ điển với việc thu gom rác tham khảo. Thậm chí 15 năm trước Java đã không sử dụng tính năng ref. Tham chiếu đếm cũng chậm hơn so với GC.
bestsss

4
Không bị rò rỉ bộ nhớ. Chỉ là một vòng lặp vô hạn.
Esben Skov Pedersen

2
@Esben Ở mỗi lần lặp lại, lần trước firstkhông hữu ích và nên được thu gom rác. Trong tham chiếu đếm người thu gom rác, đối tượng sẽ không được giải phóng vì có một tài liệu tham khảo tích cực về nó (bởi chính nó). Vòng lặp vô hạn ở đây để giải thích sự rò rỉ: khi bạn chạy chương trình, bộ nhớ sẽ tăng lên vô thời hạn.
rds

@rds @ Wesley Tarle cho rằng vòng lặp không phải là vô hạn. Vẫn sẽ có một rò rỉ bộ nhớ?
nz_21
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.