内存泄露与内存溢出1定义1、内存泄漏:一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而造成那部分内存不可用的情况。
2、内存溢出:指内存不够使用而抛出异常,内存泄露是其形成的原因之一。
2危害会导致新的资源分配请求无法完成,引起系统错误,最后导致系统崩溃。
3内存泄漏分类4 内存泄露/溢出发生的区域5内存溢出异常6内存溢出常见原因7发生内存泄露的情形Java内存泄露根本原因是什么呢?答:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
具体主要有如下几大类:7.1 静态集合类引起内存泄露像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
例:解析:在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。
因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
7.2创建过大对象以上代码运行时瞬间报错。
7.3监听器在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
7.4 各种连接比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。
但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。
这种情况下一般都会在try里面去的连接,在finally里面释放连接。
7.5 内部类和外部模块等的引用内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。
此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:public void registerMsg(Object b);这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
7.6 单例模式不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露8JVM监控分析8.1查看系统资源的基本使用情况8.2查看内存使用情况8.3典型内存泄露堆内存情况9内存溢出/泄露实战9.1模拟Java堆溢出说明:Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
通过设置参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。
代码清单1 Java堆内存溢出异常测试如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。
于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。
掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
以上是处理Java堆内存问题的简单思路,处理这些问题所需要的知识、工具与经验是后面3章的主题。
9.2虚拟机栈和本地方法栈溢出JMV并不区分虚拟机栈和本地方法栈,栈部分通常会出现两种异常:1、如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
2、如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
代码清单2 虚拟机栈和本地方法栈OOM测试(因为每个方法压入栈的帧大小并不是一样的,所以只能说在大多数情况下)达到1000~2000完全没有问题,对于正常的方法调用(包括递归),这个深度应该完全够用了。
但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
如果没有这方面的处理经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。
代码清单2-5 创建线程导致内存溢出异常9.3运行时常量池溢出如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
该方法的作用是:如果池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String 对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量,如代码清单2-4所示。
代码清单2-4 运行时常量池导致的内存溢出异常运行结果:从运行结果中可以看到,运行时常量池溢出,在OutOfMemoryError后面跟随的提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分9.4方法区溢出方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。
虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等),但在本次实验中操作起来比较麻烦。
在代码清单2-8中,笔者借助CGLib直接操作字节码运行时生成了大量的动态类。
值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如Spring、Hibernate,在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。
另外,JVM 上的动态语言(例如Groovy等)通常都会持续创建类来实现语言的动态性,随着这类语言的流行,也越来越容易遇到与代码清单2-8相似的溢出场景。
代码清单2-8 借助CGLib使方法区出现内存溢出异常9.5本机直接内存溢出DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,代码清单2-9越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar 中的类才能使用Unsafe的功能)。
因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。
代码清单2-9 使用unsafe分配本机内存10内存溢出预防与解决办法10.1避免内存泄露/溢出措施1、尽早释放无用对象的引用;2、优化算法,减小空间复杂度。
3、尽量少用静态变量,因为静态变量是全局的,GC不会回收。
4、避免集中创建对象尤其是超大对象,如果可以的话尽量使用流操作。
5、尽量运用对象池技术以提高系统性能,例如连接池。
6、不要在经常调用的方法中创建对象,尤其是忌讳在循环中创建对象。
7、优化配置10.2查看系统状态监控显示:10.3发现内存泄露通常情况下,内存问题的可能会表现为:1、运行一段时间后系统崩溃,报ng.OutOfMemoryError异常2、启动时直接崩溃,报错ng.OutOfMemoryError3、系统已经不能正常提供服务,但无错误日志4、系统运行正常,响应速度逐渐减慢为了分析判断内存问题,通常除了查看基本的系统状态信息外,还可以对内存、CPU、线程分别进详细分析。
例如:对内存堆DUMP进行分析,如下图所示:(报表清晰的显示发现内存泄露的个数)另外,还可以通过一些性能监测分析工具辅助分析,如 JProfiler、Optimizeit Profiler进行分析。
10.4定位内存泄露思路:1、查看线程独占的超长生命周期对象,及其代码调用关系2、查看占用空间较多的对象及其代码调用关系为解决内存泄露问题,一般需要Dump出内存快照转储文件,再通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。
从上图信息可知,dump文件在位置:C:\Users\ADMINI~1\AppData\Local\Temp\visualvm.dat\localhost_368\heapdump-146491 9773882.hprof内存泄露的关键位置将dump文件导入eclipse中,经Eclipse Memory Analyzer分析,得出报表。
在Accumulated Objects by Class区域,可以看到垃圾对象占用内存空间的信息,如下如所示:由上图可以清晰的看到哪些类占用了内存空间,便可以进行响应代码查错与修改。