美文网首页
23. java虚拟机总结-和OOM相关的 (六)

23. java虚拟机总结-和OOM相关的 (六)

作者: 任振铭 | 来源:发表于2020-08-05 07:48 被阅读0次

    垃圾回收算法

    可达性分析法(根搜索算法,GC ROOTS)

    从 GC Roots 向下追溯、搜索,会产生一个叫作 Reference Chain 的链条。当一个对象不能和任何一个 GC Root 产生关系时,就会被无情的诛杀掉。
    如图所示,Obj5、Obj6、Obj7,由于不能和 GC Root 产生关联,发生 GC 时,就会被摧毁。


    GC ROOTS.png
    GC Roots 有哪些

    GC Roots 是一组必须活跃的引用。用通俗的话来说,就是程序接下来通过直接引用或者间接引用,能够访问到的潜在被使用的对象。
    GC Roots 包括:

        Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用。
        所有当前被加载的 Java 类。
        Java 类的引用类型静态变量。
        运行时常量池里的引用类型常量(String 或 Class 类型)。
        JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类。
        用于同步的监控对象,比如调用了对象的 wait() 方法。
        JNI handles,包括 global handles 和 local handles。
    

    这些 GC Roots 大体可以分为三大类,下面这种说法更加好记一些:

        1  活动线程相关的各种引用。(虚拟机栈(栈帧中的本地变量表)中引用的对象。)
        2  类的静态变量的引用。(方法区中类静态属性引用的对象。方法区中常量引用的对象。)
        3  JNI 引用。(本地方法栈中JNI(即一般说的Native方法)引用的对象。)
    
    gc roots包括.png

    有两个注意点:

    1. 我们这里说的是活跃的引用,而不是对象,对象是不能作为 GC Roots 的。
    2. GC 过程是找出所有活对象,并把其余空间认定为“无用”;而不是找出所有死掉的对象,并回收它们占用的空间。所以,哪怕 JVM 的堆非常的大,基于 tracing 的 GC 方式,回收速度也会非常快。

    引用计数法

    因为有循环依赖的硬伤,现在主流的 JVM,没有一个是采用引用计数法来实现 GC 的,所以我们大体了解一下就可以。引用计数法是在对象头里维护一个 counter 计数器,被引用一次数量 +1,引用失效记数 -1。计数器为 0 时,就被认为无效。你现在可以忘掉引用计数的方式了

    引用类型

    面试题:能够找到 Reference Chain 的对象,就一定会存活么?
    不一定,看引用类型

    强引用 Strong references

    即使程序会异常终止,这种对象也不会被回收的。比如下边,这种情况我们需要手动将其引用关系断开才会被回收

    Object obj = new Object()
    
    软引用 Soft references

    软引用用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,这种特性非常适合用在缓存技术上。比如网页缓存、图片缓存等。
    有一个相关的 JVM 参数。它的意思是:每 MB 堆空闲空间中 SoftReference 的存活时间。这个值的默认时间是1秒(1000)。

    -XX:SoftRefLRUPolicyMSPerMB=<N>
    

    这里要特别说明的是,网络上一些流传的优化方法,即把这个值设置成 0,其实是错误的,这样容易引发故障,感兴趣的话你可以自行搜索一下。

    这种比较偏门的优化手段,除非在你对其原理相当了解的情况下,才能设置一些比较特殊的值。比如 0 值,无限大等,这种值在 JVM 的设置中,最好不要发生。

    弱引用 Weak references

    当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。弱引用拥有更短的生命周期
    它的应用场景和软引用类似,可以在一些对内存更加敏感的系统里采用

    虚引用 Phantom References

    形同虚设的引用,在现实场景中用的不是很多。虚引用必须和引用队列(ReferenceQueue)联合使用。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

    Object  object = new Object();
    ReferenceQueue queue = new ReferenceQueue();
    // 虚引用,必须与一个引用队列关联
    PhantomReference pr = new PhantomReference(object, queue);
    

    虚引用主要用来跟踪对象被垃圾回收的活动。
    当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的引用队列中。

    程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

    下面的方法,就是一个用于监控 GC 发生的例子。

    private static void startMonitoring(ReferenceQueue<MyObject> referenceQueue, Reference<MyObject> ref) {
         ExecutorService ex = Executors.newSingleThreadExecutor();
         ex.execute(() -> {
             while (referenceQueue.poll()!=ref) {
                 //don't hang forever
                 if(finishFlag){
                     break;
                }
            }
             System.out.println("-- ref gc'ed --");
    
        });
         ex.shutdown();
    }
    

    基于虚引用,有一个更加优雅的实现方式,那就是 Java 9 以后新加入的 Cleaner,用来替代 Object 类的 finalizer 方法。

    典型 OOM 场景

    内存区域有哪些会发生 OOM 呢?我们可以从内存区域划分图上,看一下彩色部分。


    oom发生的内存区域.png

    除了程序计数器,其他区域都有OOM溢出的可能。但是最常见的还是发生在堆上。

    oom发生.png

    OOM 到底是什么引起的呢?有几个原因:

    内存的容量太小了,需要扩容,或者需要调整堆的空间。
    错误的引用方式,发生了内存泄漏。没有及时的切断与 GC Roots 的关系。比如线程池里的线程,在复用的情况下忘记清理 ThreadLocal 的内容。
    接口没有进行范围校验,外部传参超出范围。比如数据库查询时的每页条数等。
    对堆外内存无限制的使用。这种情况一旦发生更加严重,会造成操作系统内存耗尽。

    典型的内存泄漏场景,原因在于对象没有及时的释放自己的引用。比如一个局部变量,被外部的静态集合引用。


    oom场景之一.png

    你在平常写代码时,一定要注意这种情况,千万不要为了方便把对象到处引用。即使引用了,也要在合适时机进行手动清理。关于这部分的问题根源排查,我们将在实践课程中详细介绍。

    相关文章

      网友评论

          本文标题:23. java虚拟机总结-和OOM相关的 (六)

          本文链接:https://www.haomeiwen.com/subject/gigrhktx.html