Bootstrap

第3章 垃圾收集器与内存分配策略

3.2 对象已死吗?

      在垃圾回收器回收之前需要确定对象是活着还是死了(不在被任何途径调用)。
3.2.1 引用计数算法
1、什么是引用计数算法?它的弊端是什么?

     引用计数算法:给对象添加一个引用计数器,当对象被引用时,计数器加一。当引用实效时,计数器减一。当任何时刻计数器为0时,就认为对象已死。这种方法方便,高效。但是有一个问题,当两个对象相互应用对方,但是不在有外界调用它们时。这两个对象的程序计数器都不为0,所以这两个对象不会被垃圾回收器回收。例如下面程序:

public class ReferenceCountingGC {


    public Object instance = null;

    private static final int _1MB = 1024*1024;

    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {

        ReferenceCountingGC gc1 = new ReferenceCountingGC();

        ReferenceCountingGC gc2 = new ReferenceCountingGC();

        gc1.instance = gc2;

        gc2.instance = gc1;
        //虽然gc1本身为null,但gc2.instance还引用gc1,所以引用计数算法无法通知GC收集器回收它
        gc1 = null;

        gc2 = null;

        System.gc();

    }
}

Java虚拟机并没有采用引用计数算法来管理内存(主要是动态分配的内存部分)

3.2.2 可达性分析算法
1、什么是可达性分析算法?
2、在java语言中可作为GC Root的对象包括哪四种?

该算法的思路是通过一系列通过称为GC Root的对象开始向下搜索,所走的搜索路径称为引用链。当从GC Root到对象没有一条可达的引用链时就说明该对象已死。

可作为GC Root的对象有:

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

3.2.3 再谈引用
1、强引用,软引用,弱引用,虚引用之间的区别?

    jdk1.2之前对引用的定义:如果一块内存中存储着一个对象的地址那么这块内存称为一个引用。这种引用只有引用和不引用两种状态,对于那些不必须的对象就无能为力了。jdk1.2 之后引入了4种新的引用类型:1.强引用 2.软引用 3.弱引用 4.虚引用

    a> 强引用:程序中普遍存在的,类似Object o = new Object(),只要有强引用对象就不能被回收。

    b>软引用:有些对象还有用但非必需的,这些对象使用软引用。当内存溢出之前会将这些对象进行回收,如果回收后内存还不满足则抛出内存溢出异常。在JDK1.2之后,提供SoftReference类来实现软引用

   c>弱引用:有些对象是非必需的,这些对象使用弱引用。当垃圾回收器工作时,无论内存是否足够都会回收这些对象。在JDK1.2之后,提供WeakReference类来实现弱引用

   d>虚引用,这些引用的对象对于回收没有任何影响。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时会收到一个系统通知。在JDK1.2之后,提供PhantomReference类来实现软引用

3.4.2 生存还是死亡
1、在可达性分析中“不可达”的对象,真正的被宣告死亡,至少要经历的两次标记过程是什么?
2、finalize()方法可以被系统重复调用吗?

    即使通过可达性分析算法,某个对象没有可达的引用链,也不能说明该对象已经死亡。垃圾回收器在回收一个对象时会对其进行两次标记。第一次标记后会对它们进行一次筛选,执行筛选的条件是该对象是否覆盖了finalize()方法或者虚拟机是否已经调了finalize()方法。这两种情况虚拟机都视为“没有必要执行”。

如果对象执行了finalize()方法,那么该对象会被放倒一个F-Queue队列中。稍后虚拟机会创建一个低优先级的finalize线程去执行finalize()方法,但不能保证方法执行完成。因为这个方法执行速度很慢,如果都执行完成会影响垃圾回收的效率。稍后GC会小范围内进行第二次标记,如果这时对象有到GC Root的引用链时该对象会移除被回收的集合,否则会被回收。

一次对象自我拯救的示例:

/*
   对象可以在被GC时自救
   这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC {

   
    public static FinalizeEscapeGC SAVE_HOOK = null;

    @Override

    protected void finalize() throws Throwable {

        super.finalize();

        System.out.println("finalize exec!");

        SAVE_HOOK = this;

    }

    public static void isAlive() {

        System.out.println("i am is alive");

    }

    public static void main(String[] args) throws InterruptedException {

        SAVE_HOOK = new FinalizeEscapeGC();

        SAVE_HOOK = null;

        System.gc();

        Thread.sleep(500);

        if(SAVE_HOOK == null) {

            System.out.print("i am dead");

        }else {

            SAVE_HOOK.isAlive();

        }

        SAVE_HOOK = null;

        System.gc();

        Thread.sleep(500);

        if(SAVE_HOOK == null) {

            System.out.print("i am dead");

        }else {

            SAVE_HOOK.isAlive();

        }

    }

}

3.2.5 回收方法区
1、方法区(永久代)中的常量如何被判为废弃常量?在何时会被回收?
2、而要判定一个类是“无用的类”,需要同时满足哪三个条件?

虚拟机规范中规定可以不对方法区(或永久代)进行回收。在堆中尤其新生代中回收率为70%到95%。而在方法区中回收效率要低的多。

永久代中的垃圾回收包括两部分:对常量的回收和无用类的回收。对常量的回收与堆上的对象类似,如果常量池中的一个字符串,没有任何对象引用它,那么该字符串可以被回收。对类的回收复杂的多,要符合以下几个条件:1.该类的所有对象都已经被回收。2该类的classloader被回收。3该类的Class对象任何地方都不会使用到。具备了以上条件,类仅仅是可以被回收。类不跟对象一样一定需要回收。还取决于虚拟机中配置的一些参数。在大量使用动态代理,反射,cglib,生成大量jsp文件的系统中,需要提供类的卸载功能。保证方法区不会内存溢出。

3.3垃圾收集算法
3.3.1 标记—清除算法
1、什么是标记清除算法?
2、标记清除算法的不足有哪两个?

   a>标记-清除算法(mark-sweep):首先把需要清除的所有对象全部标记出来,然后一次性清除。该方法是后续所有方法的基础,但存在问题:1,标记清除两个过程效率都不高。2,清除后会造成大量的空间碎片,导致需要分配较大空间时不能满足,使得提前需要再一次垃圾回收。

 

   b>复制算法(copying):将内存分为两部分。先将对象放到一块内存里,内存放满后,将其中活着的对象,复制到另一块内存中然后把剩下的所有对象一块清除。此种方法不用考虑空间碎片问题,而且效率高只需将对象的指针移动位置,按顺序分配内存即可。缺点时使用内存只有原来的一半。浪费空间较大。

现在虚拟机大多采用这种方法来回收新生代。据研究新生代98%的对象“朝生夕死”,所以采用1:1的比例浪费空间。而是将内存分为一块较大的Eden空间和两个较小的Survivor空间,它们的比例是8:1:1,首先将Eden空间和一块Survivor空间用来存放对象。当垃圾回收时,先将次两块空间活着的对象复制到另一块Survivor空间里,然后将这两块空间的对象清除掉。这样只会“浪费”10%的空间。

但是如果10%的Survivor空间不能容纳复制过来的对象时,会将老年代的空间作为担保,把容纳不开的对象放入老年代。

   c>标记-整理算法(mark-compact):对于对象存活率比较高的情况,采用复制算法,效率会变低,所以老年代不适合此种方法。老年代所使用的方法叫标记-整理算法。标记后不对可回收对象进行回收,而是将存活的对象向一边移动,然后把边界以外的内存全部清除掉。

   d>分带回收算法:该算法是以上算法的综合应用。java堆将分为新生代和老年代。新生代由于对象死亡率较高,所以使用“复制”算法进行回收。老年代的对象死亡率小,所以使用“标记-清除”或者“标记-整理”进行回收。

3.3.2 复制算法
1、什么是复制算法?
2、复制算法的不足是什么?
3、为什么复制算法常用在新生代的垃圾回收中?老年代为什么不行?

3.3.3 标记—整理算法
1、什么是标记—整理算法?

3.3.4 分代收集算法
1、什么是标记整理算法?

3.4 HotSpot的算法实现
3.4.1 枚举根节点
1、准确式GC是如何知道内存中哪些地方存放着对象引用的?采用了哪种数据结构?

3.4.2 安全点
1、什么是安全点?
2、抢先式中断和主动式中断有什么就区别?

3.4.3 安全区域
1、什么是安全区域?安全区域解决了什么问题?

3.5 垃圾收集器
3.5.1 Serial
1、Serial收集器与Serial Old收集器区别?这两种收集器分别作用于哪个堆区域?分别采用何种垃圾收集算法?
2、单线程还是多线程?

3.5.2 ParNew收集器
1、ParNew收集器与Serial收集器关系?
2、ParNew收集器作用区域?采用何种垃圾收集算法?
3、ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器很重要的原因是除了Serial收集器外目前只有它能跟哪个收集器配合使用?

3.5.3 Parallel Scavenge收集器
1、作用区域?垃圾收集算法?单线程or多线程?
2、 Parallel Scavenge收集器区别于其他收集器的特点是什么?
主要适合于哪些任务?
3、Parallel Scavenge收集器提供了哪两个参数用于精确控制吞吐量?
(-XX:MaxGCPauseMillis、-XX:GCTimeRatio)
分别作用是什么?

3.5.4 Serial Old收集器
1、作用区域?垃圾收集算法?单线程or多线程?

3.5.5 Parallel Old 收集器
1、作用区域?垃圾收集算法?单线程or多线程?
2、出现的巨大意义?

3.5.6 CMS收集器
1、CMS收集器是一款以什么为目标的收集器?
2、作用区域?垃圾收集算法?单线程or多线程?
3、运作过程四步骤以及详细介绍?哪些步骤需要stop the world?哪些步骤可并发执行?哪些步骤时间最久?
4、CMS三大缺点?

3.5.7 G1收集器
1.相比于其他GC收集器,G1收集器具有哪几四大优点?
2、G1收集器从整体上来看是采用什么算法?从局部上来看是采用什么算法?
3、G1收集器的运作大致可分为哪四个步骤?

3.6 内存分配与回收策略
3.6.1 对象优先在Eden分配
1、大多数时候,对象的内存在哪里分配?

3.6.2 大对象直接进入老年代

3.6.3 长期存活的对象将直接进入老年代
1、什么是对象年龄计数器?
2、对象从出生到被移入老年代的具体流程是什么?

3.6.4 动态对象年龄判定
1、对象一定必须达到默认年龄才能进入老年代吗?什么是动态对象年龄判定?

3.6.5 空间分配担保
1、什么是“冒险”?
 

;