一.如何肯定某个对象是“垃圾”?html
二.典型的垃圾收集算法java
三.典型的垃圾收集器程序员
在这一小节咱们先了解一个最基本的问题:若是肯定某个对象是“垃圾”?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使用,那么垃圾收集器如何肯定某个对象是“垃圾”?—即经过什么方法判断一个对象能够被回收了。算法
在java中是经过引用来和对象进行关联的,也就是说若是要操做对象,必须经过引用来进行。那么很显然一个简单的办法就是经过引用计数来判断一个对象是否能够被回收。不失通常性,若是一个对象没有任何引用与之关联,则说明该对象基本不太可能在其余地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。数组
这种方式的特色是实现简单,并且效率较高,可是它没法解决循环引用的问题,所以在Java中并无采用这种方式(Python采用的是引用计数法)。多线程
最后总结一下日常遇到的比较常见的将对象断定为可回收对象的状况:并发
1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象,好比下面的代码:spa
Object obj = new Object(); obj = null; Object obj1 = new Object(); Object obj2 = new Object(); obj1 = obj2;
2)局部引用所指向的对象,好比下面这段代码:线程
void fun() { ..... for(int i=0;i<10;i++) { Object obj = new Object(); System.out.println(obj.getClass()); } }
循环每执行完一次,生成的Object对象都会成为可回收的对象。code
3)只有弱引用与其关联的对象,好比:
WeakReference<String> wr = new WeakReference<String>(new String("world"));
这是最基础的垃圾回收算法,之因此说它是最基础的是由于它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出全部须要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程以下图所示:
从图中能够很容易看出标记-清除算法实现起来比较容易,可是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会致使后续过程当中须要为大对象分配空间时没法找到足够的空间而提早触发新的一次垃圾收集动做。
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程以下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,可是却对内存空间的使用作出了高昂的代价,由于可以使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,若是存活对象不少,那么Copying算法的效率将会大大下降。
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep同样,可是在完成标记以后,它不是直接清理可回收对象,而是将存活对象都向一端移动,而后清理掉端边界之外的内存。具体过程以下图所示:
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。
目前大部分垃圾收集器对于新生代都采起Copying算法,由于新生代中每次垃圾回收都要回收大部分对象,也就是说须要复制的操做次数较少,可是实际中并非按照1:1的比例来划分新生代的空间的,通常来讲是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另外一块Survivor空间中,而后清理掉Eden和刚才使用过的Survivor空间。
而因为老年代的特色是每次回收都只回收少许对象,通常使用的是Mark-Compact算法。
注意,在堆区以外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部份内容:废弃常量和无用的类。
Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,而且在它进行垃圾收集时,必须暂停全部用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优势是实现简单高效,可是缺点是会给用户带来停顿。
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不须要暂停其余用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不一样,它主要是为了达到一个可控的吞吐量。
Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。
G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。所以它是一款并行与并发收集器,而且它能创建可预测的停顿时间模型。
对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的Eden Space和From Space,少数状况下会直接分配在老年代。若是新生代的Eden Space和From Space的空间不足,则会发起一次GC,若是进行了GC以后,Eden Space和From Space可以容纳该对象就放在Eden Space和From Space。在GC的过程当中,会将Eden Space和From Space中的存活对象移动到To Space,而后将Eden Space和From Space进行清理。若是在清理的过程当中,To Space没法足够来存储某个对象,就会将该对象移动到老年代中。在进行了GC以后,使用的即是Eden space和To Space了,下次GC时会将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认状况下,若是对象年龄达到15岁,就会移动到老年代中。
通常来讲,大对象会被直接分配到老年代,所谓的大对象是指须要大量连续存储空间的对象,最多见的一种大对象就是大数组,好比:
byte[] data = new byte[410241024]
这种通常会直接在老年代分配存储空间。
固然分配的规则并非百分之百固定的,这要取决于当前使用的是哪一种垃圾收集器组合和JVM的相关参数。
《深刻理解Java虚拟机》