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ì?
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ì?
Câu trả lời:
Đâ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:
ClassLoader
.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.ClassLoader
nó đã được tải từ đó.Do cách ThreadLocal
được triển khai trong JDK của Oracle, điều này tạo ra rò rỉ bộ nhớ:
Thread
lĩ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ộ.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 đồ.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ụ → ThreadLocal
trường tĩnh →ThreadLocal
đối tượng.
(Việc ClassLoader
nà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à ClassLoader
s đượ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 .
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ư 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 .
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.
intern
nội dung có thể băm của nó . 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/iterned.html#GC
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.
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, 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.addShutdownHook
và 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 ContextClassLoader
và AccessControlContext
, cộng với ThreadGroup
và 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ó ThreadFactory
giao 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).
ThreadLocal
bộ 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/SoftReference
có thể giữ một tham chiếu cứng trở 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ừ (!). Điều này là đặc biệt, việc KeepAliveCache
tạ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 InflaterInputStream
chuyển new java.util.zip.Inflater()
trong hàm tạo ( PNGImageDecoder
ví 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, Deflater
khô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 Deflater
hoặ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!
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)
unstarted
số 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ỉ)
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.
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ế.
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" ...
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.
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?
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.
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
, Statement
và ResultSet
trườ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 finalize
phươ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 finalize
phươ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ư
List
trườ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ặcSocket
s hoặc File
s, 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 Connection
lớp).Connection.close
và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.
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 ...
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ó.
...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ệ.
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();
}
}
}
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.
Đâ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ỏ.
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.
intern
Trườ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.
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.
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.
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 đó.
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 ...
Rò rỉ bộ nhớ là gì:
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:
Đ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).
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.
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ớ.
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.
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ỉ:
malloc
. Đừng gọifree
.Hãy nhớ rằng, cấp phát bộ nhớ trong mã gốc đến từ heap JVM.
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); }
}
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());
}
}
}
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.
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à'.
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ớ.
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.
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 ...
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 TreeMap
s, nhưng bằng cách xem xét việc thực hiện, lý do có thể là: một TreeMap.Entry
cử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 TreeMap
cấu trúc dữ liệu lớn . Mọi người thường sử dụng TreeMap
s 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 Map
trả về), bạn lưu một Entry
nơ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 map
phiê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 jvm
cà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ừ visualvm
biểu đồ này làm thế nào bộ nhớ tiếp tục phát triển.
Đ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
.
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 .
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 leakMe
sẽ không bao giờ chết.
(* đã chỉnh sửa *)
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.
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
first
khô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.