Câu hỏi Tạo rò rỉ bộ nhớ bằng Java


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

Ví dụ sẽ là gì?


2641


gốc


Tôi sẽ nói với họ rằng Java sử dụng một bộ thu gom rác và yêu cầu họ cụ thể hơn về định nghĩa của họ về "rò rỉ bộ nhớ", giải thích rằng - chặn các lỗi JVM - Java không thể rò rỉ bộ nhớ trong khá giống như cách C / C ++ có thể. Bạn phải có một tham chiếu đến đối tượng một vài nơi. - Darien
Tôi thấy nó buồn cười rằng 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 cạnh và thủ thuật và dường như hoàn toàn mất tích điểm (IMO). Họ chỉ có thể hiển thị mã mà giữ tài liệu tham khảo vô dụng cho các đối tượng sẽ không bao giờ sử dụng một lần nữa, và đồng thời không bao giờ thả những tài liệu tham khảo; người ta có thể nói rằng những trường hợp đó không phải là rò rỉ bộ nhớ "đúng" vì vẫn có những tham chiếu đến những đối tượng xung quanh, nhưng nếu chương trình không bao giờ sử dụng những tham chiếu đó nữa và cũng không bao giờ thả chúng, nó hoàn toàn tương đương với (và xấu như) a " rò rỉ bộ nhớ thực sự ". - ehabkost
Thành thật mà nói, tôi không thể tin rằng câu hỏi tương tự mà tôi hỏi về "Go" đã giảm xuống còn -1. Đây: stackoverflow.com/questions/4400311/…   Về cơ bản các rò rỉ bộ nhớ tôi đã nói về là những người đã nhận được +200 upvotes OP và tôi đã bị tấn công và xúc phạm cho hỏi nếu "Go" có cùng một vấn đề. Bằng cách nào đó tôi không chắc chắn rằng tất cả các wiki-điều đang làm việc tuyệt vời. - SyntaxT3rr0r
@ SyntaxT3rr0r - câu trả lời của darien không phải là chủ nghĩa cuồng tín. anh ta đã thừa nhận rằng một số JVM nhất định có thể có các lỗi có nghĩa là bộ nhớ bị rò rỉ. điều này khác với bản thân ngôn ngữ cho phép rò rỉ bộ nhớ. - Peter Recore
@ehabkost: Không, chúng không tương đương. (1) Bạn có khả năng đòi lại bộ nhớ, trong khi trong một "rò rỉ thực sự" chương trình C / C ++ của bạn quên phạm vi được phân bổ, không có cách nào an toàn để phục hồi. (2) Bạn có thể dễ dàng phát hiện vấn đề với hồ sơ, bởi vì bạn có thể thấy những gì các đối tượng "sưng lên" liên quan đến. (3) Một "rò rỉ thực sự" là một lỗi rõ ràng, trong khi một chương trình giữ rất nhiều đối tượng xung quanh cho đến khi chấm dứt có thể là một phần có chủ ý về cách hoạt động của nó. - Darien


Các câu trả lời:


Đây là một cách tốt để tạo ra một rò rỉ bộ nhớ thực (các đối tượng không thể tiếp cận bằng cách chạy mã nhưng vẫn được lưu trữ trong bộ nhớ) trong Java tinh khiết:

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

Điều này làm việc vì ThreadLocal giữ một tham chiếu đến đối tượng, mà giữ một tham chiếu đến Class của nó, do đó giữ một tham chiếu đến ClassLoader của nó. ClassLoader, lần lượt, giữ một tham chiếu đến tất cả các lớp mà nó đã nạp.

(Nó đã tồi tệ hơn trong nhiều triển khai JVM, đặc biệt là trước khi Java 7, bởi vì Classes và ClassLoaders được cấp phát trực tiếp vào permgen và không bao giờ GC'd ở tất cả.Tuy nhiên, bất kể như thế nào JVM xử lý xếp dỡ lớp, ThreadLocal sẽ vẫn ngăn chặn một Đối tượng lớp bị lấy lại.)

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ớ giống như sàng nếu bạn thường xuyên triển khai lại các ứng dụng xảy ra khi sử dụng ThreadLocals theo bất kỳ cách nào. (Vì container ứng dụng sử dụng Threads như được mô tả, và mỗi khi bạn triển khai lại ứng dụng một ClassLoader mới được sử dụng.)

Cập nhật: Vì nhiều người 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.


1935



Lỗi rò rỉ ClassLoader +1 là một số rò rỉ bộ nhớ thường gặp nhất trong thế giới JEE, thường do các lib bên thứ 3 chuyển đổi dữ liệu (các bộ mã hóa BeanUtils, 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 giữ tham chiếu đến các lớp của bạn (ví dụ: bằng bộ nhớ đệ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ể thu thập rác của trình nạp lớp của ứng dụng (và do đó tất cả các lớp được nạp bởi nó), do đó, với việc triển khai lặp lại, máy chủ ứng dụng cuối cùng sẽ borks. Nếu may mắn bạn có được một đầu mối với ClassCastException z.x.y.Abc không thể được đúc thành z.x.y.Abc - earcam
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 nạp, tomcat có rất nhiều dataraces và mã hóa xấu mặc dù (cần phải nhận một số thời gian và gửi các bản sửa lỗi), cộng với ConcurrentLinkedQueue tất cả các mind-boggling như cache cho các đối tượng bên trong (nhỏ), quá nhỏ đến mức ConcurrentLinkedQueue.Node chiếm nhiều bộ nhớ hơn. - bestsss
+1: Rò rỉ bộ nạp là một cơn ác mộng. Tôi đã dành hàng tuần để tìm ra chúng. Điều đáng buồn là, như những gì @earcam đã nói, chúng chủ yếu là do libs của bên thứ 3 gây ra và hầu hết các trình lược tả không thể phát hiện 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. blogs.oracle.com/fkieviet/entry/… - Adrian M
@ Nicolas: Bạn có chắc chắn không? JRockit thực hiện các đối tượng GC Class theo mặc định, và HotSpot 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
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 mới nhất đôi khi thậm chí sẽ khắc phục sự cố rò rỉ cho bạn. - Matthijs Bierman


Trường tham chiếu trường giữ trường tĩnh [trường cuối cùng đặc biệt]

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

Đang 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();

(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 vùng không thể truy cập được từ bộ thu gom rác của JVM, chẳng hạn như bộ nhớ được cấp phát 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 được dừng hoặc xóa một cách rõ ràng.

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

Tùy chọn JVM không chính xác hoặc không phù hợp, chẳng hạn như noclassgc tù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.


1062



Tôi không đồng ý rằng ngữ cảnh và thuộc tính phiên là "rò rỉ". Chúng chỉ là các biến tồn tại lâu dài. Và trường cuối cùng tĩnh là nhiều hay ít chỉ là một hằng số. Có thể tránh được các hằng số lớn, nhưng tôi không nghĩ nó công bằng khi gọi nó là một lỗ hổng bộ nhớ. - Ian McLaird
(Các luồng mở) (tệp, mạng, v.v ...), không bị rò rỉ cho thực tế, trong quá trình hoàn thiện (sẽ được sau chu kỳ GC tiếp theo) close () sẽ được lên lịch (close() thường không được gọi trong chuỗi finalizer vì có thể là một hoạt động chặn). Đó là một thực tế xấu không phải để đóng, nhưng nó không gây ra một rò rỉ. Java.sql.Connection không rõ ràng là như nhau. - bestsss
Trong hầu hết các JVM sane, nó xuất hiện như thể lớp String chỉ có một tham chiếu yếu trên intern nội dung có thể bắt đầu. Như vậy, nó Là rác được thu gom đúng cách và không bị rò rỉ. (nhưng IANAJP) mindprod.com/jgloss/interned.html#GC - Matt B.
Làm thế nào trường tĩnh giữ đối tượng tham chiếu [esp cuối cùng lĩnh vực] là một rò rỉ bộ nhớ ?? - Kanagavelu Sugumar
@cao thật. Mối 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ể bị rò rỉ rất nhiều tay cầm, nhưng vẫn còn nhiều bộ nhớ. Sau đó, Garbage Collector có thể quyết định không bận tâm đến việc thực hiện một bộ sưu tập đầy đủ vì nó vẫn còn rất nhiều bộ nhớ. Điều này có nghĩa là finalizer không được gọi, vì vậy bạn chạy ra khỏi tay cầm. Vấn đề là, finalizers sẽ (thường) được chạy trước khi bạn hết bộ nhớ từ rò rỉ suối, nhưng nó có thể không được gọi trước khi bạn chạy ra khỏi một cái gì đó khác hơn là bộ nhớ. - Patrick M


Một điều đơn giản cần làm là sử dụng một HashSet với một không chính xác (hoặc không tồn tại) hashCode() hoặc là equals()và sau đó tiếp tục thêm "trùng lặp". Thay vì bỏ qua các bản trùng lặp, tập hợp 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 phím / yếu tố xấu này bị 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.

390



Trên thực tế, bạn có thể loại bỏ các phần tử từ một HashSet ngay cả khi lớp phần tử nhận được hashCode và bằng sai; chỉ nhận được 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 các mục bên dưới và không phải là các phần tử. (Lưu ý rằng hashCode / equals không được thực hiện là không phải đủ để kích hoạt rò rỉ; các giá trị mặc định thực hiện nhận dạng đối tượng đơn giản và vì vậy bạn có thể lấy các phần tử và xóa chúng một cách bình thường.) - Donal Fellows
@ Nói chung những gì tôi đang cố gắng để nói, tôi đoán, là tôi không đồng ý với định nghĩa của bạn về một 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 nhỏ giọt-chảo dưới một rò rỉ; rò rỉ vẫn tồn tại bất kể chảo nhỏ giọt. - corsiKa
Tôi đồng ý, đây là không phải một bộ nhớ "rò rỉ", bởi vì bạn chỉ có thể loại bỏ các tham chiếu đến hashset và đợi cho GC khởi động, và mau! bộ nhớ quay trở lại. - Mehrdad
@ SyntaxT3rr0r, tôi đã giải thích câu hỏi của bạn khi yêu cầu nếu có bất cứ điều gì trong ngôn ngữ mà tự nhiên dẫn đến rò rỉ bộ nhớ. Câu trả lời là không. Nhữ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 một 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 C / C ++ sẽ hiểu nó. - Peter Lawrey
@ Peter Lawrey: ngoài ra, bạn nghĩ gì về điều này: "Không có bất cứ điều gì trong ngôn ngữ C tự nhiên rò rỉ vào bộ nhớ bị rò rỉ nếu bạn không quên tự giải phóng bộ nhớ bạn đã phân bổ". Làm thế nào mà có thể cho sự không trung thực về trí tuệ? Dù sao, tôi mệt mỏi: bạn có thể có từ cuối cùng. - SyntaxT3rr0r


Dưới đây sẽ là một trường hợp không rõ ràng nơi Java bị rò rỉ, ngoài trường hợp tiêu chuẩn của người nghe bị lãng quên, tham chiếu tĩnh, các khóa không có khả năng / có thể sửa đổi trong hashmaps, hoặc chỉ các chủ đề 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 luôn rò rỉ chuỗi, nếu chuỗi là chuỗi con, thì sự rò rỉ thậm chí tệ hơn (char cơ bản [] 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, không có nhu cầu cho phiếu bầu, mặc dù.

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

  • Runtime.addShutdownHook và không loại bỏ ... và sau đó ngay cả với removeShutdownHook do lỗi trong lớp ThreadGroup liên quan đến các chủ đề chưa được khởi động, nó có thể không được thu thập, làm rò rỉ hiệu quả ThreadGroup. JGroup có sự rò rỉ trong GossipRouter.

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

  • Tạo một chuỗi kế thừa ContextClassLoader và AccessControlContext, cộng với ThreadGroup và bất kỳ InheritedThreadLocal, tất cả các tham chiếu đó đề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 hiển thị với toàn bộ khung công tác j.u.c.Executor có tính năng siêu đơn giản ThreadFactory giao diện, nhưng hầu hết các nhà phát triển không có đầu mối nguy hiểm rình rập. Ngoài ra rất nhiều thư viện bắt đầu đề theo yêu cầu (quá nhiều thư viện phổ biến trong ngành).

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

  • Đang gọi ThreadGroup.destroy() khi ThreadGroup không có chủ đề riêng của nó, nhưng nó vẫn giữ các ThreadGroup con. Một rò rỉ xấu sẽ ngăn chặn ThreadGroup để loại bỏ từ cha mẹ của nó, nhưng tất cả các trẻ em trở thành un-enumerateable.

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

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

  • Sử dụng InflaterInputStream đi qua new java.util.zip.Inflater() trong hàm tạo (PNGImageDecoder ví dụ) và không gọi end() của inflater. Vâng, nếu bạn vượt qua trong các nhà xây dựng chỉ với new, không có cơ hội ... Và vâng, gọi close() trên luồng không đóng inflater nếu nó được truyền theo cách thủ công như tham số hàm tạo. Đây không phải là một sự rò rỉ thực sự vì nó sẽ được phát hành bởi finalizer ... khi nó xét thấy nó cần thiết. Cho đến thời điểm đó nó ăn bộ nhớ riêng rất tệ, nó có thể làm cho Linux oom_killer giết chết quá trình đó với sự miễn cưỡng. Vấn đề chính là việc hoàn thành trong Java là rất không đáng tin cậy và G1 đã làm cho 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 gốc ngay khi có thể; các finalizer chỉ là quá nghèo.

  • Trường hợp tương tự với java.util.zip.Deflater. Điều này là tồi tệ hơn kể từ khi Deflater là bộ nhớ đói trong Java, tức là luôn 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, Deflater không được sử dụng rộng rãi và theo hiểu biết của tôi, JDK không có sự lạm dụng. Luôn gọi end() nếu bạn tạo thủ công Deflater hoặc là 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ụ lược tả thông thường có sẵn.

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

Chúc may mắn và an toàn; rò rỉ là ác!


228



Creating but not starting a Thread... Yikes, tôi đã bị cắn bởi một vài thế kỷ trước đây. (Java 1.3) - leonbloy
@leonbloy, trước khi nó thậm chí còn tồi tệ hơn khi các chủ đề đã được thêm thẳng vào threadgroup, không bắt đầu có nghĩa là rò rỉ rất khó. Nó không chỉ làm tăng unstarted đếm nhưng điều đó ngăn chặn các nhóm chủ đề phá hủy (ít ác hơn nhưng vẫn là một rò rỉ) - bestsss
Cảm ơn bạn! "Đang gọi ThreadGroup.destroy() khi ThreadGroup không có chủ đề riêng ... " là một lỗi cực kỳ tinh tế; Tôi đã theo đuổi điều này trong nhiều giờ, dẫn đầu lạc lối vì liệt kê luồng trong GUI điều khiển của tôi không hiển thị gì, nhưng nhóm luồng và, có lẽ, ít nhất một nhóm con sẽ không biến mất. - Lawrence Dol
@bestsss: Tôi tò mò, tại sao bạn muốn loại bỏ một móc tắt máy, cho rằng nó chạy ở, tốt, JVM tắt máy? - Lawrence Dol


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 những ví dụ này, lập trình viên đã thực hiện một sai lầm (như không định nghĩa lại bằng / hashcode), hoặc đã bị cắn bởi một trường hợp góc của JVM / JAVA (tải lớp có static ...). Tôi nghĩ đó không phải là loại ví dụ mà một 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ớ. Bộ thu gom rác chỉ giải phóng những gì không còn được tham chiếu nữa. Chúng tôi là 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 nào tồn tại lâu dài đều có xu hướng có trạng thái chia sẻ. Nó có thể là bất cứ điều gì, thống kê, singletons ... Thường thì các ứng dụng không tầm thường có xu hướng tạo ra các đồ vật phức tạp. Chỉ cần quên để đặt một tham chiếu đến null hoặc thường xuyên quên để loại bỏ một đối tượng từ một bộ sưu tập là đủ để làm cho một rò rỉ bộ nhớ.

Tất nhiên tất cả các loại người nghe (như người nghe UI), bộ nhớ cache hoặc bất kỳ trạng thái chia sẻ lâu dài nào đều có xu hướng phát sinh rò rỉ bộ nhớ nếu không được xử lý đúng cách. Điều cần được hiểu là đây không phải là trường hợp góc Java, hoặc có vấn đề với bộ thu gom rác. Đó 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, nhưng chúng tôi không loại bỏ người nghe khi không còn cần thiết nữa. 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 để xóa chúng khỏi bộ nhớ cache.

Chúng tôi có thể có một biểu đồ phức tạp lưu trữ trạng thái trước đó là cần thiết bởi một tính toán. Nhưng trạng thái trước đó chính nó liên quan đến trạng thái trước đây và vân vân.

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 thiết lập các tham chiếu thích hợp thành null và loại bỏ các phần tử khỏi bộ sưu tập. Chúng tôi sẽ có các chiến lược bộ nhớ đệm thích hợp (kích thước bộ nhớ tối đa, số phần tử hoặc bộ tính giờ). 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 trình thông báo này không còn được sử dụng, chúng phải xóa danh sách người nghe của họ.

Một rò rỉ bộ nhớ thực sự là có thể thực sự 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 các trường hợp góc. Rò rỉ bộ nhớ là một chỉ báo cho thấy có thể có điều gì đó thiếu hoặc thậm chí là vấn đề về thiết kế.


150



Tôi thấy nó buồn cười rằng trên những câu trả lời khác, mọi người đang tìm kiếm những trường hợp và thủ thuật cạnh đó và dường như hoàn toàn thiếu điểm. Họ chỉ có thể hiển thị mã giữ các tham chiếu vô dụng đối với 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ớ "đúng" vì vẫn có những tham chiếu đến những đối tượng xung quanh, nhưng nếu chương trình không bao giờ sử dụng những tham chiếu đó nữa và cũng không bao giờ thả chúng, nó hoàn toàn tương đương với (và xấu như) a " rò rỉ bộ nhớ thực sự ". - ehabkost
@Nicolas Bousquet: "Rò rỉ bộ nhớ thực sự là kẻ bắt nạt"   Cảm ơn bạn rất nhiều. +15 upvotes. Tốt đẹp. Tôi đã bị mắng ở đây vì đã nói rằng thực tế, là cơ sở 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ó downvotes tiêu cực :( - SyntaxT3rr0r
GC trong Java và .NET theo một nghĩa nào đó được xác định trên đồ thị giả định của các đối tượng chứa tham chiếu đến các đối tượng khác giống như đồ thị của các đối tượng "quan tâm" các đối tượng khác. Trong thực tế, có thể các cạnh có thể tồn tại trong đồ thị tham chiếu không đại diện cho quan hệ "quan tâm", và có thể đối tượng quan tâm đến sự tồn tại của một đố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 WeakReference) tồn tại từ cái này sang cái khác. Nếu một tham chiếu đối tượng có một bit dự phòng, nó có thể hữu ích để có một chỉ số "quan tâm đến mục tiêu" ... - supercat
... và có 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 một chút gần, nhưng phải được chuyển đổi thành một tham chiếu mạnh trước khi nó có thể được sử dụng; nếu chu trình GC xảy ra trong khi tham chiếu mạnh tồn tại, thì đích sẽ được giả định là hữu ích. - supercat
Đây là ý kiến ​​của tôi về câu trả lời đúng. Chúng tôi đã viết một năm mô phỏng trước đây. Bằng cách nào đó, chúng tôi 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 mà chúng tôi chưa bao giờ giải quyết rò rỉ bộ nhớ nhưng đã làm cho nó trở thành «tính năng» bằng cách ghi lại nó. - nalply


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

Có thể thực hành để làm rò rỉ Java không? Tất nhiên nó là, 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?

  • Việc thực thi Java "hoàn hảo" về mặt lý thuyết có dễ bị rò rỉ không?
  • Ứng cử viên có hiểu sự khác biệt giữa lý thuyết và thực tế không?
  • Ứng cử viên có hiểu cách thu gom rác thải hoạt động không?
  • Hay cách thu gom rác thải được cho là hoạt động 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 gốc không?
  • Họ có biết làm rò rỉ bộ nhớ bằng những ngôn ngữ khác không?
  • Ứng cử viên thậm chí còn biết quản lý bộ nhớ là gì không, và điều gì đang diễn ra đằng sau cảnh trong Java?

Tôi đang đọc câu hỏi meta của bạn là "Câu trả lời tôi có thể sử dụng trong tình huống phỏng vấn này là gì". 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 hình 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 ở trong một nơi cần biết làm thế nào để 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 mà bạn có thể phát triển để phỏng vấn là học cách tích cực lắng nghe các câu hỏi và làm việc với người phỏng vấn để trích xuất ý đị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 nói đến sự lựa chọn giữa nhiều nhà phát triển tài năng, tôi sẽ thuê người lắng nghe, suy nghĩ và hiểu trước khi họ trả lời mọi lúc.


133



Bất cứ khi nào tôi hỏi câu hỏi đó, tôi đang tìm kiếm một câu trả lời khá đơn giản - tiếp tục phát triển hàng đợi, không cuối cùng đóng db vv, không phải chi tiết về trình nạp lớp / chuỗi lẻ, ngụ ý họ hiểu gc có thể và không thể làm gì cho bạn. Phụ thuộc vào công việc bạn đang phỏng vấn cho tôi đoán. - DaveC
Vui lòng xem câu hỏi của tôi, cảm ơn stackoverflow.com/questions/31108772/… - Daniel Newtown


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à cách JDBC mong đợi một nhà phát triển đóng Connection, Statement và ResultSet các trường hợp trước khi loại bỏ chúng hoặc mất tham chiếu đến chúng, thay vì dựa vào việc triển khai 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 đề ở trên là Connection đối tượng không bị đóng, và do đó kết nối vật lý sẽ vẫn mở, cho đến khi người thu gom rác đi xung quanh và thấy rằng nó không thể truy cập được. GC sẽ gọi finalize 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 thi. Hành vi kết quả là trong khi bộ nhớ sẽ được khai hoang do các đối tượng không thể truy cập được thu thập, tài nguyên (bao gồm bộ nhớ) được liên kết với Connection đối tượng có thể chỉ đơn giản là không được khai hoang.

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

Ngay cả khi trình điều khiển JDBC đã triển khai finalize, có thể cho các trường hợp ngoại lệ được ném trong khi hoàn tất. Hành vi kết quả là bất kỳ bộ nhớ nào được kết hợp với đối tượng "không hoạt động" hiện tại sẽ không được khôi phục, vì finalize được bảo đảm 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 xử lý đố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ớ - sự phục hồi đối tượng. Phục hồi đối tượng thường được thực hiện một cách có chủ ý bằng cách tạo một tham chiếu mạnh mẽ đến đối tượng đang được hoàn thành, từ một đối tượng khác. Khi đối tượng phục sinh 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ó rất nhiều ví dụ khác mà bạn có thể gợi lên - như

  • Quản lý một List trường hợp 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 nên loại bỏ các yếu tố bạn không còn cần), hoặc
  • Khai mạc Sockets hoặc Files, nhưng không đóng chúng khi chúng không còn cần thiết (tương tự như ví dụ trên liên quan đến Connection lớp học).
  • Không dỡ bỏ Singletons khi đưa xuống một ứng dụng Java EE. Rõ ràng, Classloader nạp lớp singleton sẽ giữ lại một tham chiếu đến lớp, và do đó trường hợp singleton sẽ không bao giờ được thu thập. Khi một cá thể 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.

113



Bạn sẽ đạt đến giới hạn kết nối mở tối đa trước khi bạn đạt đến giới hạn bộ nhớ thường. Đừng hỏi tôi tại sao tôi biết ... - Hardwareguy
Trình điều khiển JDBC Oracle rất nổi tiếng khi thực hiện điều này. - chotchki
@ Hardwareguy Tôi nhấ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 đặt Connection.close vào khối cuối cùng của tất cả các cuộc gọi SQL của tôi. Để có thêm niềm vui, tôi đã gọi một số quy trình lưu trữ Oracle đang chạy dài yêu cầu khóa ở phía Java để ngăn chặn quá nhiều cuộc gọi đến cơ sở dữ liệu. - Michael Shopsin
@Hardwareguy Thật thú vị nhưng nó không thực sự cần thiết mà giới hạn kết nối thực tế sẽ được nhấn 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 các kết nối cơ sở dữ liệu bị rò rỉ vẫn có sẵn trong khi rò rỉ bộ nhớ đã được giới thiệu. Tôi không chắc chắn về mọi môi trường. - Aseem Bansal
Theo kinh nghiệm của tôi, bạn sẽ bị rò rỉ bộ nhớ ngay cả khi bạn đóng kết nối. Trước tiên, bạn cần đóng ResultSet và PreparedStatement. Đã có một máy chủ bị hỏng nhiều lần sau nhiều giờ hoặc thậm chí cả ngày làm việc tốt, do OutOfMemoryErrors, cho đến khi tôi bắt đầu làm điều đó. - Bjørn Stenfeldt


Có lẽ một trong những ví dụ đơn giản nhất về rò rỉ bộ nhớ tiềm ẩn và cách tránh rò rỉ bộ nhớ, là việc triển khai thực hiện 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 đã tự mình thực hiện nó, bạn có nghĩ rằng để xóa phần tử mảng không còn được sử dụng nữa (elementData[--size] = null)? Tham chiếu đó có thể giữ một vật thể khổng lồ còn sống ...


102



'elementData [- size] = null;' không, tôi nghĩ ... - maniek
Và rò rỉ bộ nhớ ở đâu? - rds
@maniek: Tôi không ngụ ý rằng mã này thể hiện rò rỉ bộ nhớ. Tôi trích dẫn nó để cho thấy rằng đôi khi mã không rõ ràng là cần thiết để tránh lưu giữ đối tượng ngẫu nhiên. - meriton
RangeCheck (chỉ mục) là gì; ? - Koray Tugay
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. - rents


Bất cứ lúc nào bạn giữ tài liệu tham khảo xung quanh đố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 cho các ví dụ về cách rò rỉ bộ nhớ tự biểu hiện trong Java và những gì bạn có thể làm về nó.


63



Tôi không tin đây là một "rò rỉ". Đó là một lỗi, và đó là do thiết kế của chương trình và ngôn ngữ. Một rò rỉ sẽ là một vật thể treo quanh không có bất kỳ tham chiếu đến nó. - Mehrdad
@ Mehrdad: Đó chỉ là một định nghĩa hẹp mà 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ớ là một lỗi gây ra bởi thiết kế kém của chương trình. - Bill the Lizard
@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 làm thế nào bạn đang vẽ kết luận đó. Có ít hơn cách để tạo rò rỉ bộ nhớ trong Java theo bất kỳ định nghĩa nào. Nó chắc chắn vẫn là một câu hỏi hợp lệ. - Bill the Lizard
@ 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 the Lizard
@ 31eee384: Nếu bạn biết một thực tế rằng nó sẽ không được, thì nó không thể được. Chương trình, như được viết, sẽ không bao giờ truy cập dữ liệu. - Bill the Lizard


Bạn có thể làm rò rỉ bộ nhớ bằng sun.misc.Unsafe lớp học. 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ụ: java.nio các lớp học). 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 khi 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();
        }
    }

}

43



Bộ nhớ phân bổ là vô hình cho bộ thu gom rác - stemm
Bộ nhớ được cấp phát không thuộc về Java. - bestsss
Sun / oracle jvm có cụ thể không? Ví dụ. điều này sẽ làm việc trên IBM? - Berlin Brown
Bộ nhớ chắc chắn "thuộc về Java", ít nhất theo nghĩa là tôi) nó không có sẵn cho bất kỳ ai khác, ii) khi ứng dụng Java thoát nó sẽ được trả về hệ thống. Nó nằm ngay bên ngoài JVM. - Michael Anderson
Đ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 cần phải thay đổi cài đặt trình biên dịch: trong Window> Preferences> Java> Compiler> Errors / Warning> Bộ API bị hạn chế và bị hạn chế Tham chiếu bị cấm (quy tắc truy cập) thành "Warning ". - Michael Anderson