说起垃圾回收,GC的历史其实比java要久的多。第一门使用内存动态分配和垃圾回收技术的语言是Lisp。
我们需要思考几个问题?
那些内存需要回收?什么时候回收?怎么回收? 给对象中添加一个引用计数器,每当有一个地方引用它是,计数器值就加1;当引用失败时,那么计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的。
客观的说,引用计数算法实现简单,判定效率高,但是它最大的缺点就是难以解决循环引用的问题。
objA.instance = objB objB.instance = objA //上述两者的对象除了彼此之外再无任何引用,但是他们因为互相引用这对方,导致他们的计数器都不为0,所以GC无法回收它们。看我的一个写ThreadLocal的帖子种有了详细的介绍
强引用软引用弱引用虚引用 再跟搜索算法种不可达的对象,也并非是非死不可的,这时候它们还暂时处于缓刑阶段。要真正宣告一个对象死亡,至少要经历两次标记过程:
如果对象在进行根搜索算法以后,发现没有与GC Roots相连接的引用链,那么它将会被进行一次标记并且进行一次筛选,筛选的条件是次对是否有必要执行finalize方法。当对象没有覆盖这个方法,或者这个方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果这个对象被判定为有必要执行finalize方法,那么这个对象会被放入到一个F-Queue的队列中。在这个队列进行可能的二次标记。具体请看本书48页。 很多人认为方法区中(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要虚拟机在方法区中实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其实在新生代中,常规应用进行一次垃圾收集一般可以收集70% - 95%e空间,而永久代的垃圾收集效率远低于此。
永久带的垃圾回收主要分为两个部分:废弃常量和无用的类。
废弃常量
加入一个字符串“abc”已经进入了常量池中,但是差花钱系统没有任何一个Strig对象是叫做“abc"de ,换句话说是没有任何String对象引用常量池中的”abc“常量,也没有其他地方引用了这个字面量,如果在这时候发生了内存回收,而且必要的话,这个常量甚至会被请出常量池。常量池中的其他类、方法、字段的符号引用也是类似
无用的类 判定一个无用的类,必须要同时满足以下三个条件、
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例加载该类的类加载器已经被回收该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法 最基础的收集算法是Mark-Sweep,也就是标记清除算法。具体分为标记和回收两个阶段
首先标记出所有需要回收的对象,在标记完成后她以回收掉所有被标记的对象。缺点: 效率问题:标记和清除的效率都不高空间问题:标记清除之后会产生大量不连续的内存碎片,而不连续的内存碎片会导致程序运行是大对象无法找到足够的连续内存而提前触发下一次的垃圾回收。 为了解决效率问题,一种复制算法出现了
它可以将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。
这样每次都是堆其中一块进行内存回收,内存分配时就不用考虑内存碎片的复杂情况。
优点:实现简单,运行高效。
缺点:浪费了一半的空间,空间利用率太低
现在的虚拟机都是采用这种回收算法来回收新生代,新生代中的对象98%都是朝生夕死的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden和凉快较小的Sruvivor空间,每次使用Eden中的一种一块Survivor,当回收的时候,将Eden和Survivor中还存活的对象一次性拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor的空间。
HotSpot虚拟机的比例为8:1:1,当Survivor空间不够时,可以使用老年代进行分配担保。
复制算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。
更关键的时,如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存货的极端情况,所以在老年代一般不适用这种算法。
这里我们就提出了标记整理算法,标记过程仍然与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
当前商业虚拟机的垃圾收集都是采用分代收集的算法,这种算法就是根据对象的存货周期的不同将内存划分为几块,一般是把java对分为新生代和老年代,然后根据各个年代的特点用最适当的收集算法来收集死去的对象。
如果两个收集器之间存在连线,就说明它们可以搭配使用。
Serial是最基本、历史最为悠久的收集器,曾经是虚拟机新生代收集的唯一选择。这个收集器是一个单线程的收集器,它单线程的意义不仅仅是说明它只会用一个CPU或者一条收集线程全完成垃圾收集工作,更重要的是在它进行垃圾收集时必须暂停其他我有的工作线程,(Stop The World)直到它收集结束。
Serial随着越来越多的优秀的收集器的出现仿佛变得不是那么有竞争力了,但是其实到现在为止,Serial仍然是运行在Client模式下的默认新生代的收集器。它最大的优点就是简单而高效,没有过多的线程交互和线程开销。
ParNew其实就是Serial的多线程版本了,处理使用多条线程进行垃圾收集之外,其余的控制参数、收集算法、对象分配规则、回收策略都和Serial收集器一摸一样。parNew时运行在Server模式下虚拟机中首选的新生代收集器,目前只有它能和CMS收集器配合工作。
是一个新生代的收集器,他也是使用复制算法的收集器,又是并行的多线程收集器。其他的收集器关注点往往都是尽可能地缩短垃圾收集时用户线程的停顿时间,而它则是达到一个可控制的吞吐量Throughput,所谓的吞吐量就是CPU用于运行用户代码的时间与CPU熊小号时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间),如果虚拟机总共运行了100分钟,垃圾回收使用了1分钟,那么吞吐量就是99%。
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;
高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
自适应调节策略也是Parallel Scavenge与ParNew的一个重要区别。
Serial Old时Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理 算法,这个客户端是在Client模式下的虚拟机使用。
Parallel 是 Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。
CMS收集器是一种获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或者BS系统的服务端上,这类应用尤其重视服务的响应速度。它是基于标记-清除算法实现的,整个回收过程分为四个步骤。
初始标记并发标记重新标记并发清除这里我们做一些说明:
其中初始标记、重新标记这两个步骤仍然需要Stop The World。初始标记仅仅知识标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集线程都可以与用户线程一起工作,所以CMS收集器的内存回收过程是与用户线程一起并发地执行的。CMS的一些优点和缺点:
优点:并发收集,低停顿缺点 CMS收集器对CPU资源非常敏感CMS收集器无法处理浮动垃圾可能会产生大量的内存碎片G1收集器是当前收集器技术发展的最前沿成果。
基于标记整理算法实现可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,小号在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java的垃圾收集器的特征了。G1将整个JAVA堆(包括新生代和老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积成都,在后台维护一个优先列表,冒充根据允许的收集时间,优先回收垃圾最多的区域。大多数情况下,对象在新生代Eden中分配。当Eden中没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
所谓的大对象就是,需要大量连续内存空间的Java对象 ,最典型的大对象就是那种很长的字符串和数组。
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,将对象的年龄设置为1,那么每经过一次Minor GC,年龄就会增加一岁,默认年龄15岁时,就会被晋升到老年代中。这个年龄阈值可以修改。
为了更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到阈值才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
在发生Minor GC时,虚拟机会监测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则会直接进行一次Full GC,如果小于,则查看是否允许担保失败,如果允许,那么只会进行Minor GC,如果不允许,那么就进行Full GC。