目录
1 基本了解
1.1 定义
内存溢出 (out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存泄露 (memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
1.2 内存泄漏的分类
内存泄漏以发生的方式来分类,内存泄漏可以分为4类:
- 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
- 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
- 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
- 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到
1.3 Java内存回收机制
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,
1.4 Java内存泄露引起原因
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
那么,Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。具体主要有如下几大类:
1.4.1 静态集合类引起内存泄露
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}//
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
1.4.2 监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
1.4.3 各种连接和流
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
未关闭已打开流(文件,网络等),也会导致内存泄漏
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
1.4.4 单例模式
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。想象下如果A是个比较复杂的对象或者集合类型会发生什么情况
1.4.5 线程方面内存泄漏
1. Runtime.addShutdownHook后没有移除,即使使用了removeShutdownHook,由于ThreadGroup类对于未启动线程的bug,它可能不被回收,导致ThreadGroup发生内存泄露。
2. 创建但未启动线程,与上面的情形相同
3. 创建继承了ContextClassLoader和AccessControlContext的线程,ThreadGroup和InheritedThreadLocal的使用,所有这些引用都是潜在的泄露,以及所有被类加载器加载的类和所有静态引用等等。这对ThreadFactory接口作为重要组成元素整个j.u.c.Executor框架(java.util.concurrent)的影响非常明显,很多开发人员没有注意到它潜在的危险。而且很多库都会按照请求启动线程。
4. ThreadLocal缓存,很多情况下不是好的做法。有很多基于ThreadLocal的简单缓存的实现,但是如果线程在它的期望生命周期外继续运行ContextClassLoader将发生泄露。除非真正必要不要使用ThreadLocal缓存。
5. 当ThreadGroup自身没有线程但是仍然有子线程组时调用ThreadGroup.destroy()。发生内存泄露将导致该线程组不能从它的父线程组移除,不能枚举子线程组。
6. 使用WeakHashMap,value直接(间接)引用key,这是个很难发现的情形。这也适用于继承Weak/SoftReference的类可能持有对被保护对象的强引用。
7. 使用InflaterInputStream在构造函数(比如PNGImageDecoder)中传递new java.util.zip.Inflater(),不调用inflater的end()。仅仅是new的话非常安全,但如果自己创建该类作为构造函数参数时调用流的close()不能关闭inflater,可能发生内存泄露。这并不是真正的内存泄露因为它会被finalizer释放。但这消耗了很多native内存,导致linux的oom_killer杀掉进程。所以这给我们的教训是:尽可能早地释放native资源。
8. java.util.zip.Deflater也一样,它的情况更加严重。好的地方可能是很少用到Deflater。如果自己创建了Deflater或者Inflater记住必须调用end()
1.4.6 其他场景内存泄露
1. OOM for Heap=>例如:java.lang.OutOfMemoryError: Java heap space
分析:此OOM是由于JVM中heap的最大值不满足需要,将设置heap的最大值调高即可,参数样例为:-Xmx2G
解决方法:调高heap的最大值,即-Xmx的值调大(最大堆大小)
2. OOM for Perm=>例如:java.lang.OutOfMemoryError: Java perm space
分析:此OOM是由于JVM中perm的最大值不满足需要,将设置perm的最大值调高即可,参数样例为:-XX:MaxPermSize=512M
解决方法:调高永久的的最大值,即-XX:MaxPermSize的值调大(持久代)
另外,注意一点,Perm一般是在JVM启动时加载类进来,如果是JVM运行较长一段时间而不是刚启动后溢出的话,很有可能是由于运行时有类被动态加载进来,此时建议用CMS策略中的类卸载配置,如:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled
3. OOM for GC=>例如:java.lang.OutOfMemoryError: GC overhead limit exceeded
分析:此OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略,在一定比例下开始GC而不要使用默认的策略,或者将新代和老代设置合适的大小,需要进行微调存活率。
解决方法:改变GC策略,在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)设置的更合理。
4. OOM for native thread created=>如:java.lang.OutOfMemoryError: unable to create new native thread
分析:参考如下:(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads,
MaxProcessMemory 指的是一个进程的最大内存,JVMMemory JVM内存,ReservedOsMemory 保留的操作系统内存,ThreadStackSize 线程栈的大小,如果JVM内存调的过大或者可利用率小于20%,可以建议将heap及perm的最大值下调,并将线程栈调小,即-Xss调小,如:-Xss128k
解决方法:在JVM内存不能调小的前提下,将-Xss设置较小,如:-Xss:128k
5. OOM for allocate huge array=>例如:Exception in thread "main":java.lang.OutOfMemoryError: Requested array size exceeds VM limit
分析:此类信息表明应用程序(或者被应用程序调用的APIs)试图分配一个大于堆大小的数组。例如,如果应用程序new一个数组对象,大小为512M,但是最大堆大小为256M,因此OutOfMemoryError会抛出,因为数组的大小超过虚拟机的限制。
解决方法:首先检查heap的-Xmx是不是设置的过小;如果heap的-Xmx已经足够大,那么请检查应用程序是不是存在bug,例如:应用程序可能在计算数组的大小时,存在算法错误,导致数组的size很大,从而导致巨大的数组被分配。
6. OOM for small swap=>例如:Exception in thread "main": java.lang.OutOfMemoryError: request bytes for . Out of swap space?
分析:抛出这类错误,是由于从native堆中分配内存失败,并且堆内存可能接近耗尽。这类错误可能跟应用程序没有关系,例如下面两种原因也会导致错误的发生:操作系统配置了较小的交换区;系统的另外一个进程正在消耗所有的内存
解决方法:检查os的swap是不是没有设置或者设置的过小;检查是否有其他进程在消耗大量的内存,从而导致当前的JVM内存不够分配。
注意:虽然有时部分显示导致OOM的原因,但大多数情况下,显示的是提示分配失败的源模块的名称,所以有必要查看日志文件,如crash时的hs文件。
7. 应用程序创建一个长时间运行的线程(或者使用线程池,会更快地发生内存泄露)
8. 该类分配了大块内存(比如new byte[1000000]),在某个静态变量存储一个强引用,然后在ThreadLocal中存储它自身的引用。分配额外的内存new byte[1000000]是可选的(类实例泄露已经足够了),但是这样会使内存泄露更快
9. JVM的GC不可达区域,比如通过native方法分配的内存。
web应用在application范围的对象,应用未重启或者没有显式移除,getServletContext().setAttribute("SOME_MAP", map);
web应用在session范围的对象,未失效或者没有显式移除,session.setAttribute("SOME_MAP", map);
不正确或者不合适的JVM选项,比如IBM JDK的noclassgc阻止了无用类的垃圾回收
1.5 一个线程OOM对其他线程影响
1.5.1 问题及分析
问题:一个现场OOM后,其他现场还能运行吗?
分析:由上面的原因知道,线程的OOM分多种类型:
- 堆溢出(java.lang.OutOfMemoryError:Java heap space)
- 永久代溢出(java.lang.OutOfMemoryError:Permgen space)
- 不能创建现场(java.lang.OutOfMemoryError:Unable to create new native thread)
等多种情况
答案:答案是还能运行
1.5.2 代码说明
代码如下:
public class JvmThread {
public static void main(String[] args) {
new Thread(() -> {
List<byte[]> list = new ArrayList<byte[]>();
while (true) {
System.out.println(new Date().toString() + Thread.currentThread() + "==");
byte[] b = new byte[1024 * 1024 * 1];
list.add(b);
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// 线程二
new Thread(() -> {
while (true) {
System.out.println(new Date().toString() + Thread.currentThread() + "==");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
结果展示:
Wed Nov 07 14:42:18 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:18 CST 2018Thread[Thread-0,5,main]==
Wed Nov 07 14:42:19 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:19 CST 2018Thread[Thread-0,5,main]==
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.gosaint.util.JvmThread.lambda$main$0(JvmThread.java:21)
at com.gosaint.util.JvmThread$$Lambda$1/521645586.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Wed Nov 07 14:42:20 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:21 CST 2018Thread[Thread-1,5,main]==
Wed Nov 07 14:42:22 CST 2018Thread[Thread-1,5,main]==
JVM启动参数设置:
上图是JVM堆空间的变化。仔细观察一下在14:42:05~14:42:25之间曲线变化,就会发现使用堆的数量,突然间急剧下滑!
这代表这一点,当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行!
讲到这里大家应该懂了,此题的答案为:一个线程溢出后,进程里的其他线程还能照常运行。
注意了,这个例子只演示了堆溢出的情况。如果是栈溢出,结论也是一样的,大家可自行通过代码测试。
1.5.3 总结
其实发生OOM的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。
1.6 检测内存泄露工具
1.6.1 JRockit检测
Jrockit是Bea开发的符合JAVA虚拟机规范的虚拟机+虚拟机监控软件。
- 虚拟机:Jrockit Real Time
- 监控软件:Jrockit Mission Control
Jrockit Real Time与SUN的JDK是完全兼容的,也就是说以前在SUN的虚拟机上跑的程序,在Jrockit Real Time上不会出现任何问题。
因为内存泄漏有时就算在压力测试中也很难发现。大部分都是在生产环境中产生的。如果没有一个基本不影响运行效率的软件,想解决只能靠运气。
下载JRockit3.1.0,并安装,下载地址:http://www.oracle.com/technology/software/products/jrockit/index.html
要同时下载Jrockit Mission Control 3.1.0(监控软件)和Jrockit Real Time 3.1.0(虚拟机)
服务器端配置
- 在服务器段安装Jrockit Real Time 3.1.0,
- 设置应用程序,使用此Jrockit启动应用程序。
Tomcat 的设置方法是:
JAVA_OPTS=" -verbosegc -Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=本机IP "和JRE_HOME="Jrockit虚拟机路径"
将JRE_HOME改为JAVA_HOME也行
下载http://download2.bea.com/pub/license/All%20Products/BEA_WebLogic.zip,解压后将其中的LIC-WLRT20.txt文件改名为license.bea上传到%JROCKIT_HOME%/jre/下。
首先安装Jrockit Mission Control 3.1.0,然后运行
在JVM浏览器视图中,对连接器文件夹右键,选择新建连接
开始监控内存
2 栈溢出异常
2.1 栈的基本了解
- 栈存放什么:栈存储运行时声明的变量——对象引用(或基础类型, primitive)内存空间, 栈的实现是先入后出的。
- 堆存放什么:堆分配每一个对象内容(实例)内存空间。
- 栈溢出:java.lang.StackOverflowError
- 堆溢出:java.lang.OutOfMemoryError: Java heap space
- 栈溢出实现,可以递归调用方法,这样随着栈深度的增加,JVM 维持着一条长长的方法调用轨迹。
- 堆溢出实现,可以循环创建对象或大的对象;
2.2 栈溢出原因
内在两种情况:
- 线程请求的栈深度大于虚拟机允许的最大深度 StackOverflowError
- 虚拟机在扩展栈深度时,无法申请到足够的内存空间 OutOfMemoryError
外在原因:
- 是否有递归调用
- 是否有大量循环或死循环
- 全局变量是否过多
- 数组、List、map数据是否过大
那么递归为什么会导致栈溢出呢?相信大家知道栈的出入规则,就像一个瓶子,方法压栈运行,先进后出,递归的话那么先入的不能出栈,会存在栈空间中,这样就容易导致栈满而溢出。
每当你调用一个方法,在这个方法执行前都会将之前的内存地址(也就是调用点)入栈,等被调用的方法执行完将地址出栈,程序根据这个数据返回调用点。
若递归调用次数太多,就会只入栈不出栈,于是堆栈就被压爆了,此为栈溢出。
递归函数调用的太深,需要太多的内存,递归里用到的局部变量存储在堆栈中,堆栈的访问效率高,速度快,但空间有限,递归太多变量需要一直入栈而不出栈,导致需要的内存空间大于堆栈的空间