您的位置:首页 > 编程语言 > Java开发

Java的垃圾收集算法、垃圾收集器以及内存分配与回收策略

2012-12-01 16:20 771 查看
虚拟机是如何判断一个对象已经死去呢?大部分人都回答是引用计数算法。
1. 引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器值就减1,任何时刻计数器值为0的对象就是不可能再被使用的。引用计数法的实现简单,判定效率比较高,但是java中并没有选择引用计数法来管理内存,其中主要的原因是它很难解决对象之间的相互循环引用问题。objA.instance=objB;objB.instance=objA;除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们相互引用,引用计数器无法通知GC收集器回收它们。

2. 根搜索算法
java c#以及Lisp都是使用根搜索算法判定对象是否存活的。这个算法的基本思路就是通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径就称为引用链。当一个对象到引用链没有对象相连时,则证明此对象是不可用的。

对象的死亡历程
当一个对象从GC Roots不可达以后,就可以被回收了,如果类覆盖了finalize方法,就有可能会执行finalize,正常情况下执行完毕后,就面临真正的垃圾回收,如果在finalize方法中,将对象本身赋值给另外一个变量,那就可以逃脱垃圾回收的命运,但是请注意,finalize方法只能调用一次,下一次这个对象再面临垃圾回收的时候就不会再调用finalize方法了。

但是finalize方法中的工作不确定性太大,代价高昂,不鼓励使用这个方法。

垃圾收集算法

1. 标记-清除算法
算法分为标记和清除两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收掉所有被标记对象,是最基本的收集算法。

2. 复制算法
它将内存按容量划分为大小相等的两块,每次只是用其中的一块,当这一块的内存用完了,就将还活着的对象复制到另外一块上边,然后把已经是用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,高效。只是这种算法的代价是将内存缩小为原来的一半。

3. 标记-整理算法
标记过程仍然与“标记-整理算法”一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后清理掉端边界以外的内存。

4. 分代收集算法
当前商业虚拟机都采用这种算法,根据对象的存活周期将内存划分为几块,java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代中,每次垃圾收集都有大批对象死去,可以选用复制算法,只需要付出少量存活对象的复制就可以完成收集。而老年代对象存活率高,没有额外空间,必须使用标记-清理或者标记-整理算法来进行回收。

垃圾收集器

在垃圾回收的语境中,有必要先解释两个名词:并发和并行
并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发:用户线程和垃圾收集线程同时工作,用户程序继续运行,而垃圾收集程序运行于另一个CPU上

1. Serial收集器 采用复制算法
Serial收集器是最基本历史最悠久的收集器,它是一个单线程的收集器,它在垃圾收集的时候必须暂停其他所有的工作线程,直到收集完成。垃圾收集的工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的

但是它也有自己的优点:简单高效 对于限定单个CPU的环境来说,收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

2. ParNew收集器 采用复制算法
ParNew收集器其实就是Serial收集器的多线程版本,包括收集器可用的控制参数、收集算法、暂停所有用户线程、对象分配策略、回收策略等都和Serial收集器完全一样,它是运行在Server模式下的虚拟机中首选的新生代收集器,其中一个原因是,除了Serial收集器,目前只有它能和CMS收集器配合使用。CMS收集器是HotSpot虚拟机上第一款真正意义上的并发收集器

ParNew收集器在单CPU环境中不会比Serial收集器有更好的效果

3. Parallel Scavenge收集器 采用复制算法
Paraller Scavenge收集器也是一个新生代收集器,使用复制算法的收集器,是一个并行的多线程收集器

Parellel Scavenge收集器的特点是它的关注点和其它收集器不同,CMS等收集器尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机运行了100分钟,垃圾收集花掉1分钟,吞吐量是99%

停顿时间越短越适合与用户交互的程序,良好的响应速度能提升用户的体验;高吞吐量可以更高效的利用CPU时间

Parallel Scavenge收集器有一种称为GC自适应调用策略,虚拟机根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量

自适应策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

4. Serial Old收集器 采用标记-整理算法
它是Serial收集器的老年版本,单线程收集器,采用标记-整理算法。

5. Parallel Old收集器 采用标记-整理算法
Parallel Old是Parallel Scavenge收集器的老年版本,使用多线程和标记-整理算法。

6. CMS收集器 采用标记-清理算法
CMS收集器是一种以获得最短回收停顿时间为目标的收集器。它分为四个步骤:初始标记、并发标记、重新标记、并发清除其中初始标记和重新标记两个步骤任然需要暂停其它线程。

初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段一般比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清理过程中,收集器都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用于线程一起并发执行的。





CMS收集器的缺点:
CMS收集器对CPU资源非常敏感,面向并发设计的程序都对CPU资源比较敏感,虽然并发阶段不会导致应用线程停顿,但是会因为占用了一部分线程导致应用程序编码,总吞吐量会降低。

CMS是基于标记-清理算法,收集结束的时候会产生大量空间碎片,空间碎片过多的时候将会给大家分配带来很大的麻烦,玩玩会出现老年代有很大剩余空间,但是无法找打足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

7. G1收集器 采用标记-整理算法
G1收集器是收集器理论进一步发展的产物,它与CMS收集器相比有两个重要的改进:G1收集器基于标记-整理算法,不会产生空间碎片,对于长时间运行的应用程序来说非常重要。二是,它可以非常精确的控制停顿,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,它能够避免全区域的垃圾收集。

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,少数情况下也可能会直接分配在老年代中

1. 对象优先在Eden分配
对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC

2. 大对象直接进入老年代
所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组

3. 长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,当年龄增加到一定程序时,就会被晋升到老年代中

新生代使用复制收集算法
Minor GC:发生在新生代的垃圾收集动作,Java对象大多都具有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快

Major GC/Full GC:发生在老年代的GC,Major GC的速度一般会比Minor GC慢10倍以上。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: