java内存泄露与内存溢出
基本概念
内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上看,内存泄露是内存溢出的一种诱因,不是唯一因素。
JAVA中的内存泄露
Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。Java中的内存泄露与C++中的表现有所不同。在C++中,所有被分配了内存的对象,不再使用后,都必须程序员手动的释放他们。所以,每个类,都会含有一个析构函数,作用就是完成清理工作,如果我们忘记了某些对象的释放,就会造成内存泄露。
但是在Java中,我们不用(也没有办法)自己释放内存,无用的对象由GC自动清理,这也极大的简化了我们的编程工作。但实际有时候一些不再会被使用的对象,在GC看来不能被释放,就会造成内存泄露。内存泄露的根本原因:长生命周期的对象持有短生命周期对象的引用。我们知道,对象都有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。下面给出了内存泄露中的一些例子:
全局变量缓存局部变量,且没有清空操作造成内存泄露
这里的object实例,其实我们期望它只作用于method1()方法中,且其他地方不会再用到它,但是,当method1()方法执行完成后,object对象所分配的内存不会马上被认为是可以被释放的对象,只有在Simple类创建的对象被释放后才会被释放,严格的说,这就是一种内存泄露。解决方法就是将object作为method1()方法中的局部变量。当然,如果一定要这么写,可以改为这样:
这样,之前"new Object()"分配的内存,就可以被GC回收。
容器使用时的内存泄露
各种提供close()方法的对象
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,以及使用其他框架的时候,除非其显式的调用了其close()方法(或类似方法)将其连接关闭,否则是不会自动被GC回收的。其实原因依然是长生命周期对象持有短生命周期对象的引用。
单例模式导致的内存泄露
单例模式,很多时候我们可以把它的生命周期与整个程序的生命周期看做差不多的,所以是一个长生命周期的对象。如果这个对象持有其他对象的引用,也很容易发生内存泄露。
JAVA中的内存溢出
堆相关内存溢出
outOfMemoryError: java heap space
在jvm规范中,堆中的内存是用来生成对象实例和数组的。如果细分,堆内存还可以分为年轻代和老年代,年轻代包括一个eden区和两个survivor区。当生成新对象时,内存的申请过程如下:
jvm先尝试在eden区分配新建对象所需的内存
如果内存大小足够,申请结束,否则下一步
jvm启动youngGC试图将eden区中不活跃的对象释放掉,释放后若eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
Surivior区被用来作为Eden及old的中间交换区域,当old区域空间足够时,Survivor区的对象将会被移到old区,否则会被保留在Survivor区
当old区域空间不够时,JVM会在old区进行full GC
full GC后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则会出现"out of memory错误"
解决办法
将堆内存 dump 下来,使用 MAT 分析一下,解决内存泄漏;
如果没有内存泄漏,使用 -Xmx 增大堆内存;
如果有自定义的 Finalizable 对象,考虑其存在的必要性。
java.lang.OutOfMemoryError:GCoverheadlimitexceeded
JDK6新增错误类型,当GC为释放很小空间占用大量时间抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。
解决方法:
查看系统是否使用大内存的代码或死循环
通过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit
方法区内存溢出
outOfMemoryError: permgem space
在jvm规范中,方法区主要存放的是类信息、常量、静态变量等
报错原因
永久代是 HotSot 虚拟机对 方法区的具体实现,存放了已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。需要注意的是,在Java8后,永久代有了一个新名字:元空间,元空间使用的是本地内存。永久代里存在的信息也有了若干变化:
字符串常量由永久代转移到堆中;
和永久代相关的JVM参数已移除。
出现永久代或元空间的溢出的原因可能有如下几种:
有频繁的常量池操作(eg. String.intern),这种情况只适用于Java7之前应用;
加载了大量的类信息,且没有及时卸载;
应用部署完后没有重启。
这种事Perm区内存不够,可通过调整JVM的配置:
-XX:MaxPermSize=128m
-XX:PermSize=128m
设置永久区的初始空间和最大空间
-XX:PermSize设置持久代(perm gen)初始值,物理内存的1/64
-XX:MaxPermSize设置持久代最大值,物理内存的1/4
线程栈溢出
java.lang.StackOverflowError
线程栈是线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时发生的错误。
一般线程栈溢出是由于递归太深或方法调用层级过多导致的。
发生栈溢出的错误信息为:
解决方法:优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小
java.lang.OutOfMemoryError.unabletocreatenewnativethread
Stack空间不足以创建额外的线程,要么创建的线程过多,要么Stack空间确实小了。
解决:由于JVM没有提供参数设置总的stack空间大小,但可以设置单个线程栈的大小;而系统的用户空间一共是3G,除了Text/Data/BSS/MemoryMapping几个段之外,Heap和Stack空间的总量有限,是此消彼长的。因此遇到这个错误,可以通过两个途径解决:1.通过-Xss启动参数减少单个线程栈大小,这样便能开更多线程(当然不能太小,太小会出现StackOverflowError);2.通过-Xms-Xmx两参数减少Heap大小,将内存让给Stack(前提是保证Heap空间够用)。
本地方法溢出
java.lang.OutOfMemoryError: stack_trace_with_native_method
这种情况表明,本地方法在运行时出现了内存分配失败。和java.lang.OutOfMemoryError : unable to create new native Thread 保存不同,方法栈溢出出现在 JVM 的代码层面,而本地方法溢出发生在JNI代码或本地方法处。
java内存泄露排查
JVM如果出现内存泄露,典型的现象就是系统FullGC比较频繁。到最后干脆OOM(Out of Memory)了。
当发现应用内存溢出或长时间使用内存很高的情况下,通过内存dump进行分析可找到原因。当发现cpu使用率很高时,通过线程dump定位具体哪个线程在做哪个工作占用了过多的资源。内存dump是指通过jmap -dump 输出的文件,而线程dump是指通过jstack 输出的信息。在linux操作系统下(已安装jdk),执行jps命令,列出正在运行的java程序的进程ID。
使用top查看目前正在运行的进程使用系统资源情况。
首先是内存dump:
jmap –dump:live,format=b,file=heap.bin
其次是线程dump,比如说::
jstack -m >jvm_deadlocks.txt
jstack -l >jvm_listlocks.txt
但是dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程太折腾不到万不得已最好别这么干。
可以用更轻量级的在线分析,用jmap查看存活的对象情况(jmap -histo:live [pid])。
比如上图所示,HashTable占用了大量的内存,如何找到导致这个事情发生的原因?可以进一步使用btrace来排查。
JvisualVM
VisualVM 是Netbeans的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。在JDK_HOME/bin(默认是C:\Program Files\Java\jdk1.6.0_13\bin)目录下面,有一个jvisualvm.exe文件,双击打开,从UI上来看,这个软件是基于NetBeans开发的了。
VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序或远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。