提及垃圾收集(Garbage Collection,GC),大部分人都把这项技术当作Java语言的伴生产物。事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当List还在胚胎时期时,人们就在思考GC须要完成的3件事情:算法
1、哪些内存须要回收?安全
从JVM区域结构看,可将这些区域划分为“静态内存”和“动态内存”两类。程序计数器、虚拟机栈、本地方法3个区域是“静态”的,由于这几个区域的内存分配和回收都具有肯定性,都随着线程而生,随着线程而灭。但Java堆和方法区不同,内存分配都存在不肯定性,只有在程序处于运行期间才能知道会建立哪些对象,这部份内存和回收都是动态的,垃圾收集器所关注的是这部份内存。多线程
在堆里面存放着Java世界几乎全部的对象实例,垃圾回收器在对堆进行回收前,第一件事情就是就是要肯定这些对象哪些还"存活"着,哪些已经"死去"。那么又怎么肯定对象已经"死去"呢?并发
1.引用计数法:测试
分配对象时给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是没有再被使用了。客观地说,引用计数法(Reference Counting)的实现简单,判断效率也很高,可是在主流的Java虚拟机里面没有选用引用计数法来管理内存,其中最主要的缘由是它很难解决对象之间相互循环引用的问题。例如:线程
public class ReferenceCountingGC { public Object instance = null; private byte[] bigsize = new byte[2*1024*1024]; public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
当设置objA = null;objB = null后这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,可是它们由于互相引用着对方,致使它们的引用计数都不为0,因而引用计数算法没法通知GC收集器回收它们。若是这个对象特别大,则会形成严重的内存泄露。设计
2.可达性分析算法:调试
可达性分析(Reachability Analysis)的基本思想是经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(也就是GC Roots到这个对象不可达),则证实此对象是不可用的。以下图所示:code
对象Object五、Object六、Object7相互虽然有关联,可是它们到GC Roots是不可达的,因此它们将会被断定为是可回收的对象。在Java语言中,可做为GC Roots的对象包括下面几种:对象
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即通常说的Native方法)引用的对象。
2、何时回收?
虚拟机为了分析GC Roots这项工做必须在一个能确保一致性的快照中进行,这里的“一致性”的意思就是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上——这叫安全点。固然,程序执行时并不是在全部地方都能停顿下来开始GC,只有到达安全点时才能暂停。安全点选址也有规定的,选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的。这里的长时间执行的最明显特征是指令列复用,例如方法调用、循环跳转、异常跳转等。
虚拟机为了能让全部线程都“跑”到安全点上停顿下来,设计了两个方案:抢先式中断和主动式中断。其中抢先式中断是虚拟机发生GC时,首先把全部线程所有中断,若是发生有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。这种方式如今比较用了。而主动式中断是虚拟机须要GC时仅仅简单的设置一个标志,各个线程执行到安全点时主动去轮询这个标志,发现中断标志为真时就本身中断挂起。
3、如何回收?
3.1 垃圾收集算法:
(1)标记-清除(Mark-Sweep)算法
这是最基础的算法,就像它名字同样,算法分为“标记”和“清除”两个阶段:首先标记处全部须要回收的对象(如哪些内存须要回收所描述的对象),对标记完成后统一回收全部被标记的对象,以下图所示:
缺点:一个是效率问题,标记和清除两个过程的效率都不高;另外一个是空间问题,标记清除后悔产生大量的不连续的内存碎片,可能会致使后续没法分配大对象而致使再一次触发垃圾收集动做。
(2)复制算法
为了针对标记-清除算法的不足,复制算法将可用内存容量划分为大小相等的两块,每次只使用一块。当一块的内存用完了,就将还存活的对象复制到另外一块上面去。而后把已使用过的内存空间一次清理掉,以下图所示:
缺点:使用内存比原来缩小了一半。
如今的商业虚拟机都采用这种收集算法来回收新生代,有企业分析的得出其实并不需求将内存按1:1的比例划分,由于新生代中的对象大部分都是“朝生夕死”的。因此,HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1。一块Eden和两块Survivor,每次使用一块Eden和一块Survivor,也就是说只有10%是浪费的。若是另外一块Survivor都没法存放上次垃圾回收的对象时,那这些对象将经过“担保机制”进入老年代了。
(3)标记-整理(Mark-Compact)算法
复制算法通常是对对象存活率较低的一种回收操做,但对于对象存活率较高的内存区域(老年代)来讲,效果就不是那么理想了,标记-整理算法所以诞生了。标记-整理算法和标记-清除算法差很少,都是一开始对回收对象进行标记,但后续不是直接对对象清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存,以下图所示:
(4)分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不一样的区域。通常状况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集时只有少许对象须要被回收,而新生代的特色是每次垃圾回收时都有大量的对象须要被回收,那么就能够根据不一样代的特色采起最适合的收集算法。
3.2 垃圾收集器:
(1)七种垃圾收集器:
说明:
注意:并行与并发
(2)经常使用五种组合:
(2.1)Serial/Serial Old:
特色:
说明:
STW(stop the world):编译代码时为每个方法注入safepoint(方法中循环结束的点、方法执行结束的点),在暂停应用时,须要等待全部的用户线程进入safepoint,以后暂停全部线程,而后进行垃圾回收。
适用场合:
(2.2)ParNew/Serial Old:
说明:
(2.3)Parallel Scavenge/Parallel Old:
特色:
说明:
参数设置:
适用场合:
(2.4)ParNew/CMS:
说明:
CMS主要注重STW的缩短(该时间越短,用户体验越好,因此主要用于处理不少的交互任务的状况)
特色:
初始标记与从新标记都会暂停全部用户线程(即STW),可是时间较短;并发标记与并发清理时间较长,可是不须要STW
关于并发标记期间怎样记录发生变更的引用关系对象,在从新标记期间怎样扫描这些对象
缺点:
标记-清理算法:会产生内存碎片,因为是在老年代,可能会提早触发Full GC(这正是咱们要尽可能减小的)
参数设置:
-XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4
适用场合:
(2.5)G1
说明:
原理:
运做流程:
优势:
适用范围: