jvm-2 GC

发布于
java jvm

手动GC 易产生内存泄漏,直到内存溢出

垃圾回收算法

引用计数
  • 无法处理循环引用
  • 每次引用产生和消除,都伴随一次加或减法运算,有一定性能影响
标记清除

现代垃圾回收算法的思想基础

回收后的空间是不连续的,之后在不连续的内存空间中分配的效率要低于连续的空间

经典标记复制

解决标记清除算法内存回收后不连续的问题而衍生

  • 可用内存折半
  • 存活对象较多时,要复制大量对象,对效率也有影响
jvm

JVM 很多垃圾回收器都用到标记复制思想,为解决经典标记复制算法中内存折半问题,JVM 将内存划分为 3 个区

  • eden
  • from
  • to

from 和 to 大小相同,地位相等,且可角色互换

from 和 to 也被称为 survivor 空间,即幸存者空间,用于存放未被回收的对象

GC 时,从根节点出发,标记 eden 和 from 区所有存活对象,然后将这些对象复制到 to 区。而下次进行垃圾回收时,to 和 from 区角色互换,将 to 区 和 eden 区的幸存对象复制到 from 区

通过这种方法,解决经典标记复制算法中内存折半问题

通过调整 eden 区,from 区和 to 区的比例,来控制内存的利用率

标记压缩

新生代: 复制算法的高效性建立在存活对象少、垃圾对象多的前提下

老年代: 大部分对象都是存活对象,再用复制算法, 复制成本高

和标记清除一样, 可达标记,但之后, 不只清理未标记的对象, 而将所有的存活对象压缩到内存的一端,之后, 清理边界外所有的空间

既避免了碎片的产生, 又不需要两块相同的内存空间,性价比高

所以标记复制算法和标记压缩算法分别代表了空间换时间和时间换空间思想,并分别活跃在各自擅长的场景中


分代相关优化

卡表:

每一比特表示老年代的某一区域中的所有对象是否持有新生代对象的引用,加快新生代的回收速度

分区:根据目标停顿时间,每次回收若干小区间,而非整个堆

GC Root

  • 活着的线程和虚拟机栈(栈桢中的本地变量表)中的引用的对象

  • 方法区中的类静态属性引用的对象

  • 方法区中的常量引用的对象

  • 本地方法栈中 JNI 的引用的对象

  • 所有 synchronized 锁住的对象引用,即用于同步的监控对象

  • finalize 执行队列中的对象

    public class Finalize {
      
      public static Finalize obj;
      
      @Override
      protected void finalize() {
          System.out.println("in finalize");
          obj = this;
      }
      
      public static void main(String[] args) throws InterruptedException {
          Finalize o = new Finalize();
          o = null;
          System.gc();
          Thread.sleep(1000);
          if (obj != null) {
              System.out.println("object alive");
          }
          obj = null;
          System.gc();
          Thread.sleep(1000);
          if (obj == null) {
              System.out.println("finalize only invoke once");
          }
      }
    }
    

## 引用的强度

强引用

Object obj = new Object();

强引用所指向的对象在任何时候都不会被系统回收, 虚拟机宁愿抛出 OOM 异常, 也不会回收强引用所指向对象

软引用

一个对象如果只被软引用,只有堆空间不足时,才会被回收

public class SoftRef {

    private byte[] data = new byte[5 * 1024 * 1024];
    public static void main(String[] args) throws InterruptedException {
        SoftRef softRef = new SoftRef();
        ReferenceQueue referenceQueue = new ReferenceQueue<SoftRef>();
        SoftReference<SoftRef> softReference = new SoftReference<SoftRef>(softRef, referenceQueue);
        softRef = null;
        if (referenceQueue.poll() == null) {
            System.out.println("current reference queue is empty");
        }
        if (softReference.get() != null) {
            System.out.println("could get ref");
        }
        System.gc();
        if (softReference.get() != null) {
            System.out.println("still could get ref");
        }
        byte[] b = new byte[1024 * 1024 * 6];
        if (softReference.get() == null) {
            System.out.println("couldn't get ref");
        }
        if (referenceQueue.poll() == softReference) {
            System.out.println("reference queue work");
        }
    }
}

-Xmx10m 设堆最大为 10m

创建软引用并进行垃圾回收,软引用的对象并未被回收

再申请一个大对象时,当前剩余内存大小不足以应对新的内存请求,对之前的软引用对象进行回收


可以在创建软引用的时候将该软引用注册在一个引用队列中,该软引用对应的对象被回收时,JVM 会将该软引用推入队列中来作为对象回收的一个通知

finalize 复活不会被推入引用队列,只有对象真正的被回收时,才会被加入到引用队列

弱引用

只要发生了 GC 就会被清除,不关心当前内存是否严重不足

public class WeakRef {
    public static void main(String[] args) throws InterruptedException {
        WeakRef WeakRef = new WeakRef();
        ReferenceQueue<WeakRef> referenceQueue = new ReferenceQueue<WeakRef>();
        WeakReference<WeakRef> ref = new WeakReference<WeakRef>(WeakRef, referenceQueue);
        WeakRef = null;
        if (ref.get() != null) {
            System.out.println("weak reference alive");
        }
        if (referenceQueue.poll() == null) {
            System.out.println("reference queue is empty");
        }
        System.gc();
        Thread.sleep(1000);
        if (ref.get() == null) {
            System.out.println("weak reference cleaned");
        }
        if (referenceQueue.poll() == ref) {
            System.out.println("reference queue work");
        }
    }
}

软引用、弱引用都非常适合来保存那些可有可无的缓存数据

内存不足时, 缓存被回收, 不会导致内存溢出

内存充足时, 緩存又可以存在相当长的时间, 起到加速

虚引用

get 方总是会失败,并且必须和引用队列一起使用, 作用在于跟踪垃圾回收过程