jvm垃圾收集算法简介

1         Tracing算法

tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器。

 

2         Compacting算法

compacting算法与tracing算法配合使用解决了堆碎片问题。基于tracing的垃圾回收吸收了compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用 在新的位置能识别原来 的对象。在基于compacting算法的收集器的实现中,一般增加句柄和句柄表。(引自《深入Java虚拟机》)

 

3         Copy算法

拷贝算法将一个堆分成两半,每一半叫做半区。其中一个半区叫做当前区,用来内存分配。当垃圾收集运行时,有生命的内存单元从当前区找出,然后拷贝到另一个半区。当完成时,另一个半区只包含活着的单元(没有垃圾)因而成为一个新的活跃半区。在这个过程中,单元被自动的整合。不能接近的单元(垃圾)没有被拷贝,因而自动被回收。拷贝算法高效的给新的对象分配空间,并且消灭了碎片。但是另一方面,拷贝算法只让应用程序使用一半的内存 ,每次都拷贝所有活着的对象,这对于采用分页式内存管理的系统来说会导致巨大的页面失效。

一般的拷贝收集器算法被称为“停止并拷贝”。在这个方案中,堆被分为两个区域,任何时候都只使用其中的一个区域。对象在同一个区域中分配,直到这个区域被耗尽。此时,程序执行被中止,堆被遍历,遍历时遇到的活动对象被拷贝到另外一个区域。当停止和拷贝过程结束时,程序恢复执行。内存将从新的堆区域中分配,直到它也被用尽。那时程序将被再次中止,遍历堆,活动对象又被拷贝回原来的区域。这种方法带来的代价就是,对于指定大小的堆来说需要两倍大小的内存,因为任何时候都只能使用其中的一半。(引自《深入Java虚拟机》)



 

图表 3  Copy算法模拟图

 

 

4         Counting算法

引用计数法是唯一没有使用根集的垃圾回收得法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1。当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,必须实时运行。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量 ,计数器加1,而每次现有对象出了作用域生,计数器减1

 

5         Generational算法

stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

 

6         Adapting算法

在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。使用自适用方法,Java虚拟机实现的设计者不需要只选择一种特定的垃圾收集算法。可以使用多种技术,以便在每种技术最擅长的场合使用它们。(引自《深入Java虚拟机》

7    JDK1.4.1的垃圾收集算法

JVM 1.2 及以后版本使用的技术称为分代垃圾收集器generational garbage collection,它结合了这两种技术以结合二者的长处,结果就是对象分配开销非常小。

 

7.1 老对象和年轻对象

在任何一个应用程序堆中,一些对象在创建后很快就成为垃圾,另一些则在程序的整个运行期间一直保持生存。经验分析表明,对于大多数面向对象的语言,包括 Java 语言,绝大多数对象——可以多达 98%(这取决于您对年轻对象的衡量标准)是在年轻的时候死亡的。可以用时钟秒数、对象分配以后甴内存管理子系统分配的总字节或者对象分配后经历的垃圾收集的次数来计算对象的寿命。但是不管您如何计量,分析表明了同一件事——大多数对象是在年轻的时候死亡的。大多数对象在年轻时死亡这一事实对于收集器的选择很有意义。特别是,当大多数对象在年轻时死亡时,复制收集器可以执行得相当好,因为复制收集器完全不访问死亡的对象,它们只是将活的对象复制到另一个堆区域中,然后一次性收回所有的剩余空间。

那些经历过第一次垃圾收集后仍能生存的对象,很大部分会成为长寿的或者永久的对象。根据短寿对象和长寿对象的混合比例,不同垃圾收集策略的性能会有非常大的差别。当大多数对象在年轻时死亡时,复制收集器可以工作得很好,因为年轻时死亡的对象永远不需要复制。不过,复制收集器处理长寿对象却很糟糕,它要从一个半空间向另一个半空间反复来回复制这些对象。相反,标记-整理收集器对于长寿对象可以工作得很好,因为长寿对象趋向于沉在堆的底部,从而不用再复制。不过,标记清除和标记整理收集器要做很多额外的分析死亡对象的工作,因为在清除阶段它们必须分析堆中的每一个对象。

 

7.2小的收集

分代收集的一个优点是它不同时收集所有的代,因此可以使垃圾收集暂停更短。当分配器不能满足分配请求时,它首先触发一个小的收集(minor collection,它只收集最年轻的代。因为年轻代中的许多对象已经死亡,复制收集器完全不用分析死亡的对象,所以小的收集的暂停可以相当短并通常可以回收大量的堆空间。如果小的收集释放了足够的堆空间,那么用户程序就可以立即恢复。如果它不能释放足够的堆空间,那么它就继续收集上一代,直到回收了足够的内存。(在垃圾收集器进行了全部收集以后仍不能回收足够的内存时,它将扩展堆或者抛出(out of memory)

 

7.3 代间引用

跟踪垃圾收集器,如复制、标记-清除和标记-整理等垃圾收集器,都是从根集(root set)开始扫描,遍历对象间的引用,直到访问了所有活的对象。

分代跟踪收集器从根集开始,但是并不遍历指向更老一代中对象的引用,这减少了要跟踪的对象图的大小。但是这也带来一个问题——如果更老一代中的对象引用一个不能通过从根开始的所有其他引用链到达的更年轻的对象该怎么办?

为了解决这个问题,分代收集器必须显式地跟踪从老对象到年轻对象的引用并将这些老到年轻的引用加入到小的收集的根集中。有两种创建从老对象到年轻对象的引用的方法。要么是将老对象中包含的引用修改为指向年轻对象,要么是将引用其他年轻对象的年轻对象提升为更老的一代。



  

 

7.4 卡片收集

Sun JDK 使用一种称为 卡片标记(card marking)算法的改进算法以标识对老一代对象的字段中包含的指针的修改。在这种方法中,堆分为一组卡片,每个卡片一般都小于一个内存页。JVM 维护着一个卡片映射,对应于堆中的每一个卡片都有一个位(在某些实现中是一个字节)。每次修改堆中对象中的指针字段时,就在卡片映射中设置对应那张卡片的相应位。在垃圾收集时,就对与老一代中卡片相关联的标记位进行检查,对脏的卡片扫描以寻找对年轻代有引用的对象。然后清除标记位。卡片标记有几项开销——卡片映射所需的额外空间、对每一个指针存储所做的额外工作,以及在垃圾收集时做的额外工作。对每一个非初始化堆指针存储,卡片标记算法可以只增加两到三个机器指令,并要求在小的收集时对所有脏卡片上的对象进行扫描。

 

你可能感兴趣的:(jvm,jdk,算法,虚拟机,活动)