深入理解Java虚拟机(2)—— 可达性分析算法、引用类型、对象的自我拯救、垃圾收集算法、HotSpot算法实现、垃圾收集器...

1. 概述

  • 引用计数算法

    给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。Java不是用该算法判断对象是否存活。

  • 可达性分析算法

    通过“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。

    在Java语言中,可作为GC Roots对象包括下面几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区常量池中引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

  • 引用

 引用分为强引用、软引用、弱引用、虚引用4中,4中引用强度一次逐渐减弱。

  • 强引用就是指程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用存在,垃圾收集器就不会回收掉被引用的对象。
  • 软引用用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行二次回收。提供了SoftReference来实现软引用
  • 弱引用也是用来描述非必须对象,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集器发生之前。当垃圾收集器工作时,无论当前内存是否足够都会回收。提供WeakReference实现弱引用
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成印影响,也无法通过虚引用来取得一个对象实例。提供PhantomReference类实现虚引用。

  • 对象的自我拯救

    即时在可达性分析算法中不可达对象,也并非是“非死不可”的,这时候他们暂时处于“缓刑”阶段,要真正宣告一个对象的死亡,至少要经理两次标记过程:   如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finaliza()方法。当对象没有覆盖finaliza()方法,或者finaliza()方法已经被调用,虚拟机将两种情况视为“没有必要执行”。

    如果这个对象有必要执行finaliza()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动创建、低优先级的Finalizer线程去执行。这个执行只是虚拟机会触发这个方法,不会承诺等待他运行结束。这样做的原因是,如果一个独享在finalize()中执行缓慢,或者死循环,将可能导致F-Queue队列中其他对象处于永久等待的,导致整个内存回收系统崩溃。

    finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在Finaliza()中成功拯救自己(只要重新与引用链上的任何一个对象进行关联),那么第二次标记时他将被移除“即将回收”的集合;如果对象这时候还没逃避,那基本上它就真的被回收了。

/**
 * 一次对象的自我拯救
 * 1. 对象可以被GC时自我拯救
 * 2. 这种自救机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 */
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 Throwable {
        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();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead: ");
        }
    }
}

2. 垃圾收集算法

2.1 标记-清除算法

    算法分为“标记”和“清除”两个阶段:首先标记处需要回收的对象,在标记完成后统一回收所有标记对象。

不足之处:效率低,空间问题:会产生大量不连续的空间碎皮,碎片过多导致无法分配较大对象。


2.2 复制算法    

    将可用内存按容量划分为大小相等的两块,每次使用其中的一块。每当这一块内存使用完后,就会将还存活的对象复制到另一块内存上,然后把用过的内存一次清理掉。这种算法导致内存缩小为了之前的一般。

2.3 标记-整理算法

    标记过程与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

2.4 分代收集算法

    根据对象存活周期的不同将内存划分为几块。将Java堆分为新生代和老年代,根据各个年代的特点采用最合适的收集算法。新生代中每次垃圾收集都有大量对象死去,就使用复制算法;而老年代中对象存活率高,就需要使用“标记-整理”算法进行回收。

3.HotSpot算法实现

3.1 枚举根节点  

    当使用可达性分析算法时,GC Roots节点中存在大量引用链,依次逐个检查会消耗很多时间。GC停顿:当GC开始时,必须保证执行系统暂停,不然分析过程中对象引用关系在不断变化,结果准确性不能保证。

    准确式GC:当执行系统停顿时,不需要一个不漏的检查完全部引用,虚拟机有办法知道哪些地方存在对象引用。在HotSpot中,使用OopMap的数据结构。在类加载完成,HotPot会把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC扫描直接可以得知信息。

3.2 安全点

     HotSpot在JIT编译过程中,会在特定的位置记录下栈和寄存器中哪些位置是引用,这个位置称为安全点。程序执行时只有到达安全点时才会执行GC。安全点以程序“是否具有让程序长时间执行的特征”为标准进行选定。一般选定方法调用、循环跳转、异常跳转等,会产生安全点。

    GC发生时会让所有的线程都执行到最近的安全点上再停顿。有两种方案:抢先式中断和主动式中断。

  • 抢先式中断:GC发生时,首先把所有的线程全部中断,如果有线程中断不在安全点上,就恢复线程,使其执行到安全点上。(几乎没有虚拟机实现这种方式)
  • 主动式中断:不直接对线程操作,简单的设置一个标志,各个线程执行时去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点时重合的。

3.3 安全区域 

    如果线程处于sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求。对于这种情况就需要安全区域来解决。安全区域是指一段代码片段之中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。我们可以吧安全区域看做扩展的安全点。  当线程执行到了安全区域中的代码时,首先标志自己进入了安全区域,当发生GC时,就不用管该线程了。

4 . 垃圾收集器

    垃圾收集器目前有Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器。前三种为新生代收集器。


4.1 Serial收集器

    该收集器为单线程新生代收集器,使用复制算法进行垃圾收集。在它进行垃圾收集时,必须暂停其他所有的工作线程。该收集器简单高效,对于单CPU环境,没有线程交互的开销,效率最高。

4.2 ParNew收集器

    ParNew收集器就是Serial收集器的多线程版本。ParNew收集也是使用-XX:+UseConcMarkSweepGC选项后的默认收集器,可以使用-XX:+UseParNewGC选项来强制指定它。它默认开启的收集线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads 参数限制垃圾收集的线程数。

4.3 Parallel Scavenge收集器

    Parallel Scavenge收集器是一个新生代收集器,他是用复制算法的收集器,也是并行的多线程收集器。它的目标是达到一个可控制的吞吐量。吞吐量= 运行用户代码时间 /(运行用户代码时间+垃圾收集时间)。

    Parallel Scavenge收集提供两个参数用户精确控制吞吐量,控制最大垃圾收集停顿时间-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 MaxGCPauseMillis参数允许的值是有个大于0的毫秒数,收集器尽可能保证内存回收花费的时间不超过设定值。GCTimeRatio参数的值应当是一个大于0且小于100的整数。允许最大GC时间占总时间的 1/(N + 1)

4.4 Serial Old 收集器

    Serial Old是Serial收集器的老年代版本,单线程收集器,使用“标记——整理”算法。主要意义在于给Client模式下虚拟机使用。

4.5 Parellel Old 收集器 

    Pareller Old是Parellel Scavenge收集器的老年代版本,使用多线程和“标记-整理算法”。

4.6 CMS收集器

            CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除“算法实现,整个过程分为四部,1.初始标记,2.并发标记,3.重新标记,4.并发清除。其中初始标记与重新标记仍然需要停顿,但是并发标记和并发清除会随着用户进程一起进行。有以下3个缺点:

              (1)对CPU资源敏感,在并发阶段因为占用了一部分线程,导致应用程序变慢,总吞吐量降低。CMS默认启动
                的回收线程是(CPU数量 + 3)/ 4,也就是CPU在4个以上,并发回收时垃圾收集线程不少于25%CPU资源。
                如果CPU核心数较少时,对用户程序影响较大。虚拟机提供一种“增量式并发收集器”i-CMS 的CMS收集器
                变种,在并发标记、清理时让GC线程、用户线程交替运行(现在不适用)。
              (2)无法处理“ 浮动垃圾”。CMS并发清理时,程序运行还会有新的垃圾产生,CMS无法再当次收集处理
                他们,只好期待下一次GC时再清理掉,这部分垃圾称为“浮动垃圾”。可以通过
                -XX:CMSInitiatingOccupancyFraction 命令修改触发GC的百分比,默认92%。设置的过高容易出错。
              (3)CMS基于“标记—清除”算法实现,GC会导致大量的空间碎片。没有足够大的空间,不得不提前
                触发Full GC,因此CMS提供-XX:+UserCMSCompactAtFullCollection开关参数(默认开启),用于CMS收
                集器顶不住要Full GC时 开启内存碎片合并整理,停顿时间会变长。还提供了

                -XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,再来一 次                                         带压缩的(默认为0,表示每次进入Full GC都进行碎片整理)。

4. G1收集器

    是一款面向服务器的垃圾收集器,G1可以充分使用多个CPU,利用并发方式让Java程序继续运行;采用不同的方式进行分代垃圾收集;整体采用“标记—整理”算法,局部(region之间)采用“复制”算法;能使使用者明确指定在一个长度为M毫秒的时间片段,消耗在垃圾收集器上的时间不超过N毫秒。

    G1将整个Java堆分为多个大小相等的独立区域(region),新生代与老年代不再物理隔离,他们都是一部分Region的集合。G1可以通过每一个Region垃圾回收的价值,去回收价值最大的。

    G1收集器中,Region中对象的相互引用(其他收集器新生代与老年代的对象引用),虚拟机使用Remembered Set来避免全栈扫描。G1中每个Region都有一个对应的Remembered Set,当虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收,在GC根节点的枚举范围加入Remembered Set 保证不对全栈扫描。

    G1收集器运作分为 1.初始标记 2.并发标记 3.最终标记 4.筛选回收。:初始标记仅仅标记一下GC Roots能直接关联到的对象,并让下一阶段用户线程并发进行,这个阶段需要暂停,但是耗时很短;并发标记从GC Roots对堆中对象进行可达性分析,找出存活对象,可与用户线程一起运行,耗时长;最终标记修整并发标记时程序持续运行发生变化的标记记录,将对象变化记录在线程的Remembered Set Logs中,在将其中数据合并到Remembered Set中,需要停顿,可并行执行。筛选回收对各个Region的回收价值和成本记性排序,根据用户的期望进行回收。



转载于:https://my.oschina.net/chunzao/blog/2885240

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值