內存是開發人員擁有的最寶貴的資源之一。因此,內存效率是你將要編寫的任何程序的核心。當一個程序在運行時儘可能少地使用內存,而仍在執行它的設計任務時,可以說它是內存高效的。
什麼是內存泄漏?
當應用程式不再使用對象,但垃圾回收器(GC)無法從工作內存中清除對象時,就會發生內存泄漏。這是有問題的,因為這些對象占用的內存本來可以被程序的其他部分使用。隨著時間的推移,這種情況會逐漸累積並導致系統性能的下降。
Java中的垃圾回收
Java的流行在很大程度上歸功於它的自動內存管理。GC是一個隱式處理內存分配和釋放的程序。這是一個非常漂亮的程序,可以處理大多數可能發生的內存泄漏。然而,這並不是萬無一失的。內存泄漏仍然會悄悄地出現在毫無防備的開發人員身上,占用寶貴的資源,在極端情況下,會導致可怕的後果java.lang.OutOfMemoryError. (需要注意的是,OutOfMemoryError不一定是因為內存泄漏。有時,這只是糟糕的代碼實踐,比如在內存中加載大文件)。
RAM內存的價格在2019年創下歷史新低,並在過去10年左右逐漸走低。很多開發人員都有一種奢侈的享受,那就是永遠不必處理內存不足的問題,但這並沒有使問題變得不那麼明顯。
Android開發人員特別容易出現內存不足的情況,因為移動設備的RAM訪問量遠遠低於PC。大多數現代手機使用LPDDR3(低功耗雙數據速率3)RAM,而你在大多數PC機上都會找到DDR3和DDR4組件。換句話說,雖然8GB的RAM對於一部手機來說是相當寬裕的,但它並不如你在PC機上得到的那樣強大。
為了不失專注,更強大的RAM並不能完全解決問題。當GC在試圖清理未引用的對象時,內存泄漏的應用程式將面臨嚴重的性能問題。如果應用程式變得太大,性能將因交換而嚴重下降或被系統殺死。
任何Java開發人員都應該關注內存泄漏問題。本文將探討它們的原因、如何識別它們以及如何在應用程式中處理它們。雖然在移動設備上處理更多受限內存會帶來很多複雜的問題,但是本文將探討內存泄漏以及如何在java se(我們大多數人都習慣使用的普通Java)中處理它們。
什麼導致內存泄漏?
內存泄漏可能是由很多令人眩暈的事情引起的。三個最常見的原因是:
- 誤用static欄位
- 未關閉streams流
- 未關閉connections連接
1. 誤用Static欄位
只要擁有靜態欄位的類被加載到JVM中,靜態欄位就會存在於內存中,也就是說,當JVM中沒有該類的實例時。此時,類將被卸載,靜態欄位將被標記為垃圾回收。抓住了嗎?靜態類基本上可以永遠存在於內存中。
考慮以下代碼:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Logger;
public class Main {
public List<Integer> list = new ArrayList<>();
public void populateList() {
Logger.getGlobal().info("Debug Point 2");
for (int i = 0; i < 10000000; i++) {
list.add(new Random().nextInt());
}
Logger.getGlobal().info("Debug Point 3");
}
public static void main(String[] args) {
Logger.getGlobal().info("Debug Point 1");
new Main().populateList();
Logger.getGlobal().info("Debug Point 4");
try {
System.gc();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
就程序而言,這並不令人印象深刻:我們創建了一個具有公共ArrayList的新類。然後我們用一百萬條記錄填充這個ArrayList。使用我們方便的開源Java評測工具VisualVM,我們在運行後得到以下圖形:
在1秒的時候,堆大小會隨著JVM為程序分配大約417MB的內存而增加,在額外的一秒鐘內,JVM會清除內存中的所有內容。使用的和分配的內存都會關閉,程序也會關閉。
我們將其與之前調整為使ArrayList靜態的代碼進行比較:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Logger;
public class Main {
public static List<Integer> list = new ArrayList<>();
public void populateList() {
Logger.getGlobal().info("Debug Point 2");
for (int i = 0; i < 10000000; i++) {
list.add(new Random().nextInt());
}
Logger.getGlobal().info("Debug Point 3");
}
public static void main(String[] args) {
Logger.getGlobal().info("Debug Point 1");
new Main().populateList();
Logger.getGlobal().info("Debug Point 4");
try {
System.gc();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
這一次,堆大小增加到大約390MB,在程序運行完成後(在略微向下傾斜的點上),使用的內存在程序結束前保持停滯。
如何避免這個錯誤:儘量減少應用程式中靜態欄位的使用。
2. 未關閉Streams流
在本文的上下文中,內存泄漏被定義為當代碼持有對對象的引用,從而垃圾回收器無法對此進行任何操作時發生的。根據這個定義,封閉流並不完全是「內存泄漏」(除非你有一個未關閉的引用)。
然而,大多數作業系統都會限制一個應用程式一次可以打開多少個文件(在FileInputStream的情況下)。如果這樣一個流沒有被關閉,在GC意識到這些流需要關閉之前可能需要相當長的時間,因此這是一個泄漏,而不是內存泄漏本身。
一個更引人注目的例子是使用URLConnection(或類似的類)加載大型對象。
如果不同時關閉FileInputStream和ReadableByteChannel,下面的代碼將導致潛在的問題。
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
public class URLeak {
public static void main(String[] args) throws IOException {
URL url = new URL("https://raw.githubusercontent.com/zemirco/sf-city-lots-json/master/citylots.json");
ReadableByteChannel rbc = Channels.newChannel(url.openStream());
FileOutputStream outputStream = new FileOutputStream("/");
outputStream.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
}
}
簡單的解決方法是關閉流。
outputStream.close();
rbc.close();
3. 未關閉Connections連接
未關閉的資料庫連接是一個很難調試的問題。正如標題中所反映的那樣,我在實現我的網站的一些功能時,從中吸取了教訓。每次有交通堵塞,什麼都不會發生。沒有錯誤,沒有異常,也沒有崩潰,但是伺服器在每次請求時都會超時。更奇怪的是,一旦請求數量減少,神秘的錯誤也隨之減少。
稍微挖掘一下就會發現,在這種情況下,所有涉及資料庫的進程都被排隊,但從未被處理過。資料庫有問題。
最終,我們遇到了一堆亂七八糟的代碼:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class DBLeak {
public static void main(String[] args) throws SQLException {
Connection connection = JDBCHelper.getConnection();
PreparedStatement stmt = null;
try {
stmt = connection.prepareStatement("SELECT ...");
} catch (SQLException e) {
e.printStackTrace();
} finally {
// Release the statement
stmt.close();
// Notice how the connection is never closed. Easy to miss.
}
}
}
同樣的例子可能更糟:
import java.sql.*;
public class DBLeak1 {
public static void main(String[] args) {
try {
String realName = getRealNameFromDatabase("aFriskyWaterMelon", "team90%waterFortheWin");
System.out.println(realName);
} catch (SQLException e) {
e.printStackTrace();
}
}
public static String getRealNameFromDatabase(String username, String password) throws SQLException {
Connection con = DriverManager.getConnection("jdbc:myDriver:devDB",
username,
password);
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT first_name, last_name FROM users");
String firstName = "";
String lastName = "";
while (rs.next()) {
firstName = rs.getString("first_name");
lastName = rs.getString("last_name");
}
return firstName + " " + lastName;
}
}
在這種情況下,每個資源都會泄漏。
幸運的是,有幾種方法可以解決此問題:
- 使用ORM:ORM負責為您打開和關閉任何資源。對於SQL,最流行的選項之一是Hibernate。
- 自動資源管理是一個期待已久的特性,它最終在Java8中引入。它也被稱為try-with-resources
- 使用jOOQ:jOOQ並不完全是一個ORM,但它確實為您自動管理所有資料庫資源。
如何檢測內存泄漏
使用分析器
探測器是一種工具,它允許您監視JVM的不同方面,包括線程執行和垃圾回收。如果您想比較不同的方法,並找出哪種方法在諸如內存分配之類的功能方面最有效,那麼這很有用。
在本教程中,我們使用了VisualVM,但是如果VisualVM不適合您的需要,那麼諸如任務控制、Netbeans Profiler和JProfiler等工具都是可用的。
使用Heap Dumps堆轉儲
如果您不想學習如何使用其他新工具,那麼堆轉儲可能會有所幫助。堆轉儲是JVM內存中任何一個實例中所有對象的快照。堆轉儲允許您查看JVM中的某些對象在任何特定點所占用的空間。它們對於了解應用程式生成的對象數非常有用。
小結
對於大多數開發人員來說,內存泄漏是一個相關的問題,不應掉以輕心。如果它們出現在生產環境中,則很難檢測到,甚至更難解決,最終導致致命的應用程式崩潰。然而,遵循諸如編寫測試、代碼評審和評測之類的最佳實踐可以幫助將應用程式中內存丟失的可能性降到最低。
休息一下^__^
原文連結:http://javakk.com/952.html
如果覺得本文對你有幫助,可以轉發關注支持一下