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

三个问题:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 怎么回收内存?

3.2 哪些内存需要回收?

​ 一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件的分支所需要的内存也可能不一样。只有处于运行期间,我们才能知道程序需要创建哪些对象。

3.2.1 引用计数算法

​ 在对象中创建一个引用计数器,每个有一个地方引用它的时候,计数器加一。当引用失效时,计数器减一。任何时刻计数器为零的对象就是不可能被使用的。

优点:高效

缺点:情况多种多样,比如相互循环问题

Obj a = new Obj();
Obj b = new Obj();
a.instance = b;
b.instance = a;

3.2.2 可达性分析算法

​ 通过一系列的“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链连接,那么证明此对象时不可能被使用。

怎么判断是GC Roots?
  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,比如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NPE,OOM)等,还有系统类加载器。
  6. 所用被同步锁持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

​ 在分代收集和局部回收时,有可能这些区域被堆中的其它区域所引用。这时需要将这些关联区域的对象也一起加入到GC Roots中。

3.2.3 再谈引用

  1. 强引用

    /**
     * 强引用
     *      如果一个对象具有强引用,那就类似于必不可少的物品,不会被垃圾回收器回收。
     *      当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
     *
     *      如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,
     *      这样一来的话,JVM在合适的时间就会回收该对象。
     * @author ZhengChaoHui
     * @Date 2020/6/30 8:47
     */
    public class StrongRef {
        public static void main(String[] args) {
            StrongRef.m1();
        }
        public static void m1(){
            Object o = new Object();
            Object[] objects = new Object[Integer.MAX_VALUE];
        }
    }
    
    执行报错:
        Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    	at references.StrongRef.m1(StrongRef.java:19)
    	at references.StrongRef.main(StrongRef.java:15)
            
    
  2. 软引用

    /**
     * 软引用
     *      软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。
     *      对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
     *      因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
     *
     *      软引用可以和一个引用队列(ReferenceQueue)联合使用,
     *      如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
     * @author ZhengChaoHui
     * @Date 2020/6/29 19:42
     */
    @SuppressWarnings("all")
    public class SoftRef {
        public static void main(String[] args) {
            Obj obj = new Obj();
            SoftReference<Obj> sr = new SoftReference<Obj>(obj);
            obj = null;
            System.out.println(sr.get());
        }
    }
    class Obj{
        int[] obj;
        public Obj(){
            obj = new int[1000];
        }
    }
    
  3. 弱引用

    /**
     * 弱引用
     *      弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
     *      在java中,用java.lang.ref.WeakReference类来表示。
     *      弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
     *      在垃圾回收器线程扫描它所管辖的内存区域的过程中,
     *      一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
     *      不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
     *      软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
     * @author ZhengChaoHui
     * @Date 2020/6/29 19:46
     */
    public class WeakRef {
        public static void main(String[] args) {
            WeakReference<String> wr = new WeakReference<String>(new String("Hello"));
            //输出Hello
            System.out.println(wr.get());
            //通知jvm回收(这句是无法确保此时JVM一定会进行垃圾回收的)
            System.gc();
            //输出null
            System.out.println(wr.get());
        }
    }
    
  4. 虚引用

    /**
     * 虚引用
     *      虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。
     *      在java中用java.lang.ref.PhantomReference类表示。
     *      如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
     *      虚引用主要用来跟踪对象被垃圾回收的活动。
     *      *虚引用必须和引用队列关联使用*
     * @author ZhengChaoHui
     * @Date 2020/6/30 8:56
     */
    public class PhantomRef {
        public static void main(String[] args) {
            ReferenceQueue<String> queue = new ReferenceQueue<>();
            System.out.println(queue.poll());
            PhantomReference<String> ptr = new PhantomReference<>(new String("hello"), queue);
            System.out.println(queue.poll());
            System.out.println(ptr.get());
        }
    }
    
    输出:null
         null
    

参考文章:

Java基础篇 - 强引用、弱引用、软引用和虚引用

3.2.4 finalize方法

​ 要想宣告一个对象真正死亡,至少要经历两次标记过程:如果对象在经过可达性分析后发现没有与GC Roots相连接的引用链,那么就会被第一次标记,随后进行一次筛选,筛选条件是此对象是否有必要进行finalize方法。假如对象没有覆盖finalize方法,或者finalize方法已经被调用过,那么虚拟机都将其视为‘没有必要执行‘。

​ 如果在finalize方法里面对象又重新引用了引用链上的任意对象,那么对象可以跳脱。但是再下一次收集的时候,就不会再判断是否执行finalize方法了,直接宣告死亡(如果没有引用到引用链)。

3.2.5 回收方法区

​ 方法区的垃圾收集主要回收两部分:废弃的常量不再使用的类型

​ 判断废弃的常量只需要判断是否有其它对方引用它。

​ 判断一个类型不再使用:

  1. 该类的所有实例都已经被回收。
  2. 加载该类的加载器已经被回收。
  3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.3 垃圾收集算法(重点)

3.3.1 分代收集理论

弱分代假说

绝大多数对象都是朝生夕灭的。

强分代假说

熬过越多次垃圾收集过程的对象就越难消亡。

​ 根据这两个假说奠定了多款垃圾收集器的一致设计原则:需要分区域、分代收集。

跨代引用假说

跨代引用相对于同代引用来说仅占极少数

部分收集 Partial GC

  1. Minor GC/ Young GC:目标只是新生代的垃圾收集
  2. Major GC/Old GC:目标只是老年代的垃圾收集(目前只有CMS收集器拥有)
  3. Mixed GC:目标是整个新生代和部分老年区的垃圾收集

整堆收集 Full GC

​ 收集整个Java堆和方法区的垃圾收集

3.3.2 标记-清除算法(Mark-Sweep)

在这里插入图片描述

先标记需要清除的,然后删除。或者标记不需要清除的,删除没有标记的。

优点:简单,不需要移动对象。

缺点:产生大量内存碎片,导致内存分配复杂。而且再需要大量回收对象的时候,效率低下。

3.3.3 标记复制算法(Mark-Copy)

在这里插入图片描述

优点:在需要大量清除可回收对象的时候,只需要将少量的对象移动到另一个半区即可。

缺点:浪费一半的空间。

折中:Appel式回收,将新生代分为Eden和两个Survivor区(一个from,一个to),比例是8:1:1。

一文看懂JVM内存布局及GC原理

3.3.4 标记整理算法(Mark-Compact)

在这里插入图片描述

优点:便于内存分配。

缺点:如果存活对象过多,STW会很长,因为移动对象需要暂停用户进程。

折中:平时多数时间使用标记-清除算法,直到内存的碎片化程度已经大到影响对象分配时,再采用一个标记-整理算法收集一次。

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

​ 迄今为止,所有的收集器在这一步骤都要STW。

​ OopMap里面存着引用的位置。

3.4.2 安全点

​ 在“特定位置”记录OopMap信息,这些位置就是安全点。

​ 安全点的选取一般以“是否具有让程序长时间运行”的标准选取的。一般在方法调用、循环跳转、异常跳转等地方设置。

抢先式中断:GC时,所有线程中断,然后让没跑到安全点的线程继续跑到安全点。(几乎不使用)

主动式中断:设置一个标志位,让线程轮询访问,一旦发现中断标记为真就在最近的安全点上中断挂起。

3.4.3 安全区域

​ 针对休眠或者阻塞的线程不能主动中断,JVM引入安全区域来解决。

安全区域是指能够在某一段代码里面,引用关系不会发生变化。(其实就是安全点的扩展拉伸)

​ 线程进入到安全区域的时候,会标记自己已经进入安全区域,等到出去的时候,它要检查根节点枚举是否完成。如果完成,继续运行,否则,等待。GC时可以不用管在安全区域的线程。

3.4.4 记忆集和卡表

​ 记忆集:

一种用于记录从非收集区区域指向收集区域的指针集合的数据结构

​ 卡表是记忆集的一种实现。

在这里插入图片描述

​ 卡表标记为1,说明对应的卡页存在跨代指针。

3.4.5 写屏障

​ 写屏障包括写前屏障和写后屏障,和AOP中的Around相似。

​ 在写后屏障中更新卡表状态。

​ 卡表容易出现“伪共享”,可以不采用写屏障,而是先检查卡表状态,只有没有标记过的卡表才去标记其为脏。

3.4.5 并发的可达性分析

在这里插入图片描述

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
增量更新:

​ 增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了

原始快照:

​ 原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

3.5 经典垃圾收集器

在这里插入图片描述

3.5.1 Serial收集器

在这里插入图片描述

3.5.2 ParNew收集器

在这里插入图片描述

3.5.3 Parallel Scavenge收集器

吞 吐 量 = 运 行 用 户 代 码 时 间 运 行 用 户 代 码 时 间 + 运 行 垃 圾 收 集 时 间 吞吐量 = \frac{运行用户代码时间} {运行用户代码时间 + 运行垃圾收集时间} =+

3.5.4 Serial Old收集器

在这里插入图片描述

3.5.5 Parallel Old收集器

在这里插入图片描述

3.5.6 CMS收集器

在这里插入图片描述

  1. Inital Mark 初始标记: 主要是标记GC Root开始的下级(注:仅下一级)对象,这个过程会STW,但是跟GC Root直接关联的下级对象不会很多,因此这个过程其实很快。
  2. Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有STW。
  3. Remark 再标志:为啥还要再标记一次?因为第2步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW时间不会很长)
  4. Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep-标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact标记-整理”法,把垃圾清理后,剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮GC,才会被清理掉。

3.5.7 Garbage First收集器

​ 它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
​ CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。

​ G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过 程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在 这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的

在这里插入图片描述

​ 与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”

参考书籍/文章:

  1. 一文看懂 JVM 内存布局及 GC 原理
  2. 深入理解Java虚拟机
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值