JAVA垃圾回收器怎么判断哪个变量需要回收

来源:互联网 发布:js 对象按key值排序 编辑:程序博客网 时间:2024/04/30 22:57

首先要明确几点:

Java是在堆上为对象分配空间的

  垃圾回收器只跟内存有关,什么IO啊,网络连接啊,管它P事

  当可用内存数量较低时,Sun版本的垃圾回收器才会被激活

  在垃圾回收器回收垃圾之前,我们先来了解一下Java分配对象的方式,Java的堆更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度相当快。Java的"堆指针"只是简单地移动到尚未分配的领域。也就是说,分配空间的时候,"堆指针"只管依次往前移动而不管后面的对象是否还要被释放掉。如果可用内存耗尽之前程序就退出就再好不过了,这样的话垃圾回收器压根就不会被激活。

  但是由于"堆指针"只管依次往前移动,那么你肯定会想,总有一天内存会被耗尽,垃圾回收器就开始释放内存。这里有人肯定会问:怎么判断某个对象该被回收呢?答案就是当堆栈或静态存储区没有对这个对象的引用时,就表示程序(员)对这个对象没有兴趣了,它就应该被回收了。有两种方法来知道这个对象有没有被引用:第一种是遍历堆上的对象找引用;第二种是遍历堆栈或静态存储区的引用找对象。前者的实现叫做"引用计数法",意思就是当有引用连接至对象时,引用计数加1,当引用离开作用域或被置为null时,引用计数减1,这种方法有个缺陷,如果对象之间存在循环引用,可能会出现"对象应该被回收,但引用计数却不为零"的情况。

  Java采用的是后者,在这种方式下,Java虚拟机采用一种"自适应"的垃圾回收技术,如何处理找到的存活对象(也就是说不是垃圾),Java有两种方式:

  一种是"停止-复制":理论上是先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。当对象被复制到新堆上时,它们是一个挨着一个的,所以新堆保持紧凑排列(这也是为什么分配对象的时候"堆指针"只管依次往前移动)。然后就可以按前述方法简单、直接地分配内存了。这将导致大量内存复制行为,内存分配是以较大的"块"为单位的。有了块之后,垃圾回收器就可以不往堆里拷贝对象了,直接就可以往废弃的块里拷贝对象了。

  另一种是"标记-清扫":它的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象一个标记。这个过程中不会回收任何对象。只有全部标记完成时,没有标记的对象将被释放,不会发生任何复制工作,所以剩下的堆空间是不连续的,然后垃圾回收器重新整理剩余的对象,使它们是连续排列的。

  当垃圾回收器第一次启动时,它执行的是"停止-复制",因为这个时刻内存有太多的垃圾。然后Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到"标记-清扫"方式;同样,Java虚拟机会跟踪"标记-清扫"效果,要是堆空间出现很多碎片,就会切换到"停止-复制"方式。这就是所谓的"自适应"技术。

  其实仔细想一下,"停止-复制"和"标记-清扫"无非就是:"在大量的垃圾中找干净的东西和在大量干净的东西里找垃圾"。不同的环境用不同的方式,这样做完全是为了提高效率,要知道,无论哪种方式,Java都会先暂停程序的运行,所以,垃圾回收器的效率其实是很低的。Java用效率换回了C++没有的垃圾回收器和运行时的灵活,我认为这是明智的选择(虽然它只跟内存有关),随着硬件的飞速发展,我相信,开发时间要比运行效率重要得多!

垃圾回收算法

  到这里大家可能会问垃圾收集器是如何知道哪些对象是存活的(需要的),而哪些对

 

  象是死亡的(不需要的)呢?许多Java的垃圾收集器都使用了引用的根集,作为分析对象

 

  存活与否的依据。引用的根集是正在执行的Java程序随时都可以访问的引用的变量的集合

 

  ――也就是存在堆栈(Stack)或是静态存储空间(Static storage)上的引用变量。从这些

 

  根集变量出发可直接或是间接到达的对象,垃圾收集器会认为这些对象是生命尚存的对象

 

  ;相对的从这些根集变量出发通过任意途径都无法到达的对象,就是死亡的,它们就会成

 

  为下一次垃圾收集的对象。具体过程就像这样:从堆栈(Stack)和静态存储区域(Static

 

  storage)开始,走访所有引用之后,就能找出所有活动的对象。对于自己找到的每个引用

 

  ,都必须再跟踪到它指向的那个对象,然后跟随那个对象中的所有引用,"跟踪追击"到

 

  它们指向的对象,如此反复,直到遍历了根源于堆栈或静态存储区域中的引用所形成的整

 

  个网络为止。中途经历的每个对象都必须仍处于活动状态。

 

  对于垃圾收集器找到的那些活动对象,具体应采取哪些操作呢?这正是我们接下去要

 

  讨论的,大部分垃圾收集器都是基于几种算法的工作方式,并且它们是自适应的,也就是

 

说他们会根据情况自动的选择恰当的工作方式。

1、基于Copying算法的"停止而后复制(stop-and-copy)"方式:就像字面意思一

 

  样,当一个内存堆满了,程序首先会停止运行。随后,基于Copying算法的垃圾收集器从根

 

  集变量中跟踪所有存活的对象,并将已找到的每个活动对象从一个内存堆复制到另一个,

 

  留下所有的垃圾。除此以外,随着对象复制到新堆,它们会一个接一个地首尾聚焦在一起

 

  。这样可使新堆显得更加紧凑(并使新的存储区域可以简单地从最末端腾出空间,就像前

 

  面讲述的那样)。当然,将一个对象从一处挪到另一处时,指向那个对象的所有引用(引

 

  用变量)都必须改变。对于那些通过跟踪内存堆的对象而获得的引用,以及那些静态存储

 

  区域,都可以立即改变。但在"遍历"过程中,还有可能遇到指向这个对象的其他引用。

 

一旦发现这个问题,就得通过后续过程才能找到。

 

  但是在有的情况下将使得"复制式垃圾收集器"显得效率极为低下,比如随着程序进

 

  入了稳定状态之后,它几乎不产生或产生很少的垃圾。尽管如此,这时的一个"复制式垃

 

  圾收集器"仍会将所有内存从一处复制到另一处,这显得非常浪费。更糟的是:程序中的

 

  对象不仅不死亡,而且一个个都很庞大,对两个大的内存堆管理也将成为不必要的消耗。

 

2、基于Tracing算法、Compacting算法的"标记和清除(make-and-sweep)"方式

 

  :对于常规性的应用,标记和清除显得非常慢,但一旦知道自己不产生垃圾,或者只产生

 

  很少的垃圾,它的速度就会非常快。标记和清除(make-and-sweep)采用的逻辑:基于

 

  Tracing算法从堆栈和静态存储区域开始,并跟踪所有引用,寻找活动对象。然而,每次发

 

  现一个活动对象的时候,就会设置一个标记(一个位或多个位),为那个对象作上"记号

 

  "。但此时尚不收集那个对象。只有在标记过程结束,清除过程才正式开始。在清除过程

 

  中,不复存活的对象会被释放然而,不会进行任何形式的复制(这时的堆中被使用的空间

 

  呈现不连续的状态)。所以假若收集器决定使这个断续的内存堆密集(compact),它将使

 

  用Compacting算法重新整理他所找到的对象:垃圾收集器将所有的对象移置堆的一端。堆

 

  的另一端就变成了一个相邻的空闲内存区。收集器会对它移动的所有对象的所有引用进行

 

  更新,这样这些引用能够在新的位置识别同样的对象。为了简化对象引用的更新,compac

 

  ting算法增加了间接的一层(level of indirection)。间接层通过句柄(handle)和句

 

  柄表实现。在这种情况下,对象引用总是指向句柄表中同样的句柄入口。反过来,句柄入

 

  口包含了句柄的实际引用。当compacting标记和清除垃圾收集器移动对象时,收集器在句

 

  柄表中只修改实际的对象引用对句柄入口的所有引用没有受到影响。虽然使用句柄表简化

 

  了堆碎片,访问每个对象增加了额外的开销这一缺点,这使得程序变慢。

 

3、Generational算法:前面提到的"停止而后复制(stop-and-copy)"方式的一

 

  个缺点是收集器必须复制所有的存活对象,这就在程序执行之前增加了程序等待时间。在

 

  实际中,我们可以发现:a、大多数程序对象存在的时间比较短,b、大多数程序创建了少

 

  量的长时间存在的对象。Generational算法用以下的方法克服重复的复制长时间存在的对

 

  象而带来的低效率:它们将内存堆分成两个或多个子堆,每个子堆都作为对象的一代(ge

 

  neration)。垃圾收集更频繁地发生在代表年轻一代的子堆上。因为大多数程序对象存在

 

  的时间都比较短,程序将逐渐不引用这些对象,垃圾收集器将从最年轻的子堆中收集他们

 

  。在分代式的(generational)垃圾收集器运行一段时间后,上次运行中存活下来的对象

 

  移动到下一个最高代的子堆中。每个日益增多的老一代子堆不会经常进行垃圾收集,因此

 

  能够节约时间。 SUN的JVM中Garbage collector使用的是一种修改过的分代方式管理较

 

  老的对象空间:引入了一种称为Incremental(train)增量式的算法。因为GC在JVM中通常

 

  是由一个或一组进程来实现的,它本身也和用户程序一样占用heap空间,运行时也占用CP

 

  U。当GC进程运行时,应用程序停止运行。因此,当GC运行时间较长时,用户能够感到Jav

 

  a程序的停顿,另外一方面,如果GC运行时间太短,则可能对象回收率太低,这意味着还有

 

  很多应该回收的对象没有被回收,仍然占用大量内存。因此,在设计GC的时候,就必须在

 

  停顿时间和回收率之间进行权衡。增量式GC就是通过一定的回收算法,把一个长时间的中

 

  断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然,增量式GC在

 

  整体性能上可能不如普通GC的效率高(通常会使系统速度降低10%左右),但是它能够减少

 

  程序的最长停顿时间。下图就表示了,增量式GC和普通GC的比较。其中灰色部分表示线程

 

占用CPU的时间。

 

  4、Adaptive算法:对于特定的情况,一些垃圾收集算法要优于其他算法。Adaptive算

 

  法监视当前的堆的情况,并基于具体的情况选择适当的垃圾收集方式,或者Adaptive算法

 

  可能要将对象分成子堆,并在每个子堆赋予一个不同的垃圾收集方式。

原创粉丝点击