考虑垃圾收集器在应用程序生命周期中可能永远不会运行的可能性。无法保证JVM何时或是否会调用垃圾收集器——即使程序显式调用System.gc()。通常,垃圾收集器不会自动运行,直到程序需要比当前可用内存更多的内存。此时,JVM将首先尝试通过调用垃圾收集器来获得更多的可用内存。如果这种尝试仍然没有释放足够的资源,那么JVM将从 *** 作系统获得更多的内存,直到它最终达到允许的最大值。
以一个小型的Java应用程序为例,它显示一些简单的用户界面元素用于配置修改,并且存在内存泄漏。很有可能在应用程序关闭之前,垃圾收集器甚至不会被调用,因为JVM可能有足够的内存来创建程序所需的所有对象,并有剩余的内存来备用。因此,在这种情况下,即使一些死对象在程序执行时占用了内存,但对于所有实际用途来说,这真的无关紧要。
如果正在开发的Java代码打算一天24小时在服务器上运行,那么内存泄漏比我们的配置实用程序的情况要严重得多。即使是某些代码中本来应该连续运行的最小漏洞,最终也会导致JVM耗尽所有可用的内存。
最后一个考虑是,Java内存泄漏不应该被视为像其他语言(如C++)中发生的那样危险,因为在其他语言中,内存会丢失,永远不会返回到 *** 作系统。在Java应用程序的情况下,对象都是依附在 *** 作系统给JVM的内存资源上。因此,理论上,一旦Java应用程序及其JVM关闭,所有分配的内存都将返回给 *** 作系统。
常见内存泄露情况
长生命周期的对象持有短生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有连接被关闭后,垃圾收集器才会回收对应的对象。
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 ,程序很容易就产生内存泄漏
如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(你认为垃圾收集器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾收集器不能正常工作)
解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收;
内存泄漏和内存溢出辨析
内存溢出和内存泄露,在程序运行时,都会表现出OOM异常,但是两者还是有本质不同的:
一般情况下内存泄漏的避免在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度。我们有时也将其称为“对象游离”。
例如:
public class FileSearch{ private byte [] content private File mFilepublic FileSearch(File file){ mFile = file } public boolean hasString(String str){ int size = getFileSize(mFile) content = new byte [size]loadFile(mFile, content)String s = new String(content)return s.contains(str)}}
在这段代码中,FileSearch 类中有一个函数 hasString ,用来判断文档中是否含有指定的字符串。流程是先将mFile 加载到内存中,然后进行判断。但是,这里的问题是,将 content 声明为了实例变量,而不是本地变量。于是,在此函数返回之后,内存中仍然存在整个文件的数据。而很明显,这些数据我们后续是不再需要的,这就造成了内存的无故浪费。
要避免这种情况下的内存泄露,要求我们以C/C++ 的内存管理思维来管理自己分配的内存。第一,是在声明对象引用之前,明确内存对象的有效作用域。在一个函数内有效的内存对象,应该声明为 local 变量,与类实例生命周期相同的要声明为实例变量……以此类推。第二,在内存对象不再需要时,记得手动将其引用置空。
复杂数据结构中的内存泄露问题
在实际的项目中,我们经常用到一些较为复杂的数据结构用于缓存程序运行过程中需要的数据信息。有时,由于数据结构过于复杂,或者我们存在一些特殊的需求(例如,在内存允许的情况下,尽可能多的缓存信息来提高程序的运行速度等情况),我们很难对数据结构中数据的生命周期作出明确的界定。这个时候,我们可以使用Java 中一种特殊的机制来达到防止内存泄露的目的。
之前我们介绍过,Java 的 GC 机制是建立在跟踪内存的引用机制上的。而在此之前,我们所使用的引用都只是定义一个“ Object o”这样形式的。事实上,这只是 Java 引用机制中的一种默认情况,除此之外,还有其他的一些引用方式。通过使用这些特殊的引用机制,配合 GC 机制,就可以达到一些我们需要的效果。
Java中的几种引用方式
Java中有几种不同的引用方式,它们分别是:强引用、软引用、弱引用和虚引用。下面,我们首先详细地了解下这几种引用方式的意义。
强引用
在此之前我们介绍的内容中所使用的引用 都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用(SoftReference )
SoftReference 类的一个典型用途就是用于内存敏感的高速缓存。SoftReference 的原理是:在保持对对象的引用时保证在 JVM 报告内存不足情况之前将清除所有的软引用。关键之处在于,垃圾收集器在运行时可能会(也可能不会)释放软可及对象。对象是否被释放取决于垃圾收集器的算法 以及垃圾收集器运行时可用的内存数量。
弱引用(WeakReference )
WeakReference 类的一个典型用途就是规范化映射( canonicalized mapping )。另外,对于那些生存期相对较长而且重新创建的开销也不高的对象来说,弱引用也比较有用。关键之处在于,垃圾收集器运行时如果碰到了弱可及对象,将释放 WeakReference 引用的对象。然而,请注意,垃圾收集器可能要运行多次才能找到并释放弱可及对象。
虚引用(PhantomReference )
PhantomReference 类只能用于跟踪对被引用对象即将进行的收集。同样,它还能用于执行 pre-mortem 清除 *** 作。PhantomReference 必须与 ReferenceQueue 类一起使用。需要 ReferenceQueue 是因为它能够充当通知机制。当垃圾收集器确定了某个对象是虚可及对象时, PhantomReference 对象就被放在它的 ReferenceQueue 上。将 PhantomReference 对象放在 ReferenceQueue 上也就是一个通知,表明 PhantomReference 对象引用的对象已经结束,可供收集了。这使您能够刚好在对象占用的内存被回收之前采取行动。Reference与 ReferenceQueue 的配合使用。
GC、 Reference 与 ReferenceQueue 的交互
A、 GC无法删除存在强引用的对象的内存。
B、 GC发现一个只有软引用的对象内存,那么:
① SoftReference对象的 referent 域被设置为 null ,从而使该对象不再引用 heap 对象。
② SoftReference引用过的 heap 对象被声明为 finalizable 。
③ 当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放, SoftReference 对象就被添加到它的 ReferenceQueue (如果后者存在的话)。
C、 GC发现一个只有弱引用的对象内存,那么:
① WeakReference对象的 referent 域被设置为 null , 从而使该对象不再引用heap 对象。
② WeakReference引用过的 heap 对象被声明为 finalizable 。
③ 当heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放时, WeakReference 对象就被添加到它的 ReferenceQueue (如果后者存在的话)。
D、 GC发现一个只有虚引用的对象内存,那么:
① PhantomReference引用过的 heap 对象被声明为 finalizable 。
② PhantomReference在堆对象被释放之前就被添加到它的 ReferenceQueue 。
值得注意的地方有以下几点:
1、 GC 在一般情况下不会发现软引用的内存对象,只有在内存明显不足的时候才会发现并释放软引用对象的内存。
2、 GC 对弱引用的发现和释放也不是立即的,有时需要重复几次 GC ,才会发现并释放弱引用的内存对象。3、软引用和弱引用在添加到 ReferenceQueue 的时候,其指向真实内存的引用已经被置为空了,相关的内存也已经被释放掉了。而虚引用在添加到 ReferenceQueue 的时候,内存还没有释放,仍然可以对其进行访问。
代码示例
通过以上的介绍,相信您对Java 的引用机制以及几种引用方式的异同已经有了一定了解。光是概念,可能过于抽象,下面我们通过一个例子来演示如何在代码中使用 Reference 机制。
String str = new String( " hello " ) // ①ReferenceQueue <String > rq = new ReferenceQueue <String >() // ②WeakReference <String > wf = new WeakReference <String >(str, rq) // ③str = null // ④取消"hello"对象的强引用String str1 = wf.get() // ⑤假如"hello"对象没有被回收,str1引用"hello"对象// 假如"hello"对象没有被回收,rq.poll()返回nullReference <? extends String > ref = rq.poll() // ⑥
在以上代码中,注意⑤⑥两处地方。假如“hello ”对象没有被回收 wf.get() 将返回“ hello ”字符串对象, rq.poll() 返回 null ;而加入“ hello ”对象已经被回收了,那么 wf.get() 返回 null , rq.poll() 返回 Reference 对象,但是此 Reference 对象中已经没有 str 对象的引用了 ( PhantomReference 则与WeakReference 、 SoftReference 不同 )。
引用机制与复杂数据结构的联合应用
了解了GC 机制、引用机制,并配合上 ReferenceQueue ,我们就可以实现一些防止内存溢出的复杂数据类型。
例如,SoftReference 具有构建 Cache 系统的特质,因此我们可以结合哈希表实现一个简单的缓存系统。这样既能保证能够尽可能多的缓存信息,又可以保证 Java 虚拟机不会因为内存泄露而抛出 OutOfMemoryError 。这种缓存机制特别适合于内存对象生命周期长,且生成内存对象的耗时比较长的情况,例如缓存列表封面图片等。对于一些生命周期较长,但是生成内存对象开销不大的情况,使用WeakReference 能够达到更好的内存管理的效果。
附SoftHashmap 的源码一份,相信看过之后,大家会对 Reference 机制的应用有更深入的理解。
一、Java内存回收机制不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址。Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在Java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。
二、Java内存泄露引起原因
首先,什么是内存泄露看经常听人谈起内存泄露,但要问什么是内存泄露,没几个说得清楚。内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,但有时也会很严重,会提示你Out of memory。
那么,Java内存泄露根本原因是什么呢看长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。具体主要有如下几大类:
1、静态集合类引起内存泄露:
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
例:
Static Vector v = new Vector(10)
for (int i = 1i<100i++)
{
Object o = new Object()
v.add(o)
o = null
}//
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例:
public static void main(String[] args)
{
Set set = new HashSet()
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)
System.out.println("总共有:"+set.size()+" 个元素!")//结果:总共有:3 个元素!
p3.setAge(2)//修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3)//此时remove不掉,造成内存泄漏
set.add(p3)//重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!")//结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person)
}
}
3、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
5、内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b)
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的 *** 作去除引用。
6、单例模式
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在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是个比较复杂的对象或者集合类型会发生什么情况
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)