JVM调优(8)Java的内存泄漏

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Allen202/article/details/85104991

内存溢出和内存泄漏

内存溢出
out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

内存泄露
memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!

以发生的方式来分类,内存泄漏可以分为4类:

  • 常发性内存泄漏: 发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  • 偶发性内存泄漏: 发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  • 一次性内存泄漏: 发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
  • 隐式内存泄漏: 程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

Java内存回收方式

Java判断对象是否可以回收使用的而是可达性分析算法。
但对于不同生命周期的内回收方式不同,这部分可以参考:JVM调优
这个算法的基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object3, object4, object5虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。
在这里插入图片描述

问题的提出

通过GC程序员大部分情况下是不需要关心内存占用和释放问题。但是,如果程序写的不合理,也可以说不正确的话,或者GC,JVM设置不合理。系统就有可能也存在内存泄露。

随着越来越多的服务器程序采用Java技术,集群,分布式,多线程等,服务器程序往往长期运行,有时产品发布活动多并发等。内存泄露问题也就变得十分关键,即使每次运行少量泄漏,长期运行之后,系统也是面临崩溃的危险。

内存泄漏引起的原因

为什么会有内存泄露呢?因为无用对象持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费而引起的。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。

那么,Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。具体主要有如下几大类

1、各种连接。

比如IO连接,网络连接和数据库连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC回收的。对于Resultset和Statement对象可以不进行显式回收,但Connection一定要显式回收,因为Connection在任何时候都无法自动回收,而Connection一旦回收,Resultset和Statement对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement对象(关闭其中一个,另外一个也会关闭,否则就会造成大量的Statement对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

2、单例模式

不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:

3、监听器

在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。

4、静态集合类引起内存泄露

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

5、无界限阻塞队列

如果使用像LinkedBlockingQueue这样的无界的阻塞队列,如果消费者业务处理很慢,而生产者往队列中添加对象速度快,无界的阻塞队列就无限长,就可能导致内存不足。

6、JDK6/7的subString()

在JDK6中subString是是通过改变offset和count来创建一个新的String对象,value相当于一个仓库,还是用的父String的。

依JDK7对subString实现的源码为例:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
      throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
      throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
      throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
  }
public String(char value[], int offset, int count) {
    if (offset < 0) {
      throw new StringIndexOutOfBoundsException(offset);
    }
    if (count < 0) {
      throw new StringIndexOutOfBoundsException(count);
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
      throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
  }
public static char[] copyOfRange(char[] original, int from, int to) {
        int newLength = to - from;
        if (newLength < 0)
            throw new IllegalArgumentException(from + " > " + to);
        char[] copy = new char[newLength];   //是创建了一个新的char数组
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));
        return copy;
    }

例子

String str1 = "12rgabu94";
String str2 = str.substring(0, 3);
str1 = null;

虽然str1为null,但是由于str1和str2之前指向的对象用的是同一个value数组,导致第一个String对象无法被GC,这样就可能形成了内存泄漏。具体的讲解请参考:The substring() Method in JDK 6 and JDK 7
1.6和1.7的内存变化如图:
在这里插入图片描述

如何避免java内存泄漏?

1、尽早释放无用对象的引用。好的办法是使用临时变量的时候,让引用变量在推出活动域后自动设置为null,暗示垃圾收集器来收集该对象,防止发生内存泄漏。
2、程序进行字符串处理时,尽量避免使用String,而应该使用StringBuffer。因为String类是不可变的,每一个String对象都会独立占用内存一块区域。
3、尽量少用静态变量。因为静态变量是全局的,存在方法区,GC不会回收。(用永久代实现的方法区,垃圾回收行为在这个区域是比较少出现的,垃圾回收器的主要目标是针对常量池和类型的卸载)
4、避免集中创建对象,尤其是大对象,如果可以的话尽量使用流操作。JVM会突然需要大量neicun,这时会出发GC优化系统内存环境
5、尽量运用对象池技术以提高系统性能。生命周期长的对象拥有生命周期短的对象时容易引发内存泄漏,例如大集合对象拥有大数据量的业务对象的时候,可以考虑分块进行处理,然后解决一块释放一块的策略。
6、不要在经常调用的方法中创建对象,尤其忌讳在循环中创建对象。可以适当的使用hashtable,vector创建一组对象容器,然后从容器中去取这些对象,而不用每次new之后又丢弃。
7、优化配置 JVM调优(6)之参数配置

分析内存泄露

1、把Java应用程序使用的heap dump下来
2、使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象
3、必要时,需要分析嫌疑对象和其他对象的引用关系。
4、查看程序的源代码,找出嫌疑对象数量过多的原因。

dump heap

如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把运行中JVM的heap dump下来。
JDK自带的jmap工具,可以做这件事情。它的执行方法是:

jmap -dump:format=b,file=heap.bin <pid>

format=b的含义是,dump出来的文件时二进制格式。
file-heap.bin的含义是,dump出来的文件名是heap.bin。
就是JVM的进程号。执行ps aux | grep java或者jps,找到JVM的pid;

JProfiler

JProfiler 是一个商业授权的Java剖析工具,由EJ技术有限公司,针对的Java EE和Java SE应用程序开发的。它把CPU、执行绪和内存的剖析组合在一个强大的应用中。JProfiler可提供许多IDE整合和应用服务器整合用途。JProfiler的是一个独立的应用程序,但其提供Eclipse和IntelliJ等IDE的插件。它允许两个内存剖面评估内存使用情况和动态分配泄漏和CPU剖析,以评估线程冲突。JProfiler直觉式的GUI让你可以找到性能瓶颈、抓出内存漏失(memory leaks)、并解决执行绪的问题。它让你得以对heap walker作资源回收器的root analysis,可以轻易找出内存漏失;heap快照(snapshot)模式让未被参照(reference)的对象、稍微被参照的对象、或在终结(finalization)队列的对象都会被移除;整合精灵以便剖析浏览器的Java外挂功能。
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Allen202/article/details/85104991