【JVM】(四) :垃圾回收机制(GC)

垃圾的标准

对象被判定为垃圾的标准:

  • 没有被其他对象引用

判断对象是否为垃圾的算法:

  • 引用计数算法
  • 可达性分析算法

引用计数算法

判断对象的引用数量:

  • 通过判断对象的引用数量来决定对象是否可以被回收
  • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1
  • 任何引用计数 为0的对象实例可以被当作垃圾收集

代码示例

    public void  ReferenceQuoteCounterProblem(){
        MyObject object1 = new MyObject();  //(1) count=1  创建对象

        MyObject object2=object1;  //(2) count=2

        object1= null;   //(3)count=1

        object2=null;   //(4)  count=0  该对象实例可以被当作垃圾收集

    }

如下图所示,每一根指向或剪断堆中的线代表引用计数器+1或-1

优点:执行效率高,程序执行受影响较小

缺点:无法检测出循环引用的情况,导致内存泄漏

代码示例

    public void  ReferenceQuoteCounterProblem2(){
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        object1.childNode = object2;
        object2.childNode = object1;

    }

注:该算法机制在jvm不常用

可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收

 

可以作为GC Root的对象

  • 虚拟机栈中引用的对象(栈帧中 的本地变量表)
  • 方法区中的常量引用的对象
  • 方法区中的类静态属性引用的对象
  • 本地方法栈中JNI(Native方法)的引用对象
  • 活跃线程的引用的对象

回收算法

垃圾回收算法

  • 标记-清除算法(Mark-Sweep)
  • 复制算法(Copying)
  • 标记-整理算法(Compacting)
  • 分代收集算法(Generational Collector)

 注:这里只讲最常用的四种回收算法

标记-清除算法(Mark-Sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

  • 标记:从根集合进行扫描,对存活的对象进行标记
  • 清除:对堆内存从头到尾镜像线性遍历,回收不可达对象内存

如下图所示

 

 注:该算法缺点明显,由于标记清除不需要对象的移动,因此会造成多个不连续的碎片。空间碎片太多,当存在分配较大的对象内存(占四个格子)时,没有足够的连续内存,而不得不提前触发另一次GC工作(一直保持clean状态), 导致OOM

复制算法(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。(推倒重建,只需要移动堆顶指针,按顺序分配内容,高效简单)

  1. 分为对象面和空闲面
  2. 对象在对象面上创建
  3. 存活的对象被从对象面复制到空闲面
  4. 将对象面所有对象内存清除

如下图所示

优点:

  • 解决碎片化问题
  • 顺序分配内存,简单高效
  • 适用于对象存活低的场景(分代收集-年轻代)

标记-整理算法(Compacting)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。(标记加移动地址造成成本更高)

  • 标记:从根集合进行扫描,对存活对象进行标记
  • 整理-清除:移动所有存活的对象,且按照内存地址依次序依次排列,然后将末端内存地址以后的内存全部回收

如下图所示

优点:

  • 避免内存的不连续性
  • 不用设置两块内存互换
  • 适用于存活率高的场景(分代收集-老年代)

分代收集算法(Generational Collector)

这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。虚拟机生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。这样可以减少复制对象的时间。(这里只演示JDK8中堆)

  1. 垃圾回收算法的组合拳
  2. 按照对象生命周期的不同划分区域,每个区域采用不同的垃圾回收算法
  3. 目的:提高JVM的回收效率

在Java8及以上版本的虚拟机分代垃圾回收机制中,应用程序可用的堆空间可以分为年轻代与老年代,然后年轻代有被分为Eden区,From区与To区,如下图所示:

GC分类

  • Minor GC:发生在年轻代中垃圾收集动作,采用的复制算法。
  • Full Gc:发生在老年代中。

年轻代

年轻代是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命,具有有朝生夕死的性质。(尽可能快速地收集掉那些生命周期短的对象)

  • 一个Eden区和两个Survivor区组成
  • Eden与两个Survivor占比为8:1:1
  • 年轻代占整个堆的1/3大小

采用算法

  • 复制算法

年轻代三次GC回收流程图如下:

  1. 当系统创建一个对象时,这个对象的年龄也被确定了(0岁),总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区,这时From区的对象增加1(1岁)。
  2. 这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次YoungGC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区(年龄继续加1)。
  3. 再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。
  4. 经过若干次YoungGC后,有些对象在From与To之间来回游荡,这时候From区与To区亮出了底线(默认阈值15),这些家伙要是到现在还没挂掉,对不起,一起滚到(复制)老年代吧。

对象如何晋升到老年代

  • 经历一定Minnor次数依然存活的对象
  • Survivor区中存放放不下的对象
  • 新生成的大对象(-XX:+PretenuerSizeThreshold)

常用的调优参数

  • -XX:SurvivorRatio :Eden与两个Survivor的比值,默认 8:1:1
  • -XX:NewRatio:老年代和年轻代内存大小比例,默认 2:1
  • -XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值

老年代

存放生命周期较长的对象

采用算法

  • 标记-清理算法
  • 标记-整理算法

触发Full GC的条件

  • 老年代空间不足
  • 永久代空间不足(JDK8之前存在)
  • CMS GC时出现 promotion failed ,concurrent mode failure(可能触发)
  • Minor GC 晋升到老年代的平均大小大于老年大剩余空间
  • 调用System.gc()(可能触发)
  • 使用RMI来进行RPC或者管理的JDK应用,每个小时执行一次Full GC

猜你喜欢

转载自www.cnblogs.com/kongliuyi/p/11299320.html