垃圾收集算法

垃圾收集算法

垃圾收集(Garbage Collection,GC)需要思考的3个问题:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

了解GC和内存分配的目的:当需要抛出各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,需要自动的垃圾回收和内存分配进行监控和调节。

​ 程序计数器、虚拟机栈和本地方法栈三个区域随着线程而生,随线程而灭;栈中栈帧随着方法的进入和退出进行着出栈和入栈操作。每一个栈帧在类结构确定下来时就确定需要多大的内存空间了。因此这三个区域的内存分配和回收都具备确定性。

​ Java堆和方法区则不一样,一个接口的各个实现类需要的内存大小会不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间才能知道会创建那些对象。这部分内存的分配是动态的,垃圾回收也指的是回收这部分内存。

判断对象是否存活

垃圾收集器在对堆进行回收之前,需要判断哪些对象不可能再被通过任何途径而使用(对象死去),哪些对象还会被使用到(对象存活)。

引用计数算法

基本思想给对象中添加一个引用计数器,每当有一个地方引用,计数器就加一,当有引用失效,计数器就减一;当计数器为0的时候就说明此对象不可能再被使用。

  • 有点:<1>实现简单,垃圾对象便于辨识;<2>判断效率高,回收没有延迟性。
  • 缺点:<1>需要单独的字段存储器,增加存储空间的开销;<2>每次赋值都需要更新计数器,增加时间开销;<3>计数器无法处理循环引用的情况,这条导致Java的垃圾回收器没有使用此算法。

在这里插入图片描述

将P的指向NULL之后,红色圈内的三个对象应该均可被回收,但是由于引用计数法的原因。导致了内存泄漏。

在这里插入图片描述

上述程序发生了GC,说明Java虚拟机没有使用引用计数法来标记对象是否存活。

GC开头的说明此次垃圾回收为Minor GC,而Full GC开头的说明此次垃圾回收为stop-the-world的类型。

PSYoungGen表示新生代且收集器是Parallel Scavenge 收集器,ParOldGen表示老年代且收集器为Parallel Old。

eden、from、to表示新生代代中各区域的划分。

Metaspace表示元空间的大小,JDK>=1.8。

方括号内的"6717K->496K(76288K)“表示"GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”

方括号外的"6717K->504K(251392K)“表示"GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。

"0.0007292 secs"表示该内存区域本次GC所占用的时间。

System.gc():默认情况下,调用System.gc()会显示的触发Full GC。调用System.gc()无法保证堆垃圾回收器的调用,是告诉JVM希望此刻进行一次GC,会不会立马执行,由JVM决定。

在这里插入图片描述

可达性分析算法

基本思想通过一系列的成为"GC Roots"的对象作为起点,从这些起点向下搜索,所搜走过的路径称为引用链,当一个对象到GC Roots没有引用连(就是从GC Roots到该对象没有可达路径),则该对象是不可用的。

图2

上图中,对象Object5、Object6虽然互有关联,但是他们到GC Roots是不可达到,所以会被判定为可回收对象。

可作为GCRoots的对象:

  • 虚拟机栈
  • 方法区的类属性所引用的对象
  • 方法区中常量所引用的对象
  • 本地方法栈中引用的对象

GC Roots包含的元素

  • 虚拟机栈中引用的对象
  • 本地方法栈内引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 虚拟机内部使用的对象(基本数据类型对应的Class对象,系统类加载器,常驻的异常对象(NullPointException等))。

由于Root采用栈的方式存储变量和指针,所以一个指针保存了堆内存里面的对象,但其本身不在堆内存中,那它就是一个Root。

Java中的引用

JDK1.2之后,Java的引用分为:

  • 强引用(Strong Reference),程序中普遍存在的,类似"Object object = new Object()"这类的。只要强引用还存在,垃圾收集器就不会回收掉被引用的对象。
  • 软引用(Soft Reference),用来描述一些有用但非必须的对象。对于这类对象,系统在将要发生内存溢出异常之前,会把这些对象列入回收范围内进行二次回收。如果这次回收还没有足够的空间,就抛出内存溢出异常。
  • 弱引用(Weak Reference),用来描述非必须的对象。被弱引用关联的对象只能存活到下一次垃圾收集发生之前,当垃圾收集器工作时,不论当前内存是否够用,都会回收掉只被弱引用关联的对象。
  • 虚引用(Phantom Reference),强度最弱的一种引用。对象是否有虚引用的存在完全不影响其生存时间,也无法通过虚引用来取得一个对象实例。为对象设置虚引用关联的唯一目的就是在这个对象被收集器回收的时候收到一个系统通知。

对象的finalization机制

​ finalization机制允许开发者在对象销毁之前提供自定义处理。通过重写finalize()方法进行一些资源释放和清理工作。

​ finalize()方法可能会使对象复活;并且finalize()方法的执行是没有保障的,完全由GC决定。所以不要主动的调用finalize()方法,应该由垃圾回收机制来决定调用机制。

由于finalize()方法的存在,虚拟机中的对象由下面三种状态:

  • 可触及的:从根节点开始,可以到达该对象
  • 可复活的:对象的引用都被释放,但是对象有可能在finalize()方法中复活
  • 不可触及的:对象的finalize()方法被调用,并且没有复活。

finalize()方法只会被调用一次。

public class ObjectRelive {
  public static ObjectRelive obj;// 类变量,属于GC Roots

  @Override
  protected void finalize() throws Throwable {
    // TODO Auto-generated method stub
    super.finalize();
    System.out.println("用户重写的finalize()方法");
    obj = this;
  }

  public static void main(String[] args) {
    try {

      obj = new ObjectRelive();
      obj = null;
      //赋值为null后的第一次拯救
      System.gc();// 手动调用垃圾回收器
      System.out.println("第一次GC");
      Thread.sleep(3000);

      if (obj == null) {
        System.out.println("obj 已死。");
      } else {
        System.out.println("obj 依旧存活。");
      }
      obj = null;
      //赋值为null后的第二次拯救
      System.gc();// 手动调用垃圾回收器
      System.out.println("第二次GC");
      Thread.sleep(3000);

      if (obj == null) {
        System.out.println("obj 已死。");
      } else {
        System.out.println("obj 依旧存活。");
      }

    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

图2.1

垃圾收集算法

标记清除算法

​ 最基础的收集算法,首先标记所有被引用的对象,在标记完成后对堆内存从头到尾进行遍历,如果发现某个对象没有被标记,则将其回收。

​ 在对象被确定收集之前,至少需要两次标记。

​ 如果对象在可达性分析之后没有与GC Roots对象连接的引用链,会被第一次标记并进行一次筛选。筛选的条件是,对象是否有必要执行finalize()方法(当对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,虚拟机就认为没有必要执行次方法)。有必要执行的finalize()方法的对象将会被放置在名为F-Queue的队列中。

​ 对象可以在finalize()方法中"拯救自己"(只要重新与引用链上的任何一个对象建立连接)。虚拟机会自动建立低优先级Finalizer线程区执行finalize()方法。如果对象没有重新与GCRoots引用链上的对象建立连接,那么虚拟机将对对象进行第二次标记。在第二次标记的时候还没有被移除"即将回收"的集合,这个对象就基本确认回收了。

​ 存在的不足:

  • 标记和清除两个过程的效率都不高
  • 标记清除之后会产生大量不连续的内存碎片,这种碎片太多会导致程序在后需的运行中无法找到足够大的连续空间而必须提前触发另一次垃圾回收机制
复制算法

​ 复制算法将内存分为大小相同的两块,每次只使用其中的一块,另一块为保留区域。当这一块用完了,就进行垃圾收集,将存活的对象复制到另一块内存上面(即保留区域),再把已经使用的过的那一块内存空间一次清理掉(清理掉之后的空间就变为了保留区域)。

  • 优点:
    • 没有标记和清除掉过程,运行高效
    • 复制过去以后保证了空间的连续性,不会出现"碎片"问题
  • 缺点:
    • 内存空间只能使用一般,造成空间浪费
    • 对于G1这种拆分成很多Region的GC,GC也需要维护Region之间的引用关系。

图3

​ IBM研究发现,很多对象都是“朝生夕死”的,所以内存的划分不需要按照1:1来划分。将对内存划分为较大的Eden空间和两块较小的Survivor区域,每次使用Eden和其中一个Survivor空间。当回收的时候,将Eden和Survivor中还存活的对象一次性分复制到另一个Survivor空间中,然后清理掉Eden还刚用过的Survivor空间。HotSpt虚拟机默认Eden和Survivor的比例是8:1,所以每次新生代中可用内存空间为90%,只有10%的内存用作保留区域。

​ 但是如果存活的对象所占空间超过了10%,也就是另一块Survivor空间不足以存放所有的粗活对象,那么久通过分配担保机制将使对象进入老年代中。

标记整理算法

​ 标记整理算法的过程与标记清除算法类似,其中标记的过程是一样的。在标记完之后不直接对对象进行清理,而是让所有存活的对象都想内存的一端移动,然后直接清理掉端边界以外的所有内存空间。

​ 标记整理算法的效果等同于标记清除算法执行完成后,在进行一次碎片整理。

​ 二者的本质差异在于:

  • 标记清除算法是非移动式的回收算法

  • 标记整理算法是移动式的回收算法

  • 优点

    • 消除标记清除算法中内存区域分散的缺点。
    • 消除复制算法中内存减半的代价
  • 缺点

    • 低于复制算法的效率
    • 移动对象的同时,如果对象被其他对象引用,还需要调整引用地址

对比

标记清除标记整理复制
速度最慢最快
空间开销通常需要存活对象的2倍大小
移动对象
分代收集算法

​ 分代收集是指把Java堆分为新生代和老年代。基于分代的概念,依据新生代和老年代的特点选择合适的回收算法。

  • 新生代:区域相对老年代较小,对象的生命周期短、存活率低、回收频繁

    这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活的对象大小有关,所以适用于新生代。

  • 老年代:区域较大,对象的生命周期长,存活率高,回收没有年轻代频繁

    这种存活率高的对象,一般由标记清除或标记整理和标记清除混合实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值