java使用getinputstream_如何避免Java应用中的内存泄露

a2e7b568b0499bcd647f1679f715217f.png

背景

最近同事做的分词器项目遇到Memory Leak问题,应用会把Jieba的C++版本作为so库提供给一个ES的分词器wrapper使用,以避免Java和C++版本Jieba在分词上的差异。上线了两天,碰到了问题,让我帮忙看下。我查了一下相关的Java文档,有几篇极有价值,摘录和翻译于此。有需要的同学自取。

什么是Java内存泄露

Java相较于其他语言、最核心的改进就是具有内存管理功能的虚拟机JVM。Java的垃圾回收器(GC)会帮助我们进行内存的分配和释放。

但是,这并不意味着Java应用中不会发生内存泄露。

在这篇文章中,我们将描述最通用类型的内存泄露,分析原因并看看怎样避免和检测泄露的发生。

1. Java内存泄露具体指什么?

内存泄露是指对象不再被应用使用,但是GC却不能将其从内存中释放。这往往是因为对象本身还被引用(referenced)。随着越来越多的内存被占用,程序最终导致严重的OOM错误(OutOfMemoryError)。

请看下面的图表

5c29fe56247537ccbe472639e793c4c6.png

这里有两种对象类型--referenced和unreferenced. GC只能回收unreferenced的对象。而referenced的对象几遍应用不再使用,GC也不能回收。

检测泄露不容易,通常常用动态检测的方式,下面我们看看一些内存泄露的通用场景。

2. Java堆泄露

我们首先看看经典的内存泄露场景--对象被连续创建但是没有释放。

通过把堆内存的大小设置得小一些可以帮助更快的复现问题。

-Xms<size>
-Xmx<size>

参数分别指定堆的最小和最大大小。

2.1 静态的域成员持有对象引用

看下面的例子

private Random random = new Random();
public static final ArrayList<Double> list = new ArrayList<Double>(1000000);

@Test
public void givenStaticField_whenLotsOfOperations_thenMemoryLeak() throws InterruptedException {
    for (int i = 0; i < 1000000; i++) {
        list.add(random.nextDouble());
    }
    
    System.gc();
    Thread.sleep(10000); // to allow GC do its job
}

我们创建了一个静态的ArrayList. 这个静态数组在JVM的生命周期中不会被释放,哪怕计算操作已经全部完成我们调用了一个Thread.sleep(10000) 来让GC有机会进行全部的回收操作。

让我们用profiler测试并分析下JVM:

b5f381876bc1cdda9cf08bb698bdea63.png

注意到第2秒的时候,循环过程运行结束,所有的东西都加载到list中了。

然后,GC被触发,接着,你可以看到,list并没有被回收,内存消耗也没有降低。从图中可以看出,分配的资源是61MB,Eden中的占用了6.5MB,Survivor空间中是2.5MB,Old Generation是21MB。

下面我们把静态的ArrayList换成局部变量,看看结果如何:

public void givenNormalField_whenLotsOfOperations_thenGCWorksFine() throws InterruptedException {
    addElementsToTheList();
    System.gc();
    Thread.sleep(10000); // to allow GC do its job
}
    
private void addElementsToTheList(){
    ArrayList<Double> list = new ArrayList<Double>(1000000);
    for (int i = 0; i < 1000000; i++) {
        list.add(random.nextDouble());
    }
}

这次,我们再观察GC的回收动作

4a493b93125a480e6ebd5fca0313bdb3.png

在第50秒钟,可以看到大量的内存被回收。注意观察Eden部分和Old Generation部分。

如何避免?

首先,我们需要特别注意使用static,声明集合或者重型的对象为static,将把它的生命周期跟JVM本身的声明周期绑定在一起,这回使得对象本身无法被回收。

2.2. 调用String.intern()

第二种频繁引起内存泄露的场景就是String操作--具体就是String.intern()调用。

看下面的例子

public void givenLengthString_whenIntern_thenOutOfMemory()
  throws IOException, InterruptedException {
    Thread.sleep(15000);
    
    String str 
      = new Scanner(new File("src/test/resources/large.txt"), "UTF-8")
      .useDelimiter("A").next();
    str.intern();
    
    System.gc(); 
    Thread.sleep(15000);
}

这里,我们将一段文本内容存入到内存中,然后我们使用.intern()方法将它从heap栈区放到常量池中。

intern调用将把str字符串放到JVM的常量内存池,在这个位置的对象,GC无法进行释放。

afcff8be55a65b1fbd0fa396b474f4f0.png

从这张图中我们可以看到15秒后内存趋于稳定。最终导致高内存使用,而无法释放。

如何避免?

interned后的字符串对象被存储在JVM的PermGen区域--如果应用需要在大字符串上进行各种操作,我们可能需要加载permanent generation的大小;

-XX:MaxPermSize=<size>

第二个解决方案就是使用Java 8– PermGen 空间被Metaspace 替代– 这就不会在使用String的Intern方法时引起OutOfMemoryError 错误。

2c891ba84235a30b9af1de59cc35568b.png

2.3. 未关闭的流

大多数程序员都会忘记关闭打开的流。在Java 7中引入了try-with-resource语句,这在一定程度上解决了这个问题。

为什么是一定程度上? 因为try-with-resources语法是可选的:

@Test(expected = OutOfMemoryError.class)
public void givenURL_whenUnclosedStream_thenOutOfMemory()
  throws IOException, URISyntaxException {
    String str = "";
    URLConnection conn 
      = new URL("http://norvig.com/big.txt").openConnection();
    BufferedReader br = new BufferedReader(
      new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
    
    while (br.readLine() != null) {
        str += br.readLine();
    } 
    
    //
}

看看内存profile的时候会是什么样子

0898d4216fbace5dae78b15202653fe0.png

可以看到,堆内存使用一直在增高,这与打开的流没有正常关闭相关。

进一步看,可以指导,不关闭打开的流将引起两种类型的泄露--一种是底层资源的泄露(主要指文件描述符,打开链接数等),一种是内存泄露。

如何避免?

我们要么手动关闭流,要么就利用Java8的自动关闭语句。

try (BufferedReader br = new BufferedReader(
  new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
    // further implementation
} catch (IOException e) {
    e.printStackTrace();
}

BufferedReader将在try语句的末尾自动关闭,这样就不需要在finally块中显式调用。

2.4. 未关闭的链接

这种情况跟前面的很像,主要区别就是这里处理的是未关闭的链接,比如:数据库链接,FTP服务器链接等。

看看下面的例子:

@Test(expected = OutOfMemoryError.class)
public void givenConnection_whenUnclosed_thenOutOfMemory()
  throws IOException, URISyntaxException {
    
    URL url = new URL("ftp://speedtest.tele2.net");
    URLConnection urlc = url.openConnection();
    InputStream is = urlc.getInputStream();
    String str = "";
    
    //
}

URLConnection仍旧打开着,这将导致一个内存泄露:

159da2f64f86ccde241ed4e9d10d4dea.png

注意GC不能释放引用的内存,即便已经不再使用了。1分钟后,GC操作数迅速减少,堆内存继续增加,最后导致内存泄露OOM.

如何避免?

显式的关闭链接.

2.5. 对要放入HashSet或者HashMap中作为Key存在的对象添加HashCode() and equals() 方法

还有一种非常普遍的内存泄露场景就是对要放入HashSet中的对象,缺少hashCode或者equals方法

具体的,当我们将重复的对象放入集合中的时候--这将导致集合增长,而不是忽略重复对象。这也将导致内存泄露。

public class Key {
    public String key;
    
    public Key(String key) {
        Key.key = key;
    }
}

看下面的场景:

@Test(expected = OutOfMemoryError.class)
public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()
  throws IOException, URISyntaxException {
    Map<Object, Object> map = System.getProperties();
    while (true) {
        map.put(new Key("key"), "value");
    }
}

这将导致下面的情况

9891e2c9fee47651f38d2f4b859b8e27.png

注意GC在1分40秒时无法进行内存回收,

如何避免?

简单的办法就是实现hashCode()和equals()实现。

这里有个简单有用的工具,就是利用Project Lombok,他提供了很多默认的标签实现。@EqualsAndHashCode.

引用

  1. Understanding Memory Leaks in Java | Baeldung.
  2. Understand and Prevent Memory Leaks in a Java Application
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在JDK1.7及之前的版本使用Process process = Runtime.getRuntime().exec("ls -l");方法执行外部命令时,可能会导致内存泄漏。这是因为JDK1.7及之前版本的Process实现,子进程的输出流和错误流的缓存区大小有限,如果子进程的输出或错误信息超出了缓存区的大小,就会导致阻塞,从而可能导致内存泄漏。 为了解决这个问题,你可以使用ProcessBuilder类代替Runtime.exec()方法。ProcessBuilder提供了更好的流控制和错误处理机制,可以避免内存泄漏问题。 以下是使用ProcessBuilder执行外部命令的示例代码: ``` public static void main(String[] args) throws Exception { ProcessBuilder pb = new ProcessBuilder("ls", "-l"); Process process = pb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } reader.close(); } ``` 这个例子与前面的示例代码类似,使用ProcessBuilder构建一个进程,并执行命令"ls -l"。通过ProcessBuilder.start()方法启动进程,并获取进程的输出流进行读取。最后,关闭流并结束程序。 需要注意的是,在使用ProcessBuilder时,你需要手动将子进程的输出流和错误流合并起来,并处理异常和错误信息。同时,为了避免命令注入和其他安全问题,你需要仔细处理命令参数,确保程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值