内存是开发人员拥有的最宝贵的资源之一。因此,内存效率是你将要编写的任何程序的核心。当一个程序在运行时尽可能少地使用内存,而仍在执行它的设计任务时,可以说它是内存高效的。
什么是内存泄漏?
当应用程序不再使用对象,但垃圾回收器(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("http://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中的某些对象在任何特定点所占用的空间。它们对于了解应用程序生成的对象数非常有用。
小结
对于大多数开发人员来说,内存泄漏是一个相关的问题,不应掉以轻心。如果它们出现在生产环境中,则很难检测到,甚至更难解决,最终导致致命的应用程序崩溃。然而,遵循诸如编写测试、代码评审和评测之类的最佳实践可以帮助将应用程序中内存丢失的可能性降到最低。
休息一下^__^
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/952.html
暂无评论