深入理解java虚拟机---第三章垃圾收集器与内存分配策略

3.1 概述
哪些内存需要回收?
什么时候回收?
怎么回收?
这里垃圾回收器关注的是Java堆和方法区的内存,程序计数器、虚拟栈、本地方法栈三个区域会随着线程而生,随着线程而灭, 不用管他。

3.2对象已经死吗

引用计数算法:它很难解决对象之间相互循环引用的问题

可达性分析算法

当一个对象到GC Roots没有任何引用链时,则证明对象时不可用的

GC Roots的对象包括下面几种:

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

再谈引用

  • 强引用
    普通的new一个对象都属于强引用。只要强引用存在,GC就永远不会回收掉被引用的对象。
  • 软引用
    软引用用来描述一些还有用但是不是必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存异常。
  • 弱引用
    当垃圾回收器工作的时候,无论当前内存是否够用,都会回收掉掉只被弱引用关联的对象。
  • 虚引用
    一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时能收到一个系统通知。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是非死不可的,这个时候它们处于缓刑期,要真正宣告一个对象死亡,只要要经过两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么它将会被第一次标记并且进行下一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行她。这里的执行是指虚拟机会触发这个finalize()方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行 缓慢,或者发生了死循环,将很有可能导致F-Queue队列中其他对象永久处于等待,导致整个回收系统奔溃。
finalize()是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()中成功拯救自己——只要重新与引用链中的任何一个对象建立关联就可以将自己移出“即将回收”的集合;如果对象这个时候还没有逃脱,那基本上它就真的被回收了。
注意的是,finalize()方法在系统中只会被调用一次,如果这次在执行finalize()方法时成功拯救了自己,那么在下一次垃圾回收时则就会被老老实实的回收。
finalize()方法运行代价高昂,不确定性大,所以我们不推荐使用这个方法。

/**
 * 此代码演示了两点: 
 * 1.对象可以在被GC时自我拯救。 
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author zzm
 */
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 mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停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();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

输出结果:

finalize mehtod executed ! 
yes,i am still alive : ) 
no,i am dead : (

回收方法区

永久代的垃圾收集主要回收两部分:废弃常量和无用的类。
回收废弃常量与java堆中的对象非常类似。例如:如果常量池中有一个“abc”常量,但是当前系统中没有一个String对象是叫“abc”的,那么这个“abc”常量就会被系统清除出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与之类似。
回收无用的类则条件苛刻了许多,类需要满足以下三个条件才能算作无用的类

  • 该类所有的实例都已经被回收,java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上诉三个条件的无用类进行回收,这里说的仅仅是“可以”,而不是像对象一样,不使用了就必然回收。是否对类进行回收,虚拟机提供了不同的参数进行控制。

3.3垃圾收集算法

1 标记—清除算法

标记、清除效率不高,且会产生大量不连续的内存碎片

2 复制算法

复制算法将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片的问题了,只要移动堆顶指针,按循序分配即可。

3 标记清理—整理算法

还有一种方式就是同样先标记好可回收的对象,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4 分带收集算法

根据对象存活周期的不同将内存划分为新生代和老年代,这样可以根据各个年代的特点使用最恰当的收集算法。在新生代中,每次垃圾收集时都会有大批对象死去,只有少量存活,那就用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-整理”或者“标记-清理”算法进行回收。

3.4 HotSpot的算法实现

1.枚举根节点:可达性分析,这项工作需要在保证一致性的快照中进行。枚举根节点需要停顿。

2.准确式GC:虚拟机可以知道某个位置记录了什么数据。在HotSpot实现中,使用一组称为OopMap的数据结构来达到这个目的。

3.安全点:在OopMap的协助写,HotSpot可以快速且准确地完成GC Roots枚举,但一个很现实的问题是:OopMap内容变化指令非常多,为每一条指令都生成一个OopMap将会需要大量的额外空间,这样GC的空间成本非常高。实际上,OopMap只在特定的位置记录了这些信息,这些位置称为“安全点”,即程序运行到这里不会导致对象的引用关系变化。

4.如何在GC发生时让线程都跑到安全点停顿下来。有两种方法,分别是抢断式中断和主动式中断,抢断式中断指的是,GC发生时把所有线程全部中断,发现没有到安全点的线程恢复起来跑到安全点。主动式中断指的是设置一个标志,让所有线程去轮询这个标志,轮询标志和安全点是重合的,判断在轮询标志中的线程就自己中断挂起。当前HotSpot使用的是主动式中断。

5.安全区域指的是对象的引用不会发生变化的区域。

3.5 垃圾收集器

1、Serial收集器:Serial收集器是最基本、发展历史最悠久的收集器。这是一个单线程收集器。它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,而且在垃圾收集过程中,虚拟机中其他线程必须暂停,等待该线程收集完成。
优点:简单而高效
缺点:需要停顿
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:虚拟机Client模式下默认的新生代收集器。

2、parNew收集器:ParNew是Serial的多线程版本。除了使用多线程收集外,其他方面与Serial没有任何区别。
优点:简单而高效
缺点:需要停顿
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:虚拟机Server模式下默认的新生代收集器。唯一可以配合CMS收集器工作的垃圾收集器。

3、Parallel Scavenge收集器:多线程处理器,吞吐量优先。吞吐量指的是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码的时间 + 垃圾收集时间 ) 。提供 -XX:MaxGCPauseMilis 用来控制最大垃圾收集停顿时间。 提供 -XX:GCTimeRatio 用来控制 GC时间占总时间的比率。提供 -XX:UseAdaptiveSizePolicy用来进行GC自适应调节策略。
优点:能够控制垃圾收集时间的吞吐量。
缺点:效率相对较低。
算法:新生代采用复制算法,老年代采用 标记-整理 算法。
使用场景:适合后台运算而不需要太多交互的任务。

4、Serial Old收集器:是Serial的老年代版本,单线程收集器,使用 标记-整理 算法。可作为CMS收集器的后备预案,在并发手机发生Concurrent Mode Failure时使用。

5、Parallel Old收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,多线程收集器,使用 标记-整理 算法。吞吐量优先原则。

6、CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用 标记-清除 算法。
整个过程需要4个步骤:

    初始标记:需要“Stop The World”,标记一下GC Roots能直接关联到的对象,速度很快。
    并发标记:是进行GC Roots Tracing的过程。
    重新标记:需要“Stop The World”,修正并发标记期间,用户程序继续运行而导致标记产生变动的那一部分标记。停顿时间稍长,但比初始标记短。
    标记清除:和用户线程并发的收集垃圾清理工作。

由于整个收集过程中耗时最长的两个部分,并发标记以及标记清除都可以和用户线程一起运行,所以CMS收集器可以看作是和用户线程一起运行的。

优点:并发收集、低停顿。
缺点:
    1、对CPU资源非常敏感。默认回收线程数是(CPU数量 + 3)/ 4 。CPU数量越多,CMS消耗资源相对来说越少。
    2、无法处理浮动垃圾。可能出现“ConCurrent Mode Failure”失败而导致另一次Full GC的产生。
    3、标记-清除算法会产生空间碎片。

7、G1收集器:面向服务器端应用的垃圾处理器。整体来看是采用 标记 - 整理 算法,而局部上来看是基于 复制 算法来实现的。

G1与其他垃圾收集器相比具备如下特点:
1、并发与并行:能使用多个CPU来缩短停顿的时间,多线程收集。
2、分代收集:G1收集器存在分代概念,但是单独管理整个堆。
3、空间整合:整体基于标记-整理算法,局部基于复制算法。都不会产生空间碎片。
4、可预测的停顿:在缩短停顿时间的基础上,能够建立停顿时间模型。

G1收集器将整个堆划分成多个相同大小的独立区域(Region),并维护一个优先列表,用于跟踪记录每一个Region的回收价值(根据回收获得的空间大小以及回收所需的时间确定),每次在允许的时间内,优先回收价值最大的Region(这就是Garbage-First名称的由来)。

虚拟机为了防止G1收集器扫描全部Region或者其他收集器扫描新生代以及老年代。G1中Region的对象引用以及其他收集器新生代与老年代之间的对象引用,虚拟机使用Remembered Set(RSet)来避免进行全堆扫描。

G1运行大致可以分为:
1、初始标记:标记一下GC Roots能直接关联到的对象。
2、并发标记:从GC Roots开始进行可达性分析。
3、最终标记:修正并发标记期间用户程序运行导致的标记变动。
4、筛选回收:进行Region的回收价值排序并进行垃圾回收。

3.6 内存分配与回收策略

  • 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

    • Minor GC与Full GC有什么不同吗?
      新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较块。

      老年代GC(Full GC):指发生在老年代的GC,出现了Full GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Sacvenge收集器的收集策略里就有直接进行Full GC的策略选择过程)。Full GC的速度一般会比Minor GC慢10倍以上。

  • 大对象直接进入老年代
    所谓大对象就是指需要大量连续内存空间的java对象,最典型的就是很长的字符串以及数据,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

  • 长期存活的对象将进入老年代
    虚拟机使用分代收集的思想来管理内存,那么内存回收的时候就必须能够识别哪些对象应该放在新生代,哪些应该放在老年代。所以,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,就被移动到Survivor空间中,并且对象年龄加一。对象在Suvivor区中每“熬过”一次Minor GC,年龄就加一,直到年龄增加到一定程度(15岁),就将会被晋升到老年代。

  • 动态对象年龄判定
    为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄都要必须达到一定程度(15岁),如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到默认要求的年龄。

  • 空间分配担保
    在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次MInor GC是有风险的;如果小于,或者参数设置为不允许冒险,那么这时就要改为进行一次Full GC。

猜你喜欢

转载自blog.csdn.net/qq_40182703/article/details/81193901