想要理解java垃圾回收机制,首先要了解什么是垃圾回收,顾名思义,垃圾回收就是回收内存中不再使用的对象。
主要分为两个阶段:
1、查找内存中不再使用的对象
2、将这些对象占用的内存释放
首先来看第一个阶段【查找内存中不再使用的对象】
判断方法有两种:
引用计数法和根搜索算法
引用计数法,当一个对象没有被任何引用指向,则视之为可回收的垃圾。
根搜索算法,是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为Reference Chain(引用链),当一个对象到GC Roots没有任何引用链相连时,则说明该对象可被回收。
再来看第二个阶段【释放对象的内存占用】
总体来说,分了三种回收算法:
1、标记-复制
它将可用内存容量分割为大小相等的两块,每次只使用其中一块,当这一块用完之后,便将其中还存活的对象复制到另一块中,然后对这一块内存空间进行一次清理。它的缺点是将内存压缩为原来的一半,代价太高。当然优点也很显然,实现简单,效率高,并且不会产生内存碎片。
2、标记-清除
该方法首先标记出需要回收的对象,标记完成后统一进行清理。优点是效率高,缺点是易产生内存碎片。
3、标记-整理
标记方法同上。整理阶段不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,并更新存活对象中所有指向被移动对象的指针。因为存在移动对象的动作,所以它的效率要比“标记-清理”低,但不会产生内存碎片。
基于分代的假设
由于对象的存活时间有长也有短,因此对于存活时间长的对象,减少被gc的次数可以避免不必要的开销。这样就把内存分成新生代和老年代,新生代存放刚创建的和存活时间较短的对象,老年代存放存活时间较长的对象。这样每次只清理年轻代,老年代仅在必要时做清理,这样可以极大提高GC效率。
当前的商业虚拟机垃圾收集都采用“分代收集”算法,在新生代中,每次垃圾收集都会发现有大批对象死去,只有少量存活,那就用“标记-复制”算法,只需要付出少量存活对象的复制成本就可以完成收集;而老年代中因为对象存活率高、没有额外空间对它进行分配,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
Java垃圾回收器历史
第一阶段:Serial(串行)收集器
Serial收集器是一个单线程的收集器,只会使用一个CPU或一个线程去完成垃圾回收工作,并在这期间,须暂停其他所有的工作线程(Stop the World),直到收集结束。由于Serail收集器没有线程交互的开销,专心做垃圾收集可以获得最高的单线程效率。
-XX:+UseSerailGC
第二阶段:Parallel(并行)收集器(目前已处于维护模式……)
Parallel收集器也称为吞吐量收集器,它的优势在于使用多线程去进行垃圾收集,这利用了多核特性,大幅提高了gc效率。
-XX:+UseParallelGC -XX:+UseParallelOldGC
PS:并发和并行的区别在于,能否同时处理多个任务。
比如,你吃饭到一半,来了个电话,你边打电话边吃,这叫并行;你停下吃饭接了电话,打完再接着吃饭,这是并发。
第三阶段:CMS(并发)收集器
Concurrent Mark Sweep收集器是一种获得最短回收停顿时间为目标的收集器,它是基于前文介绍的“标记-清除”算法实现的。
CMS收集器的堆空间被分割为两块空间,年老代和年轻代,其中年轻代被分割成一个Eden区和两个Survivor区。如图1:
图1 CMS堆结构
年轻代:一个对象初始化后,首先会进入Eden区和一个Survivor A区中,当Eden区满之后会触发一次Minor GC,Minor GC会检查Eden区,将存活对象拷贝到另一个Survivor B区(存放年轻代幸免的还没有晋升到年老代对象),并将这些存活对象的age+1,而死亡对象则作为垃圾回收掉。此时Eden区空闲,可放置新的对象,填满之后再触发Minor GC,循环往复。
年老代:位于Survivor B区的对象在年龄达到一定阈值时会晋升到年老代,这个区域存放对象时间较长,但随着时间流逝,也会有被填满的一天,此时就会触发CMS GC(old gc),简要步骤如下:
a.初始标记(CMS initial mark):需要“Stop The World”,标记GC Roots能直接关联到的对象,速度快。
b.并发标记
c.新标记:需要“Stop The World”,修正并发标记期内,因用户程序继续允许而导致标记产生变动的那一部分对象的标记记录
d.并发清除
不管是年轻代的Minor GC还是年老代的CMS GC,都会“Stop The World”,即停止用户的一切线程。其中前者耗时在复制阶段,而后者耗时在镖师垃圾阶段。
CMS收集器的优点在于并发收集、低停顿。目前很大一部分Java应用都集中在互联网站或B/S系统的服务器上,这类应用对响应速度要求高,以提供给用户较好的体验,CMS收集器就适用于该场景。缺点在于:
1、对CPU非常敏感。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是说在并发回收时,垃圾收集线程将占用不少于25%的CPU资源,如果CPU负载本来就很大,将会对应用程序造成影响。
2、CMS无法处理浮动垃圾。在清理阶段,由于用户线程还要继续运行,因此需要预留一部分空间给用户线程使用,这时会有新的垃圾不断产生,这一部分垃圾,CMS无法清理。
3、会产生大量的内存碎片
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
第四阶段:G1(并发)
相较于CMS,G1的改进在于:
1、基于“标记-整理”算法
2、能精准地控制停顿
G1能够极力避免全内存的垃圾收集,并不会等待内存耗尽(串行、并行)或快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在年老代找出具有高收收益的分区进行收集。再一个,之前的收集器进行收集的范围都是对新生代或老年代,而G1将整个Java堆(包括新生代和老年代)划分为大小固定的独立区域,并且跟踪这些区域里的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First由来)。区域划分、优先级的区域回收,避免了全内存扫描,只需要按照区域来进行扫描即可,可以在最短的时间内获得最高的收集效率。
一般的标准配置如下:
-server
-XX:+UseG1GC #开启G1GC
-XX:MaxGCPauseMillis=100 #期待的最大暂停时间,默认200ms
-XX:+ParallelRefProcEnabled #激活多线程引用处理
-XX:-ResizePLAB #当应用开启的线程较多时,关闭PLAB()的大小调整,以避免大量的线程通信所导致的性能下降。
-XX:ParallelGCThreads=28 # 8+(logical_processor -8)*(5/8)
-XX:+UnlockExperimentalVMOptions #解锁任何额外的隐藏参数
-XX:G1NewSizePercent=2 #新生代最小值,默认值5%
-XX:+PrintGCTimeStamps #打印CG发生的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps #输出GC的时间戳(以日期的形式)
-XX:+PrintGCDetails #打印GC详细信息
-Xloggc:/mnt/disk10/hbase/gc.log #指定GC log的位置
GC计算公式:
32GB heap, -XX:G1NewSizePercent=3;
64GB heap, -XX:G1NewSizePercent=2;
100GB and above heap, -XX:G1NewSizePercent=1; G1NewSizePercent的值要在1G以上:heap值*0.01
So our final command line options for theHRegionserver are:
-XX:+UseG1GC
-Xms100g -Xmx100g (Heap size used in ourtests)
-XX:MaxGCPauseMillis=100 (Desired GC pausetime in tests)
-XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:ParallelGCThreads=8+(40-8)(5/8)=28 40指的是cpu核数
-XX:G1NewSizePercent=1
Print GC log:
-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/var/log/solr/solr_gc.log