一、为什么要学习GC技术
垃圾收集也称GC,是Java内存动态分配和回收背后的关键技术,有了垃圾收集器,Java程序员就与C++程序员有了巨大的区别,C++程序员需要谨慎的申请和释放内存,而Java程序员就不需要那么辛苦了,但是我们还是需要了解GC技术,当我们需要排除各种内存溢出,内存泄漏问题时,我们就需要对这些自动化技术实施必要的监控和调节。
二、判断对象存活状态算法
1、引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一,任何时刻计数器为0的对象就是不可能再被使用的。
缺点:无法解决相互循环引用的问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;
a = null;
b = null;
//此时若发生GC,若虚拟机使用的是引用计数法,则无法将a与b标记为死亡对象
System.gc();
}
}
注意:在调用System.gc()时,只是通知系统可以进行垃圾收集,系统并不会立即执行GC,而是会等待一些条件发生才会执行,一般编程中不需要调用这个方法,因为GC是一件虚拟机自动进行的事情。
2、可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
(图片来自网络)
三、引用的类型
1、强引用
是指在程序代码中pub存在的,类似“Object obj = new Object()”这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
2、软引用
描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference。
3、弱引用
也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference。
4、虚引用
虚引用也称为幽灵引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例。
四、对象的自我拯救
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记过程:
1、若一个对象没有与GCRoots相连接的引用链,将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,这两种方法视为没必要执行。若没必要执行,则直接GC。
2、如果这个对象被判定为有必要执行finalize(),那么该对象将会被防止在一个叫F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。在执行finalize()时,该对象可以自救。(即赋值给某个引用)
五、回收方法区
方法区也称为“永久代”。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
六、垃圾收集算法
1、标记—清除算法(Mark-Sweep)
分为两个阶段:标记和清除。
首先标记处所有需要回收的对象,在标记完成后统一回收的对象,在标记完成后统一回收所有标记的对象。
不足之处:
效率问题,标记和清除两个过程的效率都不高。
空间问题,标记清除之后会产生大量不连续的问题。
(图片来自网络)
2、复制算法
为了解决效率问题,复制算法出现了。将内存分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:这样使得每次都是对整个半区进行内存回收,内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:内存缩小为原来的一半,空间消耗。
(图片来自网络)
但是一般来说,不会分配一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每此使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地赋值到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行担保分配,这些对象将直接通过分配担保机制进入老年代。
3、标记—整理算法(Mark-Compact)
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会降低。
在标记后,让所有存活对象都移动到一端,然后直接清理掉端边界以外的内存。
4、分代收集算法
一般吧Java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批量对象死去,只有少量存活,就选用复制算法。
在老年代中,因为对象存活率高,没有额外空间对他进行分配担保,就必须使用“标记-清理”或者“标记-整理算法”进行回收。
七、垃圾收集器
1、Serial收集器(新生代)
是一个单线程收集器,“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。可以与老年代的CMS收集器合作。
优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
缺点:有停顿时间,不能与用户线程并行工作。
2、ParNew收集器(新生代)
是Serial收集器的多线程版本,可与CMS收集器合作。
在单CPU环境下,性能不如Serial。
在CPU越来越多的情况下,性能越来越强。
3、Parallel Scavenge(新生代)
使用复制算法,关注点与其他收集器不同,关注吞吐量,吞吐量优先。
吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
两个重要参数:
-XX:MaxGCPauseMillis (大于0的整数) 收集器停顿时间,收集器尽可能地保证内存回收话费的时间不超过设定值。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取得。
-XX:GCTimeRatio (0到100的整数)即吞吐量的倒数。
4、Serial Old(老年代)
使用“标记-整理”算法。
两种用途:
与Parallel Scavenge 收集器搭配使用。
作为CMS收集器的后备预案。
5、Parallel Old(老年代)
是Parallel Scavenge的老年代版本。
与Parallel Scavenge这个吞吐量优先收集器合作使用。
6、CMS收集器(老年代)Concurrent Mark Sweep
Mark-Sweep,标记—清除算法
以获取最短回收停顿时间为目标的收集器。重视响应速度,常用于B/S系统的服务端。
4个步骤:
初始标记:需要Stop The World。仅仅标记一下GC Roots 能直接关联到的对象。速度很快。
并发标记:GC Roots Tracing的过程。
重新标记:需要Stop The World。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
并发清除:与用户线程并发进行清除。
最耗时的标记和清除都是可以与用户线程并发的,因此总体来说CMS收集器的内存回收过程是与用户线程一起并发执行的。
也称之为并发低停顿收集器。
三大缺点:
(1)CMS收集器对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是因为占用了一部分线程,导致应用程序变慢,总吞吐量降低。CMS默认启动回收线程数是 (CPU数量+3)/4。
(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。由于垃圾收集阶段用户线程还需要运行,那也就还需要预留足够的内存空间使用,因此CMS收集器不能像其他收集器那也等到老年代几乎完全被填满了在进行收集。要是CMS运行期间预留的内存无法满足程序需要,就会出现“Concurrent Mode Failure”失败。
(3)基于“标记-清除”算法实现的收集器,会产生大量空间碎片。
7、G1收集器(整个堆内存)
主要特点:
(1)并发与并行
G1能重复利用多CPU环境下的硬件优势,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
(2)空间整合
从整体上来看是基于“标记-整理”,从局部上来看(两个Region)来看,是基于“复制”,总而言之不会产生空间碎片。
(3)可预测的停顿
G1除了降低停顿外,还能建立可预测的停顿时间模型,能明确在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
实现方式:
G1收集器收集的是整个堆内存,将堆划分成多个大小相等的独立区域(Region),可以有计划地避免在整个堆中进行全区域垃圾回收,G1跟踪谷歌Region里面的垃圾堆积吃的价值大小,在后台维护一个优先列表,每次根据收集时间,优先回收价值最大的Region。
化整为零的思路。
虚拟机用RemeberedSet 来避免Region之间的对象引用。
四个步骤:
1、初始标记
2、并发标记
3、最终标记
4、筛选回收