JVM--垃圾回收

2018-10-26  本文已影响0人  _fatef

一、 如何定位垃圾

1. 引用计数算法(reference counting)

    /**
     * testGC() 方法执行后,objA 和 objB是否会被GC?
     * ClassName: ReferenceCountingGC 
     */
    public class ReferenceCountingGC {
        public Object instance = null;
        private static final int _1MB = 1024 * 1024;
        
        private byte[] bigSize = new byte[2 * _1MB];
        
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            
            objA = null;
            objB = null;
            
            System.gc();
        }
    }

对象 a, b 相互引用,除此之外没有其他引用指向a 或者 b,在这种情况下,a 和 b 实际已经死亡,但是由于他们的引用计数器皆不为 0 ,在引用计数算法的心中,这两个对象还活着。因此,这些循环引用对象所占据的空间将不可回收,从而造成内存泄漏。

2. 可达性分析算法

  1. 误报只会使JVM 损失了部分垃圾回收的机会,即当GC标记完成,还未开始回收,你更新了其中一个引用,使之指向 null,那么原来的指向对象本可以被回收(但没有被GC 标记为可回收,只能等待下次标记)。
  2. 漏报是已经被 GC 标记为可回收的对象,更新为被其他对象指向,垃圾回收器直接给回收掉了,则可能会直接导致JVM 崩溃。

3. Stop-the-world 以及安全点

4. Java的四种引用:强软弱虚

5. 对象的最后一次自我拯救

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    public void isAlive() {
        System.out.println("yes, i am still alive:)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 对象第一次成功自我拯救
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法优先级很低,所以暂停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        
        // 对象第二次自我拯救 失败!
        SAVE_HOOK = null;
        System.gc();
        // 由于 finalize() 方法优先级很低,所以暂停 0.5秒等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}
运行结果:
finalize method executed!
yes, i am still alive:)
no, i am dead :(

执行结果一次成功自救,一次失败,这是因为任何一个对象的 finalize() 方法都只会被系统自动调用一次,如果对象面临下一次回收,它的 finalize() 方法不会再次执行。

二、如何回收垃圾

1. 标记 - 清除(Mark-Sweep)

2. 压缩(compact)也叫标记 - 整理(Mark-Compact)

3. 复制(copy)

4. 分代收集(Generational Collection)

5. JVM 的堆划分

  • 当 调用new 指令时,它会在 Eden 区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里划空间是需要进行同步的。不然可能出现两个对象公用一段内存的事故。
  • JVM 的解决方法就是:每个线程都可以向JVM 申请一段连续的内存,作为线程私有的TLAB(线程私有缓冲区 Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。这个操作需要加锁,线程需要维护两个指针(可能更多,主要的就两个),一个指向TLAB 中空余内存的起始位置,一个则指向 TLAB 末尾。
  • 接下来的new 指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB 已经没有足够的空间来满足本次新建操作。此时,便需要当前线程重新申请新的 TLAB。
  • 如果 Eden 区的空间耗尽,此时JVM 会触发一次 Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到 Survivor 区。新生代有两个 Survivor 区,我们分别用 from 和 to来指代。其中 to 指向 Survivor 区是空的。
  • 当发生 Minor GC时,Eden 区和 from指向的 Survivor 区中的存活对象会被复制到 to指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC时,to 指向的Survivor 区还是空的。
  • 新生代和老年代的划分: JVM会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果 单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
    注:为什么是15 而不是其他?原因是 HotSpot会在对象头中的标记字段里记录年龄,分配到的空间只有4位,因此只能记录到15
  • 优缺点:Minor GC 是不用对整个堆进行垃圾回收,此时有一个问题就是老年代的对象可能引用新生代的对象。也就是说,在标记存活对象的时候,我们需要扫描老年代中的对象。如果该对象拥有对新生代对象的引用,你们这个引用也会被作为 GC Roots。如此,岂不是又做了一次全堆扫描?

6. 卡表(Card Table)

HotSpot为了解决Minor GC的时候不用进行全堆扫描而提供的方案

三、 垃圾回收器

上一篇下一篇

猜你喜欢

热点阅读