JAVA的GC机制 (转)

JAVA的GC机制(1)

   一。谁在做Garbage Collection?

    一种流行的说法:在C++里,是系统在做垃圾回收;而在Java里,是Java自身在做。

    在C++里,释放内存是手动处理的,要用delete运算符来释放分配的内存。这是流行的说法。确切地说,是应用认为不需要某实体时,就需用delete告诉系统,可以回收这块空间了。这个要求,对编码者来说,是件很麻烦、很难做到的事。随便上哪个BBS,在C/C++版块里总是有一大堆关于内存泄漏的话题。

    Java采用一种不同的,很方便的方法:Garbage Collection.垃圾回收机制放在JVM里。JVM完全负责垃圾回收事宜,应用只在需要时申请空间,而在抛弃对象时不必关心空间回收问题。

    二。对象在啥时被丢弃?

    在C++里,当对象离开其作用域时,该对象即被应用抛弃。

    是对象的生命期不再与其作用域有关,而仅仅与引用有关。

    Java的垃圾回收机制一般包含近十种算法。对这些算法中的多数,我们不必予以关心。只有其中最简单的一个:引用计数法,与编码有关。或者有一种探测是否有"活"的对象的方法,之后用"自适应"的回收技术[后面将会说到]。

    一个对象,可以有一个或多个引用变量指向它。当一个对象不再有任何一个引用变量指向它时,这个对象就被应用抛弃了。或者说,这个对象可以被垃圾回收机制回收了。

    这就是说,当不存在对某对象的任何引用时,就意味着,应用告诉JVM:我不要这个对象,你可以回收了。

    JVM的垃圾回收机制对堆空间做实时检测。当发现某对象的引用计数为0时,就将该对象列入待回收列表中。但是,并不是马上予以销毁。

    三。丢弃就被回收?

    该对象被认定为没有存在的必要了,那么它所占用的内存就可以被释放。被回收的内存可以用于后续的再分配。

    但是,并不是对象被抛弃后当即被回收的。JVM进程做空间回收有较大的系统开销。如果每当某应用进程丢弃一个对象,就立即回收它的空间,势必会使整个系统的运转效率非常低下。

    前面说过,JVM的垃圾回收机制有多个算法。除了引用计数法是用来判断对象是否已被抛弃外,其它算法是用来确定何时及如何做回收。JVM的垃圾回收机制要在时间和空间之间做个平衡。

    因此,为了提高系统效率,垃圾回收器通常只在满足两个条件时才运行:即有对象要回收且系统需要回收。切记垃圾回收要占用时间,因此,Java运行时系统只在需要的时候才使用它。因此你无法知道垃圾回收发生的精确时间。

    四。没有引用变量指向的对象有用吗?

    前面说了,没挂上引用变量的对象是被应用丢弃的,这意味着,它在堆空间里是个垃圾,随时可能被JVM回收。

    不过,这里有个不是例外的例外。对于一次性使用的对象(有些书称之为临时对象),可以不用引用变量指向它。举个最简单也最常见的例子:

    System.out.println(“I am Java!”);

    就是创建了一个字符串对象后,直接传递给println()方法。

    五。应用能干预垃圾回收吗?

    许多人对Java的垃圾回收不放心,希望在应用代码里控制JVM的垃圾回收运作。这是不可能的事。对垃圾回收机制来说,应用只有两个途径发消息给JVM。

第一个前面已经说了,就是将指向某对象的所有引用变量全部移走。这就相当于向JVM发了一个消息:这个对象不要了。

第二个是调用库方法System.gc(),多数书里说调用它让Java做垃圾回收。

    第一个是一个告知,而调用System.gc()也仅仅是一个请求。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。

    希望JVM及时回收垃圾,是一种需求。其实,还有相反的一种需要:在某段时间内最好不要回收垃圾。要求运行速度最快的实时系统,特别是嵌入式系统,往往希望如此。

    Java的垃圾回收机制是为所有Java应用进程服务的,而不是为某个特定的进程服务的。因此,任何一个进程都不能命令垃圾回收机制做什么、怎么做或做多少。

    六。对象被回收时要做的事

    一个对象在运行时,可能会有一些东西与其关连。因此,当对象即将被销毁时,有时需要做一些善后工作。可以把这些操作写在finalize()方法(常称之为终止器)里。

    protected void finalize()

    {

            // finalization code here

    }

    这个终止器的用途类似于C++里的析构函数,而且都是自动调用的。但是,两者的调用时机不一样,使两者的表现行为有重大区别。C++的析构函数总是当对象离开作用域时被调用。这就是说,C++析构函数的调用时机是确定的,且是可被应用判知的。但是,Java终止器却是在对象被销毁时。由上所知,被丢弃的对象何时被销毁,应用是无法获知的。而且,对于大多数场合,被丢弃对象在应用终止后仍未销毁。

    在编码时,考虑到这一点。譬如,某对象在运作时打开了某个文件,在对象被丢弃时不关闭它,而是把文件关闭语句写在终止器里。这样做对文件操作会造成问题。如果文件是独占打开的,则其它对象将无法访问这个文件。如果文件是共享打开的,则另一访问该文件的对象直至应用终结仍不能读到被丢弃对象写入该文件的新内容。

    至少对于文件操作,编码者应认清Java终止器与C++析构函数之间的差异。

    那么,当应用终止,会不会执行应用中的所有finalize()呢?据Bruce Eckel在《想想 in Java》里的观点:“到程序结束的时候,并非所有收尾模块都会得到调用”。这还仅仅是指应用正常终止的场合,非正常终止呢?

    因此,哪些收尾操作可以放在finalize()里,是需要酌酎的。如前文所述!

http://blog.csdn.net/rujielaisusan/article/details/4571156

 

JAVA的GC机制(2) -- 对象存储

其实要了解对象的释放,需要了解一下对象的存储,即对象是怎么样进行放置安排的呢?特别是内存中究竟是如何进行分配的呢?有五个不同的地方可以用来存储数据:

  1. 寄存器。这是最快的存储区,因为它位于不同于其它地方的存储区位置——CPU内部。但是寄存器的数量是有限的,所以寄存器会根据需要进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象。(C/C++是允许通过编译器建议寄存器的分配方式)
  2. 栈。位于RAM中,但是通过栈指针可以从处理器那里获得直接的支持。栈指针向下移动,则分配新的存储空间;向上移动,则释放掉那些内存。这是一种快速有效的内存分配方式,仅次于寄存器。创建程序时,JAVA系统必须知道存储在栈所有项的确切的生命时期,以便于向下或者向上移动栈指针。这一约束也就限制了程序的灵活性,所以只有某些JAVA数据存储在栈中——基本数据类型以及对象引用
  3. 堆。一种通用的内存池,这个也位于RAM中,用于存放JAVA所有的对象。堆不同于栈的好处是:编译器不需要知道存储的数据在堆内要存活多长的时间。因此,在堆里分配存储有很大的灵活性。当需要一个对象的时候,用new写一行代码,执行代码之后,会自动在堆中进行存储的分配。当然,为这种灵活性必须要付出的代价是:用堆进行存储分配和清理可能比用栈进行存储分配和清理花上的时间要多。堆是线程共同拥有的,但是栈确实线程独有的。
  4. 常量存储。常量值通常直接存放在程序代码内部。这样做是安全的,因为它们永远都不会被改变有时,在嵌入式系统中,常量本身会和其它部分隔开,所以在这种情况下,可以选择其他存放在ROM中。
  5. 非RAM存储——持久化数据。如果数据完全存活在程序之外,那么它可以不受程序的控制。在程序没有运行时也可以存在,也就是持久性数据。其中,两个非常基本的例子——“流对象”和“持久化对象”。在流对象中,对象转化成字节流,通常被发送到另一台机器上。而在“持久化对象”中,对象将被存储在磁盘中,比如文件或者数据库。因此,即使程序终止了,他们仍然可以保持自己的数据状态。这种存储方式的技巧在于——把对象转化成可以存放在其他媒介上的事物,在需要的时候可以恢复成常规的、存储在 RAM的对象。JAVA提供了对轻量级持久化的支持,比如JDBC以及Hibernate这样的机制提供了更加复杂的对存储在数据库以及读取对象信息的支持。
 

JAVA中GC机制(3)——GC如何工作

经过前面对GC的一些基本介绍以及JAVA中数据存储位置的介绍,多少对GC有一些了解。那么现在具体了解一下GC究竟是如何工作的。我们首先会下意识地想到,GC要工作是针对那些存储在堆上的数据进行内存释放的。那么会涉及两个问题——如何发现?如何释放?

其实我们了解到,在堆上进行内存的释放代价是非常昂贵的,也就是GC工作的时候代价是很高的。但是在堆上分配内存却提高了创建对象的速度。

JAVA中对象分配对象和释放对象,不像C++,会由程序自己管理对象的销毁之后会将对象占用的空间加以利用。但是JVM中堆的实现截然不同:它更像是一个传送带,每分配一个新的对象,它就会向前移动一格。这意味着对象存储空间的分配速度是非常快的。但为了防止在频繁的页面调度,也就有了垃圾处理器的介入,它工作的时候,一面在回收空间,一面在使堆中的对象紧凑排列。这样“堆指针”就可以很容易的移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高效的、有无限空间可供分配的堆模型。

前面有说到“引用计数”,但其实这一种标记方法会存在一个问题——死锁。所以任何一种JVM都没有采用这种机制。

==================================================================

如何发现?

在JVM中如何发现一个“活”的对象:

因为对任何一个“活”的对象,都可以追溯到其存活在栈或静态存储区域之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从栈和静态存储区开始,遍历所有的引用,就可以找到所有 “活”的对象,也就是那些可以被realize的对象。对于发现的每个引用,必须追溯它所引用的对象,然后是此对象包含的所有引用,如此进行反复,直到 “根源于栈和静态存储区的引用”所形成的网络全部被访问到位置。那么如果这种机制没有被发现的对象,就是会被自动回收的对象。

==================================================================

如何清理?

JVM中使用的是“自适应”的垃圾回收技术——自适应的、分代的、停止—复制、标记—清扫。

停止-复制 (stop - copy):

意味着,先暂停程序的运行(因此这不属于后台回收模式)。然后将所有的对象从当前堆中复制到另外一个堆中,没有复制的对象全部都是垃圾。那么复制到新堆中的对象全部都是紧挨着的,同时还需要修改相应的引用地址。

这种方法存在的问题:一,需要两个堆,并且要在两个堆中导入导出,维护比实际需要多一倍的空间。某些JVM对这种问题的处理是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存中。二,复制。由于可能程序比较稳定,没有多余的垃圾。但是仍然需要进行复制。有些JVM对这种问题的处理是,切换到下面要说的mark-sweep方式。

标记-清扫 (mark-sweep)  很慢,也不是后台处理方式

标记——从栈和静态存储区出发,遍历所有的引用,进而找出所有realize的对象。每当它找到一个存活对象,就会对这个对象进行标记。这个过程中不会释放任何的对象。当所有的标记对象都做完的时候,清理动作就开始。

清理——没有标记的对象被释放,不会发生任何的复制动作。所以剩下的堆是不连续的。垃圾回收器要是希望得到连续的空间的话,就得整理剩下的对象。

因此JVM采用的方式是:如果所有的对象都很稳定,GC的效率降低的话,就切换到mark-sweep方式,当发现堆中有很多的碎片的话,就切换到stop-copy方式。这就是自适应。

http://blog.csdn.net/rujielaisusan/article/details/4571159

 

JAVA的GC机制(4) ——内存泄漏

所有与main()进程不再有任何引用关系的对象都被JVM视作垃圾,并会被适时回收;而 如果在需要废弃一个对象的时候,引用关系解除得不彻底,就会发生非预期的内存占用,即泄露。

  • 有没有什么工具能帮忙查出某个对象是否确实不再被引用、仅仅只是在等待GC来吃?
  • 或者有什么办法实时地显示出JVM内部所有对象之间的引用关系?

举例:

  • 有一个全局范围的容器Collection<Object> c。
  • 在局部范围内创建一个Object o,此时JVM从堆上划分一块内存创建对象XXX,交给o引用。
  • 因为某些需要,或者特殊情况,o被添加进c里,则此时c也会引用对象XXX。
  • 当o引用的对象XXX完成使命后,开发者为o赋了新值null,此时o不再引用对象XXX。
  • 由于某些情况,开发者忽略了步骤3,或者步骤3是背着开发者进行的,又或者开发者没有权限从c中删除对象XXX,则c将一直保持到对象XXX的引用,而对象XXX所占据的内存空间在进程生命周期内永远无法释放。
  • 凑巧,这段代码依据一定的规则循环执行,则c内会继续添加对对象YYY、ZZZ等的引用,并且在这些对象的使命完成后也因为同样的原因而不释放空间。
  • 天长日久,系统越来越慢,终于有一天,OutOfMemoryError,成为注定的结局……


这种状况在使用外来组件时尤其容易发生,比如JDK的Logger。

所以需要尽量避免这样的方式。

http://blog.csdn.net/rujielaisusan/article/details/4571165



posted @ 2012-03-26 10:19  EileenLiu  阅读(383)  评论(0编辑  收藏  举报