java中内存泄漏和内存溢出以及异常排查

内存溢出 out of memory:

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

内存泄露 memory leak:

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

 

memory leak会最终会导致out of memory!

内存泄漏从用户的角度来考虑的话根本感觉不到,但是从程序设计的角度我们必须考虑这个问题,因为内存泄漏的堆积最终会消耗系统所有的内存。

内存泄漏的原因:

内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露,通常是长生命周期的对象持有短生命周期对象的引用就会导致内存泄漏。

 

通常有以下几种内存泄漏的场景。

1.静态集合类引起内存泄露: 如果仅仅释放引用本身(o=null),那么list仍然引用该对象,所以这个对象对GC 来说是不可回收的。

        LinkedList<Integer> list=new LinkedList<>();
        int i=0;
        while(true) {
            Integer o=new Integer(i++);
            list.add(o);
            o=null;
        }

2.集合里面对象的属性被修改以后,remove方法不起作用

Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
p3.setAge(2);    //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3);//这个移除是不会发生作用的

 

3.数据库,sockect,io等连接没有及时关闭的也会造成内存泄漏,所以需要我们显式的close。

4.单例模式中,如果单例对象持有外部对象的引用的话,也会导致外部对象不能被回收。

5.监听器(未解决)

6.在使用线程池的时候:如果没有去给队列设置固定的长度或者没有给线程池设置规定的个数的话。

在流量峰值,大量的请求进入的时候,会导致队列塞满或者创建大量的线程,也会导致内存溢出。

 

 主要发生OOM的就是Heap space(堆空间) 元空间

常见的一些错误类型:

1.java.lang.OutOfMemoryError:GC overhead limit exceeded

    默认情况下,当应用程序花费超过98%的时间用来做GC并且回收了不到2%的堆内存时,会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded错误。具体的表现就是你的应用几乎耗尽所有可用内存,并且GC多次均未能清理干净。(就是用了CPU大部分时间去GC,却只回收了2%的内存)

 

2.java.lang.OutOfMemoryError:Permgen space

元空间如何出现内存溢出的情况:

主要原因就是有太多的类或者太大的类被加载到了元空间中。

 

元空间溢出一般在三个时刻:

初始化的时候:

一开始的时候就有大量的类或者太大的类被加载到元空间中了。

这种情况需要我们修改应用程序的启动配置,去增加参数-XX:MaxMetasize的大小

重新部署的时候:

在从服务器卸载应用程序时,当前的classloader以及加载的class在没有实例引用的情况下,持久代的内存空间会被GC清理并回收。

     许多第三方库以及糟糕的资源处理方式(比如:线程、JDBC驱动程序、文件系统句柄)使得卸载以前使用的类加载器变成了一件不可能的事。反过来就意味着在每次重新部署过程中,应用程序所有的类的先前版本将仍然驻留在Permgen区中,你的每次部署都将生成几十甚至几百M的垃圾。

    就以线程和JDBC驱动来说说。很多人都会使用线程来处理一下周期性或者耗时较长的任务,这个时候一定要注意线程的生命周期问题,你需要确保线程不能比你的应用程序活得还长。否则,如果应用程序已经被卸载,线程还在继续运行,这个线程通常会维持对应用程序的classloader的引用,造成的结果就不再多说。多说一句,开发者有责任处理好这个问题,特别是如果你是第三方库的提供者的话,一定要提供线程关闭接口来处理清理工作。

   很多人都会使用线程来处理一下周期性或者耗时较长的任务,这个时候一定要注意线程的生命周期问题,你需要确保线程不能比你的应用程序活得还长。否则,如果应用程序已经被卸载,线程还在继续运行,这个线程通常会维持对应用程序的classloader的引用。类就无法被回收。

   解决这个问题主要是分析dump文件,找到引用在哪里被持有的,然后在应用程序卸载的时候把这个引用移除。jmap -dump....

运行时的oom:

主要是我们在程序的运行过程中创建了大量的类但其实它的生命周期并不长。

在没有实例引用的时候

--XX:+CMSClassUnloadingEnable(默认这个配置是未启用的),GC会扫描这个区并清理不再用的类。我们也需要配合 --XX:+UseConcMarkSweepGC 清除算法。

在确保JVM可以卸载类的情况下,拿到堆存储文件,分析应该被卸载却没有被卸载的类加载器,然后对该类加载器加载的类进行排查。

3.java.lang.OutOfMemoryError:Unable to create new native thread

就是创建太多的线程导致的堆栈溢出:

主要需要我们排查线程池中的线程的数量是否合适

4.java.lang.OutOfMemoryError:Out of swap space

java应用程序在启动时会指定所需要的内存大小,可以通过-Xmx和其他类似的启动参数来指定。在JVM请求的总内存大于可用物理内存的情况下,操作系统会将内存中的数据交换到磁盘上去。

Out of swap space?表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。

这个问题一般是操作系统级别问题引起的。

但是我们可以优化我们的代码,减少它的内存占用

5.java.lang.OutOfMemoryError:Requested array size exceeds VM limit

当你遇到Requested array size exceeds VM limit错误时,意味着你的应用程序试图分配大于Java虚拟机可以支持的数组。

6.Out of memory:Kill process or sacrifice child

当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,就会产生Out of memory:Kill process or sacrifice child错误。在这种情况下,OOM Killer会选择“流氓进程”并杀死它。

参考:https://blog.csdn.net/m0_38110132/article/details/79848426

 

直接内存的回收过程

    直接内存虽然不是 JVM 内存空间,但它的垃圾回收也由 JVM 负责。

    垃圾收集进行时,虚拟机虽然会对直接内存进行回收,
   但是直接内存却不能像新生代、老年代那样,发现空间不足了就通知收集器进行垃圾回收,
   它只能等老年代满了后 Full GC,然后“顺便”帮它清理掉内存的废弃对象。
   否则只能一直等到抛出内存溢出异常时,先 catch 掉,再在 catch 块里大喊 “System.gc()”。
   要是虚拟机还是不听,那就只能眼睁睁看着堆中还有许多空闲内存,自己却不得不抛出内存溢出异常了。

参考:

https://www.jianshu.com/p/2fdee831ed03

 

posted @ 2019-03-14 09:51  LeeJuly  阅读(1018)  评论(0编辑  收藏  举报