内存溢出,栈溢出以及内存泄露

内存溢出(OutOfMemoryError):应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。

内存溢出的类型:

(1)java.lang.OutOfMemoryError: PermGen space

原因:一般是程序中web-inf/lib下引入了大量的第三方Jar或class,使java虚拟机装载类的空间不够,JVM中永久代(Permanent Generation space)如果不进行配置的话,默认的大小为4M。这种问题经常出现第一次部署Tomcat到服务器上的时候。

解决方案(建议):TOMCAT_HOME/bin/catalina.sh在

echo "Using CATALINA_BASE:   $CATALINA_BASE"

上面加入以下行:

JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m"

建议:如果tomcat部署了多个应用,很多应用都使用了相同的jar,可以将共同的jar移到 tomcat共同的lib下,减少类的重复加载。例如:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以达到减少jar 文档重复占用内存的目的。但是,Tomcat部署多个应用,他们将运行在同一个JVM上,共享JVM的配置,那么就存在一个应用把JVM搞瘫痪之后,其他应用也无法运行的情况。所以重要的应用不建议部署在同一个Tomcat下。

 

(2)java.lang.OutOfMemoryError: Java heap space

直接原因:java虚拟机创建的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。

那么虚拟机创建的对象太多,是怎么引起的呢?本质原因可能有常见以下的情况:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小;

针对上述的原因就会有对应的解决方案*(建议)

  1. 检查对数据库查询中,是否有一次获得全部数据的查询。如果一次查询的记录的大小大于JVM虚拟机当前的可用内存,就会引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。注意,SQL查询的条件是否依赖于前端填写,如果没有该条件,是否会造成读取的数据量过大。当然也可以考虑使用分页查询。
  2. 检查List、MAP等集合对象是否有使用完后,未清除的问题。在代码中,我们可能会定义使用一些静态的List、MAP等集合,如果在静态的集合中,引用了对象,由于静态集合的生命周期是和这个应用程序的的生命周期是一样的,直到这个应用重启,那么就会使得这些对象一直不能被GC回收。建议:慎重使用这些静态的集合对象。
  3. 对于代码的问题,只能凭经验来检查代码中是否有死循环或递归调用, 检查是否有大循环重复产生新对象实体。
  4. 第三方软件的BUG,要么寻求第三方支持,要么只能通过其他方式实现功能,也没什么好说的。
  5. 如果是参数设置的过小就比较好解决。增加Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m

       关于数据库查询的问题,笔者最近在生产上就看到了这样的例子,一个查询功能的页面,查询条件就汇款流水号一个字段,这个字段没有做非空校验,由于这个功能的SQL主要是根据流水来过滤的,也就是说如果没有填写流水,几乎相当于全表查询。一开始上线的时候,表中的数据量不大,业务使用功能的时候一般也会填写流水号,一直到一段时候之后,这张表的数据量达到了数十W,业务没有输入流水号,点击了查询,系统直接内存溢出,只能重启服务。

 

如何写一段代码,故意让程序出现OOM异常?

(1)java.lang.OutOfMemoryError: PermGen space

       我们需要的是设置方法区的大小,实现方式是通过设置-XX:PermSize和-XX:MaxPermSize参数,这样可以快一点达到我们的目的。

        方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。在 JDK 1.6 及之前的版本中,由于常量池分配在永久代内String.intern() 方法是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。可以通过死循环调用String.intern()来使得方法区内存溢出。

import java.util.ArrayList;
import java.util.List;
               
public class RuntimeConstantPoolOom {
  public static void main(String[] args) {
    List list = new ArrayList();
    int i = 0;
    while (true) {
      list.add(String.valueOf(i++).intern());
    }
  }

      JDK1.7以及之后的版本,常量池被移出永久代,我们可以通过不停的加载class来模拟方法区内存溢出,《深入理解java虚拟机》中借助 CGLIB 这类字节码技术模拟了这个异常,我们这里使用不同的 classloader 来实现(同一个类在不同的 classloader 中是不同的),如下:

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;

public class MethodAreaOom {

  public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
    Set> classes = new HashSet>();
    URL url = new File("").toURI().toURL();
    URL[] urls = new URL[]{url};
    while (true) {
      ClassLoader loader = new URLClassLoader(urls);
      Class loadClass = loader.loadClass(Object.class.getName());
      classes.add(loadClass);
    }
  }
}

      在 jdk1.8 上运行上面的代码将不会出现异常,因为 jdk1.8 已结去掉了永久代,当然 -XX:PermSize=2m -XX:MaxPermSize=2m 也将被忽略。

 

(2)java.lang.OutOfMemoryError: Java heap space

同样的,我们设置jvm堆内存的大小, -Xms(堆的最小值),-Xmx(堆的最大值)为5M。

import java.util.ArrayList;
import java.util.List;
import testbean.UserBean;

public class HeapOOM {

    public static void main(String[] args) {
        List users = new ArrayList();
        while (true) {
            users.add(new UserBean());
        }
    }

 

(3)java.lang.StackOverflowError

我们在重现异常的时候就要相应的将栈内存容量设置的小一些,设置栈大小的方法是设置-Xss参数,-Xss=100K

    public class VMStackOOM {
       public static void main(String[] args) {
        Recursion recursion = new Recursion();
        try {
            recursion.recursionself();
        } catch (Throwable e) {
            System.out.println("current value :" + recursion.currentValue);
            throw e;
        }
    }
}

public class Recursion {
    public int currentValue = 0;
    public void recursionself() {
        currentValue += 1;
        recursionself();//递归调用
    }
}

 

(4)直接内存溢出

       在Java的NIO(New IO)中,支持直接内存的使用,也就是通过Java代码,获得一块堆外的内存空间,这块空间是直接向操作系统申请的。直接内存的申请速度一般要比堆内存慢,但是其访问速度要快于堆内存。因此,对于那些可复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但由于直接内存没有被Java虚拟机完全托管,若使用不当,也容易触发直接内存溢出,导致宕机。以下的程序在32位虚拟机上运行:

public class DirectBufferOOM {  
     public static void main(String args[]){ 
         for(int i=0;i<1024;i++){ 
            //申请直接内存,不占用JVM虚拟的内存,与之对应的是
            //public static ByteBuffer allocate(int capacity)申请的是JVM的内存
             ByteBuffer.allocateDirect(1024*1024); 
             System.out.println(i); 
 //            System.gc(); 
         } 
     } 
 }

      直接内存不一定能够触发GC(除非直接内存使用量达到了-XX:MaxDirectMemorySize的设置),所以保证直接内存不溢出的方法是合理地进行Full GC的执行,或者设定一个系统实际可达的-XX:MaxDirectMemorySize值(默认情况下等于-Xmx的设置)。

 

内存泄露

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

 

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

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

 

举个例子:项目上线后,每次系统启动两天后,就会出现内存溢出的错误。这种情况一般是代码中出现了缓慢的内存泄漏,这就需要使用内存查看工具了。

内存查看工具有许多,比较有名的有:Optimizeit Profiler、JProbe Profiler、JinSight和Java1.5的Jconsole等。它们的基本工作原理大同小异,都是监测Java程序运行时所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员可以根据这些信息判断程序是否有内存泄漏问题。一般来说,一个正常的系统在其启动完成后其内存的占用量是基本稳定的,而不应该是无限制的增长的。持续地观察系统运行时使用的内存的大小,可以看到在内存使用监控窗口中是基本规则的锯齿形的图线如果内存的大小持续地增长,则说明系统存在内存泄漏问题。通过间隔一段时间取一次内存快照,然后对内存快照中对象的使用与引用等信息进行比对与分析,可以找出是哪个类的对象在泄漏。

 

参考资料:

  1. https://www.cnblogs.com/lcword/p/5857909.html
  2. https://www.cnblogs.com/200911/p/3965108.html
  3. http://outofmemory.cn/c/java-outOfMemoryError
  4. https://www.niuhp.com/java/jvm-oom-pg.html
  5. http://book.51cto.com/art/201504/472203.htm

 

 

你可能感兴趣的:(JVM学习笔记)