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

1、判断对象是否死亡

垃圾收集器在对堆进行回收前,第一件事就是确定这些对象之间哪些还“存活”着。

1.1引用计数法

给对象添加一个引用计时器,每当有一个地方引用它,计数器就加1;当引用失效时,计数器就减1,任何时刻计数器为0的对象就是不可能在被使用的。

但是主流的虚拟机都没有选用这种方法来管理内存,因为他不能解决对象之间循环引用引起的问题。

例:对象objA和对象objB都有字段instance,赋指令objA.instance = objB 以及objB.instance = objA,在之后把这两个对象置空,没有任何引用,但是他们互相引用,导致引用计数法无法回收他们。

/**
 * Description: VM options参数为:  -XX:+PrintGCDetails  打印堆栈信息
 */
public class TestGC {
    private Object instance = null;
    private static final int _1mb = 1024*1024;
    //只为占点内存,方便观看gc日志是否被回收
    private byte[] bytes = new byte[2*_1mb];
    public static void main(String[] args) {
        TestGC t1 = new TestGC();
        TestGC t2 = new TestGC();
        t1.instance = t2;
        t2.instance = t1;
        t1 = null;//可以删掉对比一下输出
        t2 = null;//可以删掉对比一下输出
        //调用垃圾回收
        System.gc();
    }
}

运行结果:可以看到没有我们的bytes对象2Mb,虚拟机并没有因为这两个对象互相引用而选择不回收,相反虚拟机不是用这种方法来判断对象是否存活的。

在这里插入图片描述

1.2、可达性分析算法

算法的思想是通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,所有所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的。
在这里插入图片描述

可以作为GC Roots 的对象包括:

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

4种强度依次逐渐减弱

强引用

像Object obj = new Object() 这就是强引用,只要这种强引用存在,垃圾收集器永远不会回收被引用的对象

软引用

它来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会对这些对象列进回收的范围进行第二次回收哦,如果还没有足够的内存,才会抛出内存溢出异常。SoftReference类来实现软引用

弱引用

他比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作是,不管内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference类来实现弱引用

虚引用

它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存空间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。PhantomReference类来实现虚引用。

1.4 判断对象生存还是死亡

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

如果这个对象被判定为有必要执行finalize()方法,这个对象将被放在F-Queue队列中,并在之后虚拟机会自动创建一个低优先级的Finalizer线程去找机会触发这个方法,但不承诺会等待它运行结束,这样做是因为:如果一个对象在finalize()方法中执行缓慢或出现死循环,将会导致F-Queue队列中其他对象永久处于等待,导致内存回收崩溃。finalize()是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue进行二次小规模的标记,如果在刚刚finalize()过程中与引用链上的任何一个对象建立连接,它就可以在第二次标记时将他移除“即将回收“的集合。如果还没有逃脱,那么他就真的被回收。

代码示例:

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("我还活着");
    }
    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("结束方法执行。。。,将空对象重新指向GC Roots的引用");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();//进行gc时调用finalize()方法
        //因为finalize方法优先级很低,所以暂停几秒等它结束
        Thread.sleep(1000);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("我已经没了...");
        }

        //下面一样的代码,自救失败
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停几秒等它结束
        Thread.sleep(1000);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("我已经没了...");
        }
    }
}

运行结果:

在这里插入图片描述

两个一样的代码,执行结果确实一次逃脱成功,一次失败,因为任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,他的finalize()方法不会被再次执行。因此第二段代码自救失败。

finalize()关闭资源运行代价高,不确定性大,最好用try-finally来关闭

1.5、回收常量池和类

常量池只要没有其他地方来引用这个常量,就可以被回收

类回收要满足三个条件:

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

即使满足了三个条件,是否被回收,还是根据对应虚拟机的策略来控制。如HotSpot虚拟机提供-Xnoclassgc参数控制

2、垃圾回收算法

1.复制算法

从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存上去,之后将原来的那一块儿内存全部回收掉

在这里插入图片描述

适用场合:

  • 存活对象较少的情况下比较高效
  • 扫描了整个空间一次(标记存活对象并复制移动)
  • 用于年轻代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少

缺点:

  • 需要一块儿空的内存空间
  • 需要复制移动对象

将新生代内存分为一块较大的Eden空间和两块较小的Suivivor空间,每次使用Eden和其中一块Survivor。当回收哦时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1,也就是每次新生代内存空间只能使用(80%+10%),只有10%的空间会被浪费,但是没办法保证每次剩余的都小于剩余的10%,当Survivor空间不够时,就会依赖老年代进行分配担保

  • 分配担保就好比我们去银行贷款,如果我们信誉好,在98%都能按时偿还,银行就认为我们下一次也能还钱,只需要有一个担保人,如果我们不能还款,担保人替我们还,那银行就认为没有风险了。
  • 内存的分配担保如果另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。
2.标记清除

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段

在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

在这里插入图片描述

适用场合

  • 存活对象较多的情况下比较高效
  • 适用于年老代(即旧生代)

缺点

  • 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
  • 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
3.标记整理

标记-整理算法是在标记-清除算法的基础上做了一些优化。

首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

在这里插入图片描述

4.分代收集算法

分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

在这里插入图片描述

3、垃圾回收机制

年轻代分为Eden区和survivor区(两块儿:survivor1和survivor2),且Eden:from:to==8:1:1。

直通BAT必考题系列:JVM的4种垃圾回收算法、垃圾回收机制与总结

jvm内存结构

1)新产生的对象优先分配在Eden区(除非配置了-XX:PretenureSizeThreshold,大于该值的对象会直接进入年老代);

2)当Eden区满了或放不下了,这时候其中存活的对象会复制到Survivor1区。

这里,需要注意的是,如果存活下来的对象Survivor1区都放不下,则这些存活下来的对象全部进入年老代。之后Eden区的内存全部回收掉。

3)之后产生的对象继续分配在Eden区,当Eden区又满了或放不下了,这时候将会把Eden区和Survivor1区存活下来的对象复制到Survivor2区(同理,如果存活下来的对象Survivor2区都放不下,则这些存活下来的对象全部进入年老代),之后回收掉Eden区和Survivor1区的所有内存。

4)如上这样,会有很多对象会被复制很多次

  • (每复制一次,对象的年龄就+1),默认情况下,当对象被复制了15次(这个次数可以通过:-XX:MaxTenuringThreshold来配置),就会进入年老代了。
  • 动态对象年龄判定,如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年代。

5)当年老代满了或者存放不下将要进入年老代的存活对象的时候,就会发生一次Full GC(这个是我们最需要减少的,因为耗时很严重)。

6)老年代在放不下之后,就会产生OOM异常

6)在发生MinorGC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC是安全的。如果不成立会继续检查老年代最大可用连续时间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试进行一次Minor GC,尽管有风险;如果小于,这时进行一次Full GC

4、HotSpot的算法实现

1、枚举根节点

可达性算法中找出GC Roots节点,但是这些结点可能很多,如果逐个检查里面的引用,必然消耗很多时间,而且在找GC Roots时必须在一个保证一致性的快照中进行(整个分析roots节点期间整个操作系统不会发生变化),不可以出现一边分析节点一边对象引用不断变化。

现在主流的Java虚拟机都是准确GC,当系统暂停下来时,在HotSpot实现中,使用一组称为OopMap的数据结构来完成,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定位置记录下栈和寄存器中哪些位置是引用的。这样GC在扫描时就可以直接得知这些信息。

2、安全点

在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但是随着引用关系变化,或者说OopMap内容变化的指令非常多,为每一个指令都生成对应的OopMap。那么会需要大量的额外空间,GC成本会变高。

实际上虚拟机只在特定的位置记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并非在所有的地方都能停顿下来进行GC,只有到达安全点时才能暂停。Safepoint的选定不能太少以至于让GC等待时间太长。也不能太多,造成系统的负荷。但是大部分的指令执行都很快,程序长时间执行最显著的特征就是“指令复用”,例如循环。

理论上,在解释器的每条字节码的边界都可以放一个safepoint,不过挂在safepoint的调试符号信息要占用内存空间,如果每条机器码后面都加safepoint的话,需要保存大量的运行时数据

*****对于Sefepoint,需要考虑的问题时如何在GC发生时让所有的线程(不包括执行JNI调用的线程)都跑到最近的安全点上在停顿下来。

  • 抢先式中断
    • 不需要线程的执行代码主动配合,在GC发生时,首先所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。
  • 主动式中断
    • 当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标记,各个消除执行时主动去轮询这个标记,发现中断标志为真时就自己中断挂起。轮询标记的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器bai能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。

3、安全区域(Safe Region)

安全点保证了程序执行时,在不太长的时间内就会遇到可进入的GC的Safepoint。但是程序没有分配到CPU时间,例如线程Sleep或者Blocked状态,这时线程无法响应JVM的中断请求,对于这种情况就需要安全区域:指在一个代码片段中,引用关系不会发生变化。在这个区域的GC都是安全的。

在执行到Safe Region中的代码时,首先标识自己已经进入安全区域,这段时间里JVM要发起GC,就不用管安全区域里线程的状态。在线程要离开时,他要检查系统是否已经完成了根节点的枚举(或者时整个GC过程),如果完成了,线程就继续执行,否则必须等待知道收到可以安全离开安全区域的信号为止。

5、垃圾收集器

CMS收集器

CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。

四个阶段:

(1)初始标记

(2)并发标记

(3)重新标记

(4)并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”

直通BAT必考题系列:7种JVM垃圾收集器特点,优劣势、及使用场景

CMS收集器主要优点

  1. 并发收集
  2. 低停顿

缺点:

CMS是基于“标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。

G1收集器

G1收集器的优势:

  1. 独特的分代垃圾回收器,分代GC: 分代收集器, 同时兼顾年轻代和老年代
  2. 使用分区算法, 不要求eden, 年轻代或老年代的空间都连续
  3. 并行性: 回收期间, 可由多个线程同时工作, 有效利用多核cpu资源
  4. 空间整理: 基于标记整理算法回收, 会进行适当对象移动, 减少空间碎片
  5. 可预见性: G1可选取部分区域进行回收, 可以缩小回收范围, 减少全局停顿

G1收集器的运作大致可划分为一下步骤:

直通BAT必考题系列:7种JVM垃圾收集器特点,优劣势、及使用场景

G1收集器的阶段分以下几个步骤:

1、初始标记(它标记了从GC Root开始直接可达的对象)

2、并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)

3、最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)

4、筛选回收(首先对各个区(Region)的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分 ''区(Region))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值