Java内存泄露与溢出

          我们利用JVM对内存进行分配和管理的的最主要目的就是为了防止内存泄漏和溢出这两大问题,这也是我们初学的时候最容易忽略的;

          概念:

           内存泄露:分配出去的内存回收不了

           内存溢出:系统的内存不够用了

一、内存泄漏

          一般来说内存泄露都有两种情况,第一种情况在C/C++的,在堆中分配的内存 ,在没有将其释放掉的时候,就将所有能访问这块内存的方式都删掉(如指针重新赋值);另一种情况则是在内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)。第一种情况,在Java中已经由于垃圾回收机制的引入,得到了很好的解决。所以,Java中的内存泄漏,主要指的是第二种情况。 

         在Java中内存泄露最主要的原因有三个

         ①没有消除过期的对象引用:所谓的过期引用,指永远也不会被消除的引用

  eg:一个栈开始的时候在增加,增加到一定长度后,然后再收缩,这时候,从栈中弹出的对象将不会被当做垃圾回收,即使这个栈程序不再引用这些对象,他们也不会被回收;因为栈内部维护着这些对象的过期引用,这中情况下就发生了内存泄漏;我们写程序的时候最经常忽略的也是这些细节方面;

 

eg:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

 

          解决方法:就是一旦对象过期,我们就清空这些引用就行了,及时的清空引用的好处有,当我们再次调用这些过期对象的时候,程序会抛出异常,而不是错误的执行下去;在声明对象引用之前,明确内存对象的有效作用域。在一个函数内有效的内存对象,应该声明为local变量,与类实例生命周期相同的要声明为实例变量……以此类推。在内存对象不再需要时,记得手动将其引用置空。

          当然我们要注意的是不用过分小心,对于每一个对象引用,当我们不需要的时候,立马清空,这样做是没有必要的。清空对象引用应该是一种特例,而不是一种规范;一般而言只要类是自己管理内存的,我们就要警惕内存泄露的问题;

         ②内存泄漏的另一个常见来源就是缓存:

eg:你把对象引用放到缓存中,然后你给忘了,使他不再有用的很长一段时间里面仍然留在缓存中;

         解决方法:一种:当在缓存之外存在对某个项的键的引用,该项就有意义, 用WeakHashMap代表缓存,当对象过期的时候会自动消除;另一种:缓存项的生命周期是否有意义不容易确定,随着时间的推移,其中的项变得越来越没有价值,在这种情况下,缓存需要时不时的清理拿些没有用的项,LinkedHashMap类利用他的removeEldestEntry方法可以很容易的实现在该缓存添加新条目的时候清理这些过期项;对于更加复杂的缓存,必须直接使用java.lang.ref

          ③内存泄漏最常见的来源是监听器和其它回调

eg:当你实现了一个API,客户端在这个API中注册回调,却没有显式的取消注册,这时候你要是没有采取一些特殊的动作,他们机会积聚;

          解决方法:确保回调立即被当做垃圾回收的最佳方法就是保存他们的弱引用,例如,将他们保存成WeakHashMap中的键;

二、内存溢出 

       我们对JVM有所了解的都知道,在Java虚拟机规范中JVM是被分为好几个分区的,首先被分为线程共享区和线程非共享区:

        线程共享区:分为堆区、方法区和运行时常量池,其中Java堆区又被分为新生代和老年代,新生代又被分为Eden空间、From Survivor空间、To Survivor空间;二我们的方法区仅仅上是逻辑上独立的,物理上还是堆区的一部分,方法区中有一块特殊的运行时内存区,很多开发人员称之为永久代;而运行时常量池又是方法区的一部分;

        线程非共享区:不允许被所有线程共享访问的,只允许被所属的独立线程进行访问的一类内存区,包括PC寄存器(PC计数器)、Java栈区(储存对象实例)、本地方法栈(用于支持本地方法)

       我们的JVM大体上是这么进行分区的。言归正传,我们的内存溢出将围绕这些分区进行展开;

①栈溢出(StackOverFlowError)

       栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。

       出现这个异常一般是程序错误导致的,例如一个死递归函数就可能造成这种情况:

package jvm;

/**
 * Created by Taoyongpan on 2017/8/6.
 */
public class stack01 {
    //死递归
    public void test(){
        test();
    }

    public static void main(String[] args){
        stack01 s = new stack01();
        s.test();
    }
}
 这段程序就是调用test()方法的递归,抛出如下的异常、

 

 

Exception in thread "main" java.lang.StackOverflowError
	at jvm.stack01.test(stack01.java:9)
 ②堆溢出(OutOfMemoryError:java heap space)、

 

        堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryReeor:java heap space,出现这种情况的时候,我们需要根据内存溢出的时候产生的dump文件具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。

        如果是内存泄漏了,我们要找出泄露的对象是怎么被GCROOT引用起来,然后根据引用链来具体分析泄漏的原因。

         如果出现你内存溢出的问题,这往往是程序本身需要的内存大雨了我们虚拟机配置的内存,这种情况下,我们可以通过调大-Xmx来解决问题:

持久带溢出(OutOfMemoryError: PermGen space)

       我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space
我在工作可能在如下几种场景下出现此问题。

  1. 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
  2. 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
  3. 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。

我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出.下面我们通过如下的代码来模拟此种情况:

1
2
3
4
5
6
7
8
9
10
11
12
import java.util.*;
import java.lang.*;
public class OOMTest{
 
         public static void main(String... args){
                 List<String> list = new ArrayList<String>();
                 while ( true ){
                         list.add(UUID.randomUUID().toString().intern());
                 }
         }
 
}

我们通过如下的命令运行上面代码:

java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest

运行后的输入如下图所示:

1
2
3
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
         at java.lang.String.intern(Native Method)
         at OOMTest.main(OOMTest.java: 8 )

通过上面的代码,我们成功模拟了运行时常量池溢出的情况,从输出中的PermGen space可以看出确实是持久带发生了溢出,这也验证了,我们前面说的Hotspot jvm通过持久带来实现方法区的说法。

④OutOfMemoryError:unable to create native thread

        最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。 出现这种情况的时候,一般是下面两种情况导致的:

  1. 程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。
  2. 给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。 线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。

内存溢出部分出自http://www.importnew.com/14604.html。

猜你喜欢

转载自taoyongpan.iteye.com/blog/2388426