一、垃圾的检索标记
1.标记算法
被其他对象引用的被标记为存活,没有被其他对象引用的被认为是垃圾,垃圾回收会回收那些标记为垃圾的内存空间。
2.方法一:引用计数算法
(1)即通过判断对象的引用数量来决定对象是否可以被回收。针对每个对象实例,都有一个引用计数器,被引用则+1,完成引用则-1。而对于任何引用计数为0的对象实例,都可以被当作垃圾收集。
(2)优点:执行效率高,程序执行受影响较小
(3)确定:无法检测出循环引用的情况,导致内存泄露。例如:即父对象引用子对象,同时子对象引用父对象,则不可能回收。代码实例:两种对象之间相互交互,其中对象
public class GCObject {
public GCObject childObject;
}
程序中应用相互引用,则会导致这个对象从来不会被回收
public class test2 {
public static void main(String[] args) {
GCObject object1=new GCObject();
GCObject object2=new GCObject();
object1.childObject=object2;
object2.childObject=object1;
}
}
3.方法二:可达性算法
(1)定义
通过判断对象的引用链是否可达来决定对象是否可以被回收
(2)作为GC Root对象范围
- 虚拟机栈(Java方法栈,即Java方法中)中引用的对象(栈帧中的本地变量表)
- 方法区中的常量引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(Native方法)的引用对象
- 活跃线程的引用对象
二、垃圾回收算法
1.标记-清除算法(Mark and Sweep)
(1)实现
标记:基于可达性算法,从根集合进行扫描,对存活的对象进行标记
清除:对堆内存从头到尾进行线性遍历,回收不可达对象内存。最后,将标记的对象进行清楚,从而方便下一次垃圾回收。
(2)过程
(3)问题
碎片化:内存直接回收,导致内存分散,没有一个完整的内存。下一次如果申请较大的内存,会提前触发下一次垃圾回收
2.复制算法(Copying)
(1)原理
分为对象面和空闲面,一般是按照1:1分配。新对象创建在对象面上,当对象面中块内存使用完成,则将对象面中的对象全部复制到空闲面,重新排列,同时将对象面所有对象内存清除。
(2)过程
(3)优势
- 解决碎片化问题,因为复制区按照顺序重新分配内存
- 顺序分配内存,简单高效
- 适用于对象存活率低的场景
3.标记整理算法(Compacting)
(1)定义
标记:依据可达性算法,从根集合进行扫描,对存活的对象进行标记
清除:移动所有存活的对象,且按照内存地址次序依次排列,然将末端内存地址以后的内存全部回收。
(2)回收过程
(3)优势和适用场景
- 避免内存的不连续
- 不用设置两块内存互换,增加内存空间利用率
- 适用于存活率高的场景,比如老年代
4.分代回收算法(Generational Collectior)
(1)定义
针对堆中不同区域,采用不同垃圾回收算法的组合。按照对象生命周期的不同,将对象放入堆中不同的区域;对于不同的区域,采用不同的垃圾回收算法。从而,可以提高JVM的垃圾回收效率
(2)堆内存分配
JDK1.8之前,以及之后,如下图
参考:https://www.cnblogs.com/secbro/p/11718987.html
(3)年轻代
作用:尽可能快速地收集掉那些生命周期短的对象。年轻代分为:Eden区和两个Survivor区,由于新生代中存活对象较少,所以采用复制算法,简单高效。具体结构如下图
具体回收过程:
新建对象,先进入Eden区。当Eden区被对象填满时,就会执行Minor GC。第一次执行,把所有存活下来的对象转移到其中一个survivor from区。第二次及以上执行,Minor GC同样会检查survivor from区存活下来的对象,并把survivor from+Eden中存活对象转移到另一个survivor to区,同时清空survivor from+Eden区。然后from和to区域互换。这样在一段时间内,总会有一个空的survivor区。
与此同时,每存活一次,对象的记录就会加1,当大于-XX:MaxTenuring Threshold(默认15),则会进入老年代。
(4)年轻代进入老年代
- 经历一定Minor次数依然存活的对象
- Minor gc后,Survivor区中存放不下的对象
- 新生成的大对象(-XXx:+PretenuerSizeThreshold),判断如果超过这个size,则直接进入老年代
- 动态年龄判断,大于等于某个年龄的对象超过了survivor空间一半,大于等于某个年龄的对象直接进入老年代
年轻代进入老年代调优参数
- -XX:SurvivorRatio:Eden和其中一个Survivor(from或者to)的比值,默认8:1
- -XX:NewRatio:老年代和年轻代内存大小的比例
- -XXxMaxTenuring Threshold:对象从年轻代晋升到老生代经过GC次数的最大阈值
(5)老年代:存放生命周期较长的对象
采用算法原理:年代中对象较多,并且没有可以担保的内存区域,所以采用标记清理算法或者标记整理算法
具体回收算法:当老年代回收,会使用Full GC(minor GC针对新生代和Major GC针对老年代),Full GC比Minor GC慢,但执行频率低
触发full gc条件:
(a)老年代空间不足
(b)永久代空间不足(1.8之前)
(c)CMS GC时出现promotion failed,concurrent mode failure
(d)Minor GC晋升到老年代的平均大小大于老年代的剩余空间
(e)调用System.qc(),只是提醒虚拟机需要回收,具体回收时机,需要JVM决定
(f)使用RMI进行RPC或者管理JDK应用,每小时执行1次Full GC
(6)年轻代和老年代回收实例
-》1.7之前
-》1.8之后将最初的永久代取消,替换为元空间
5.常见垃圾回收器
5.1 CMS垃圾收集器
(1)使用:-XX:+UseConcMarkSweepGC,使用标记-清除算法
(2)步骤
- 初始标记:stop-the-world
- 并发标记:并发追溯标记,程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
如下图
(3)CMS存在问题
-》CMS采用的基础算法是标记-清除。所以CMS不会整理、压缩堆空间。经过CMS收集的堆会产生空间碎片。虽然节约了垃圾回收的停顿时间,但也带来堆空间的浪费。
-》需要更多的CPU资源,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切换是不靠谱的。
5.2 G1(Garbage First)垃圾收集器
(1)使用G1收集器:-XX:+UseG1GC,复制+标记-整理算法
(2)G1相较于CMS的改进点
不同于其他的分代回收算法,G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。每块区域既有可能属于O区(老年代)、也有可能是Y区(新生代),且每类区域空间可以是不连续的(对比CMS的O区和Y区都必须是连续的)。
(3)G1特点
- 并行和并发
- 分代收集
- 空间整合
- 可预测的停顿
(4)适合G1的场景
目前CMS还是默认首选的GC策略,在以下场景使用G1更适合:
- 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
- 应用在运行过程中产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期:防止高并发应用雪崩现象
(5)G1对比CMS的区别
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题。而Eden/Survivor/Old分别是多个region的逻辑集合,物理上内存地址并不连续,所以在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象,可驾驭度。G1是可以设定GC暂停的target 时间的,根据预测模型选取性价比收益更高,且一定数目的Region作为CSet,能回收多少便是多少。
- G1在回收内存后会马上同时做,合并空闲内存的工作;而CMS默认是在STW(stop the world)的时候做
- G1同时回收老年代和年轻代,而CMS只能回收老年代,需要配合一个年轻代收集器。
CMS在old gc的时候会回收整个Old区,而对于G1来说没有old gc的概念,而是区分Fully young gc和Mixed gc,其中,Fully young gc对应年轻代的垃圾回收,Mixed gc混合了年轻代和部分老年代的收集。因此每次收集肯定会回收年轻代,老年代根据内存情况可以不回收或者回收部分或者全部(这种情况应该是可能出现)。
- SATB算法在remark阶段延迟极低以及借助RememberedSet的实现可以不做全堆扫描(G1对大堆更友好)以外,最重要的是可驾驭度
参考:https://blog.csdn.net/hellozhxy/article/details/80144419