在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).
在JDK1.2之前,使用的是引用计数器算法。
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!
问题: 如果在A类中调用B类的方法,B类中调用A类的方法,这样当其他所有的引用都消失了之后,A和B还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。但是该算法并不会计算出该类型的垃圾。
在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图:虽然E和F相互关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。
注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).
垃圾收集策略有分代收集和分区收集。
标记-清除算法(老年代)
该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.
该算法会有两个问题:
所以它一般用于"垃圾不太多的区域,比如老年代"。
复制算法(新生代)
该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象(非垃圾)复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.
优点: 不用考虑碎片问题,方法简单高效。
缺点: 内存浪费严重。
现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代。
复制算法的空间分配担保:
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).
标记-整理算法(老年代)
标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.
方法区回收(永久代)
在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量 和 无用的类 .
回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:
分区收集
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间
在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿
Serial收集器是Hotspot运行在Client模式下的 默认新生代收集器 , 它在进行垃圾收集时,会暂停所有的工作进程,用一个线程去完成GC工作
特点:简单高效,适合jvm管理内存不大的情况(十兆到百兆)。
ParNew收集器其实是Serial的多线程版本,回收策略完全一样,但是他们又有着不同。
我们说了Parnew是多线程gc收集,所以它配合多核心的cpu效果更好,如果是一个cpu,他俩效果就差不多。(可用-XX:ParallelGCThreads参数控制GC线程数)
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款 真正意义上的并发收集器 , 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao),又称多并发低暂停的收集器。
由他的英文组成可以看出,它是基于标记-清除算法实现的。整个过程分4个步骤:
可以看到,初始标记、重新标记需要STW(stop the world 即:挂起用户线程)操作。因为最耗时的操作是并发标记和并发清除。所以总体上我们认为CMS的GC与用户线程是并发运行的。
优点: 并发收集、低停顿
缺点:
-XX:CMSInitiatingOccupancyFraction
来设置GC触发百分比(1.6后默认92%),当然我们还得设置启用该策略 -XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
参数,它会在GC执行完后接着进行碎片整理,但是又会有个问题,碎片整理不能并发,所以必须单线程去处理,所以如果每次GC完都整理用户线程stop的时间累积会很长,所以 XX:CMSFullGCsBeforeCompaction
参数设置隔几次GC进行一次碎片整理(默认为0)。 同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是 引入分区 的思路, 弱化分代 的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
因为每个区都有E、S、O代,所以在G1中,不需要对整个Eden等代进行回收,而是寻找可回收对象比较多的区,然后进行回收(虽然也需要STW操作,但是花费的时间是很少的),保证高效率。
G1的新生代收集跟ParNew类似,如果存活时间超过某个阈值,就会被转移到S/O区。
年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域的大小
分为以下几个阶段:
后续会持续更新干货技术分享,大家觉得不错的可以点个赞关注下,谢谢支持!