关于java内存泄漏的经典文章(一)

一 问题的提出
       Java 的一个重要优点就是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使 有内存泄漏也不是程序的责任,而是GC或JVM的问题。其实,这种想法是不正确的,因为Java也存在内存泄露,但它的表现与C++不同。
随着越来越多的服务器程序采用Java技术,例如JSP,Servlet, EJB等,服务器程序往往长期运行。另外,在很多嵌入式系统中,内存的总量非常有限。内存泄露问题也就变得十分关键,即使每次运行少量泄漏,长期运行之后,系统也是面临崩溃的危险。
二Java是如何管理内存
       为了判断Java中是否有内存泄露,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。 分配内存的方式多种多样,取决于该种语言的语法结构。但不论是哪一种语言的内存分配方式,最后都要返回所分配的内存块的起始地址,即返回一个指针到内存块的首地址。 在 Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是由GC完成的,这种收支两条线 的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。为 了更好理解GC的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为一 个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对 象,GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
       以下,我们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示意图。
       ava 使 用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种方式的优 点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很 难处理循环引用的问题),但执行效率很高
       在 Java 中,所有对象都驻留在堆内存,因此局部对象就不存在。当你创建一个对象时, Java 虚拟机( JVM )为该对象分配内存、调用构造器并开始跟踪你使用的对象。当你停止使用一个对象(就是说,当没有对该对象有效的引用时), JVM 通过垃圾回收器将该对象标记为释放状态。当垃圾回收器要释放一个对象的内存时,它首先调用该对象的 finalize() 方法(如果该对象定义了此方法的话)。垃圾回收器以独立的低优先级的方式运行,所以只有当其他线程都挂起等待内存释放的情况出现时,它才开始释放对象的内存。
三java垃圾收集器
       垃圾收集器线程是一种低优先级的线程,在一个 Java 程序的生命周期中,它只有在内存空闲的时候才有机会运行。
       垃圾收集器的特点和它的执行机制: 垃圾收集器系统有自己的一套方案来判断哪个内存块是应该被回收的,哪个是不符合要求暂不回收的。垃圾收集器在一个 Java 程序中的执行是自动的,不能强制执行,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用 System. gc 方法来 " 建议 " 执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
       垃圾收集器的工作是发现应用程序不再需要的对象,并在这些对象不再被访问或引用时将它们删除。从根节点(在 java 应用程序的整个生存周期内始终存在的那些类)开始,遍历被应用的所有节点进行清除。在它遍历这些节点的同时,它跟踪哪些对象当前正被引用着。任何类只要不再被引用,它就符合垃圾收集的条件。当删除这些对象后,就将它们所占用的内存资源返回给 jvm
(1)、垃圾收集器的主要特点:
1 .垃圾收集器的工作目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。
2 .垃圾收集器判断一个对象的内存空间是否无用的标准是:如果该对象不能再被程序中任何一个 " 活动的部分 " 所引用,此时我们就说,该对象的内存空间已经无用。所谓 " 活动的部分 " ,是指程序中某部分参与程序的调用,正在执行过程中,尚未执行完毕。
3 .垃圾收集器线程虽然是作为低优先级的线程运行,但在系统可用内存量过低的时候,它可能会突发地执行来挽救内存资源。当然其执行与否也是不可预知的。
4 .垃圾收集器不可以被强制执行,但程序员可以通过调用 System. gc 方法来建议执行垃圾收集器。
5 .不能保证一个无用的对象一定会被垃圾收集器收集,也不能保证垃圾收集器在一段 Java 语言代码中一定会执行。因此在程序执行过程中被分配出去的内存空间可能会一直保留到该程序执行完毕,除非该空间被重新分配或被其他方法回收。由此可见,完全彻底地根绝内存渗漏体的产生也是不可能的。但是请不要忘记, Java 的垃圾收集器毕竟使程序员从手工回收内存空间的繁重工作中解脱了出来。设想一个程序员要用 C C++ 来编写一段 10 万行语句的代码,那么他一定会充分体会到 Java 的垃圾收集器的优点!
6 .同样没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪一个会被首先收集。
7 .循环引用对象不会影响其被垃圾收集器收集。
8 .可以通过将对象的引用变量( reference variables ,即句柄 handles )初始化为 null 值,来暗示垃圾收集器来收集该对象。但此时,如果该对象连接有事件监听器(典型的 AWT 组件),那它还是不可以被收集。所以在设一个引用变量为 null 值之前,应注意该引用变量指向的对象是否被监听,若有,要首先除去监听器,然后才可以赋空值。
9 .每一个对象都有一个 finalize( ) 方法,这个方法是从 Object 类继承来的。
10 finalize( ) 方法用来回收内存以外的系统资源,就像是文件处理器和网络连接器。该方法的调用顺序和用来调用该方法的对象的创建顺序是无关的。换句话说,书写程序时该方法的顺序和方法的实际调用顺序是不相干的。请注意这只是 finalize( ) 方法的特点。
11 .每个对象只能调用 finalize( ) 方法一次。如果在 finalize( ) 方法执行时产生异常 exception ),则该对象仍可以被垃圾收集器收集。
12 .垃圾收集器跟踪每一个对象,收集那些不可到达的对象(即该对象没有被程序的任何 " 活的部分 " 所调用),回收其占有的内存空间。但在进行垃圾收集的时候,垃圾收集器会调用 finalize( ) 方法,通过让其他对象知道它的存在,而使不可到达的对象再次 " 复苏 " 为可到达的对象。既然每个对象只能调用一次 finalize( ) 方法,所以每个对象也只可能 " 复苏 " 一次。
13 finalize( ) 方法可以明确地被调用,但它却不能进行垃圾收集。
14 finalize( ) 方法可以被重载( overload ),但只有具备初始的 finalize( ) 方法特点的方法才可以被垃圾收集器调用。
15 .子类的 finalize( ) 方法可以明确地调用父类的 finalize( ) 方法,作为该子类对象的最后一次适当的操作。但 Java 编译器却不认为这是一次覆盖操作( overriding ),所以也不会对其调用进行检查。
16 .当 finalize( ) 方法尚未被调用时, System. runFinalization( ) 方法可以用来调用 finalize( ) 方法,并实现相同的效果,对无用对象进行垃圾收集。
此时容易引起一个误解——大家可能想 finalize() 方法是安全的,这时一些重要的事情需要注意:
首先,只有当垃圾回收器释放该对象的内存时,才会执行 finalize() 。如果在 Applet 或应用程序退出之前垃圾回收器没有释放内存,垃圾回收器将不会调用 finalize() 其次,除非垃圾回收器认为 Applet 或应用程序需要额外的内存,否则它不会试图释放不再使用的对象的内存。换句话说,有可能出现这样的情况:一个 Applet 给少量的对象分配了内存,但没有造成严重的内存需求,于是垃圾回收器没有释放这些对象的内存程序就退出了。 显然,如果为某个对象定义了 finalize() 方法, JVM 可能不会调用它,因为垃圾回收器不曾释放过那些对象的内存。即使调用 System.gc() 也可能不会起作用,因为它仅仅是给 JVM 的一个建议而不是命令,所以 finalize() 方法的作用也就不是那么明显。 Java 1.1 中有一个 System.runFinalizersOnExit() 方法部分地解决了这个问题。(不要将这个方法与 Java1.0 中的 System.runFinalizations() 方法相混淆。)不象 System.gc() 方法那样, System.runFinalizersOnExit() 方法并不立即试图启动垃圾回收器。而是当应用程序或 Applet 退出时,它调用每个对象的 finalize() 方法。 结论:不应当依靠垃圾回收器或 finalize() 来执行你的 Applet 和应用程序的资源清除工作。取而代之,应当使用确定的方法来清除那些资源或创建一个 try...finally 块(或类似的机制)来实现。
17 .当一个方法执行完毕,其中的局部变量就会超出使用范围,此时可以被当作垃圾收集,但以后每当该方法再次被调用时,其中的局部变量便会被重新创建。
18 Java 语言使用了一种 " 标记交换区的垃圾收集算法 " 。该算法会遍历程序中每一个对象的句柄,为被引用的对象做标记,然后回收尚未做标记的对象。所谓遍历可以简单地理解为 " 检查每一个 "
19 Java 语言允许程序员为任何方法添加 finalize( ) 方法,该方法会在垃圾收集器交换回收对象之前被调用。但不要过分依赖该方法对系统资源进行回收和再利用,因为该方法调用 后的执行结果是不可预知的。 通过以上对垃圾收集器特点的了解,你应该可以明确垃圾收集器的作用,和垃圾收集器判断一块内存空间是否无用的标准。简单地说,当你为一个对象赋值为 null 并且重新定向了该对象的引用者,此时该对象就符合垃圾收集器的收集标准。
        典型地, GC 不会自动执行,直到程序需要的内存比当前可用内存多时才调用,此时, jvm 将首先尝试激活 GC 以得到更多的可用内存,如果仍得不到充足的可用内存, jvm 将转向从操作系统申请更多的内存,直到最终超过分配的最大内存而导致 java.lang.OutOfMemoryError
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值