GC 及引用类型

gc范围

在JVM五种内存模型中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放,所以只有方法区(java8改为metaSpace)和堆需要进行GC。

前提条件

找出不可达的对象,将其释放,这里面讲的不可达主要是指应用程序已经没有内存块的引用了, 在Java中,某个对象对应用程序是可到达的是指:这个对象被根(根主要是指类的静态变量,或者活跃在所有线程栈的对象的引用)引用或者对象被另一个可到达的对象引用。

堆内存分区

  • 新生代:Eden,from,to 8:1:1,存放新创建的对象
  • 老年代: 存放大对象和超过一定年龄的新生代对象

GC分类

minorGC

  • 时机:如果Eden上的空间不足时,新建的对象会往from区放,去过仍然放不下会触发minorGC
  • 算法: 复制清理
  • 具体实现:将eden区和from区有引用的对象复制到to区,没有引用的对象清除,并对年龄+1
  • 优缺点
    • 优点:速度快,没有内存碎片
    • 缺点:需要一个额外的空间

majorGC

  • 时机:如果马上要进入老年区的对象所需空间大于剩余空间,触发majorGC,如果GC两次都无法满足需求,就会报OOM.
  • 算法:
    • 标记清除: 1. 从引用根节点开始标记对象;2. 遍历整个堆空间,清除没有引用的对象
      • 优缺点
        • 优点:不需要额外的空间,速度相对较快
        • 缺点:内存碎片
    • 标记压缩:1. 从根节点开始标记引用对象;2. 遍历整个堆空间,清除没有引用的对象,并把存活的空间压缩到堆的一个区域。
      • 优缺点:
        • 优点:不需要额外的空间,不存在内存碎片
        • 缺点: 速度稍慢
    • 引用计数:每增加一个引用计数加一,减少一个引用计数减一,垃圾回收时只回收计数为0的对象。
      • 缺点:不能解决循环引用额问题,会引发内存泄漏

GCRoots

在Java语言里,可作为GC Roots对象的包括如下几种:

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

引用分类

  • 强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  • 软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  • 虚引用
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用于检测对象是否已经从内存中删除,跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
虚引用的唯一目的是当对象被回收时收到一个系统通知。
  • Reference
  static class Strong{
    String str = "hello";

    Strong(String str){
      this.str=str;
    }

    @Override
    public String toString() {
      return str;
    }
  }

  public static void main(String[] args) {
    ReferenceQueue queue = new ReferenceQueue<>();
    // 软引用
    SoftReference softReference = new SoftReference<>(new Strong("aaa"),
        queue);
    System.out.println(softReference.get());    // aaa
    System.gc();
    System.out.println(softReference.get());    // aaa
    System.out.println(queue.poll());           // null
    // 弱引用
    WeakReference weakReference = new WeakReference<>(new Strong("bbb"), queue);
    System.out.println(weakReference.get());    // bbb
    System.gc();
    System.out.println(weakReference.get());    // null
    System.out.println(queue.poll().getClass());  // WeakRegerence

    // 虚引用
    Strong x = new Strong("ccc");
    WeakReference reference = new WeakReference(x);
    PhantomReference phantomReference = new PhantomReference<>(
        x, queue);
    System.out.println(reference.get());        // null
  }
 
 
  • weakHashMap
  public static void main(String[] args) throws Exception {

    // weakHashoMap key键是弱引用的键, 如果key被回收则get是会自动remove掉value
    String test = new String("test");
    String tmp = new String("tmp");
    Map weakmap = new WeakHashMap();
    Map map = new HashMap();
    map.put(test, "test");
    map.put(tmp, "tmp");


    weakmap.put(test, "test");
    weakmap.put(tmp, "tmp");

    map.remove(test);
    test=null;
    tmp=null;

    System.gc();
    Iterator itrTest = map.entrySet().iterator();
    while (itrTest.hasNext()) {
      Map.Entry en = (Map.Entry)itrTest.next();
      System.out.println("map:"+en.getKey()+":"+en.getValue());
    }

    Iterator itrTmp = weakmap.entrySet().iterator();
    while (itrTmp.hasNext()) {
      Map.Entry en = (Map.Entry)itrTmp.next();
      System.out.println("weakmap:"+en.getKey()+":"+en.getValue());
    }
  }
  • sofrReference
  public static void main(String[] args) throws InterruptedException {

    // 总堆内存设置200M
    //100M的缓存数据
    byte[] cacheData = new byte[100 * 1024 * 1024];
    //将缓存数据用软引用持有
    SoftReference cacheRef = new SoftReference<>(cacheData);
    //将缓存数据的强引用去除
    cacheData = null;
    System.out.println("第一次GC前" + cacheRef.get());  //第一次GC前[B@123772c4
    //进行一次GC后查看对象的回收情况
    System.gc();
    //等待GC
    Thread.sleep(500);
    System.out.println("第一次GC后" + cacheRef.get());  //第一次GC后[B@123772c4
    //在分配一个120M的对象,看看缓存对象的回收情况
    byte[] newData = new byte[120 * 1024 * 1024];
    System.out.println("分配后" + cacheRef.get());      // 分配后null
    
  }

垃圾回收器

CMS(并发-标记-清除)

CMS 是一种以获取最短回收停顿时间为目标的收集器。

步骤:

1.初始标记
此阶段仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快,但是会停顿

注意:这里不是 GC Roots Tracing 的过程

2.并发标记
GC Roots Tracing 的过程,这个阶段可以与用户线程一起工作,不会造成停顿,从而导致整个停顿时间大大降低
3.重新标记
是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
4.并发清除

优点:停顿时间短,但是总的 GC 时间长

缺点:

1.并发程序都是 CPU 敏感的,并发标记和并发清除可能会抢占应用 CPU
2.总的 GC 时间长
3.无法处理浮动垃圾

浮动垃圾:在并发清除过程中,程序还在运行,可能产生新的垃圾,但是本次 GC 确不可能清除掉这些新产生的垃圾了,所以这些新产生垃圾就叫浮动垃圾,也就是说在一次 CMS 的 GC 后,用户获取不到一个完全干净的内存空间,还是或多或少存在浮动垃圾的。

4.由于在并发标记和并发清除阶段,用户程序依旧在运行,所以也就需要为用户程序的运行预留一定空间,而不能想其他收集器一样会暂停用户程序的运行。在此期间,就可能发生预留空间不足,导致程序异常的情况。
5.是基于标记-清除的收集器,所以会产生内存碎片

G1

独立管理整个 java heap 空间,而不需要其他收集器的配合。

步骤:

  1. 初始标记
    与CMS 一样,只是标记一下 GC Roots 能直接关联到的对象,速度很快,但是需要停顿
  2. 并发标记
    GC Roots Tracing 过程,并发执行
  3. 最终标记
    并行执行,需要停顿
  4. 筛选回收
    并行执行,需要停顿
    G1收集器把 Heap 分为多个大小相等的 Region,G1可以有计划的避免进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先收集价值大的 Regin,保证 G1收集器在有限时间内获取最大的收集效率。

优点:

  1. 存在并发与并行操作,最大化利用硬件资源,提升收集效率
  2. 分代收集,虽然 G1可以独立管理整个 Heap,但是它还是保留了分代的概念,实际上,在分区时,这些区域(regions)被映射为逻辑上的 Eden, Survivor, 和 old generation(老年代)空间,使其有目的的收集特定区域的内存。
  3. 空间整合,G1回收内存时,是将某个或多个区域的存活对象拷贝至其他空区域,同时释放被拷贝的内存区域,这种方式在整体上看是标记-整理,在局部看(两个 Region 之间)是复制算法,所以不会产生内存碎片
  4. 可预测的停顿时间


    image.png

你可能感兴趣的:(GC 及引用类型)