当前位置:   article > 正文

JVM学习-垃圾回收(一)

JVM学习-垃圾回收(一)
什么是垃圾
  • 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
  • 如果不及时对内存的垃圾进行清理,垃圾对象所占用的内存空间会一直保留到应用程序结束,被保留的空间无法被其它对象所用,甚至可能导致内存溢出
为什么需要GC
  • 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样
  • 除了释放没用的对象,垃圾回收也可以清理内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端,以便JVM整理出的内存分配给新的对象
  • 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化
早期垃圾回收
  • 早期C/C++时代,垃圾回收基本上是手工进行的,开发人员使用new申请内存,使用delete关键字进行内存释放
  • 这种方式可以灵活控制内存释放的时间,但给开发人员带来频繁申请和释放内存的管理负担,倘若一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法清理,随着系统运行时间不断增长,垃圾对象所耗内存持续上升,直到出现内存溢出并造成应用程序崩溃
Java回收机制
  • 优势
    • 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
    • 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
  • 担忧
    • 对于Java开发人员,自动内存管理就像一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重会弱化Java开发人员在程序出现内存溢出定位问题和解决问题的能力。
    • 了解JVM的自动内存分配和内存回收原理显得非常重要,只有在真正了解JVM是如何管理内存后,才能够遇到OutOfMemoryError时,快速地根据错误异常日志定位问题和解决问题
    • 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们必须对“自动化”技术实施必要的监控和调节
  • 垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区进行回收
    • Java堆是垃圾回收器的工作重点
  • 从次数上讲
    • 频繁收集Young区
    • 较少收集Old区
    • 基本不动Perm区(元空间)
垃圾回收算法
标记阶段
  • 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象,只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,这个过程称为垃圾标记阶段
  • 在JVM中如何标记一个死亡对象呢?简单的说,当一个对象已经不再被任何存活对象继续引用时,就可以审判已经死亡
  • 判断对象存活有两种方式
    • 引用计数算法(Reference Counting)
      • 较简单,对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况
      • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收
      • 优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟性
      • 缺点
        • 它需要单独的字段存储计数器,这样增加了存储空间的开销
        • 每次赋值都需要更新计数器,伴随着加法和减法操作,增加时间开销
        • 引用计数器有一个严重的问题,即无法处理循环引用的情况,这是一条致命缺陷,导致在Java的垃圾回收器中没有使用此算法
          循环引用
    • python使用了引用计数算法,如何解决循环引用
      • 手动解除:在合适时机,解除引用关系
      • 使用弱引用weakref,weakref是python提供的标准库,旨在解决循环引用
//Java不是使用引用计数算法
public class ReferenceCountGC {
    private byte[] bigSize = new byte[5 * 1024 * 1024];
    Object reference = null;

    public static void main(String[] args) {
        ReferenceCountGC obj1 = new ReferenceCountGC();
        ReferenceCountGC obj2 = new ReferenceCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;
        //显示执行垃圾回收
        System.gc();
    }
}
//执行时添加参数-XX:+PrintGCDetails,不显示执行System.gc(),结果如下
Heap
 PSYoungGen      total 152576K, used 20726K [0x0000000716300000, 0x0000000720d00000, 0x00000007c0000000)
  eden space 131072K, 15% used [0x0000000716300000,0x000000071773d8d8,0x000000071e300000)
  from space 21504K, 0% used [0x000000071f800000,0x000000071f800000,0x0000000720d00000)
  to   space 21504K, 0% used [0x000000071e300000,0x000000071e300000,0x000000071f800000)
 ParOldGen       total 348160K, used 0K [0x00000005c2800000, 0x00000005d7c00000, 0x0000000716300000)
  object space 348160K, 0% used [0x00000005c2800000,0x00000005c2800000,0x00000005d7c00000)
 Metaspace       used 3425K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 380K, capacity 388K, committed 512K, reserved 1048576K
//执行时添加参数-XX:+PrintGCDetails,显示执行System.gc(),结果如下,
[GC (System.gc()) [PSYoungGen: 18104K->808K(152576K)] 18104K->816K(500736K), 0.0021479 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (System.gc()) [PSYoungGen: 808K->0K(152576K)] [ParOldGen: 8K->645K(348160K)] 816K->645K(500736K), [Metaspace: 3476K->3476K(1056768K)], 0.0034007 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 152576K, used 3932K [0x0000000716300000, 0x0000000720d00000, 0x00000007c0000000)
  eden space 131072K, 3% used [0x0000000716300000,0x00000007166d7230,0x000000071e300000)
  from space 21504K, 0% used [0x000000071e300000,0x000000071e300000,0x000000071f800000)
  to   space 21504K, 0% used [0x000000071f800000,0x000000071f800000,0x0000000720d00000)
 ParOldGen       total 348160K, used 645K [0x00000005c2800000, 0x00000005d7c00000, 0x0000000716300000)
  object space 348160K, 0% used [0x00000005c2800000,0x00000005c28a1460,0x00000005d7c00000)
 Metaspace       used 3491K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 可达性分析算法(根探索算法、追踪性垃圾收集)
    • 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效,更重要的是该算法有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
    • 相较于引用计数算法,可达性分析算法是Java、C#选择的,这种类型的垃圾收集通常叫做追踪性垃圾收集(Tracing Garbage Collection)
    • "GC Roots"根集合是一组必须活跃的引用
    • 基本思路
    • 可达性分析算法是以根对象集合为起始点,按照从上至下的方式搜索被根对象集体所连接的目标对象是否可达
    • 使用可达性分析算法后,内存中的存活对象都地被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    • 如果目标对象没有任何引用链相连,则是不可达的,意味着该对象已经死亡,可以标记为垃圾对象
    • 在可达性分析算法中,只能被根对象集合直接或间接连接的对象才是存活对象
      在这里插入图片描述
GC Roots
  • 虚拟机栈中引用的对象
    • 各个线程被调用的方法中使用到的参数、局部变量等
  • 本地方法内JNI引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
    • 字符串常量池(string table)里的引用
  • 所有被同步锁sychnorized持有的对象
  • Java虚拟机内部的引用
    • 基本数据类型对应的class对象,一些常驻的异常对象(NullPointerException,OutOfMemoryError),系统类加载器
  • 反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调、本地代码缓存等
    小技巧由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但自己又不存放在堆内存里面,它就是一个Root
    **注:**如果要用可达性分析算法来羊汤内存是否回收,那么分析工作必须在一个能保障一致性的快照中进行,这点不满足的话,分析结果的准确性就无法保证,这也是导致GC进行时必须“Stop The World”的一个重要原因,即使用号称不会发生停顿的CMS收集器,枚举根节点时也是必须停顿的
对象finalizaion机制
  • Java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
  • finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件,套接字和数据库连接等
  • 永远不要主动调用finalize()方法
    • 在finalize()时可能会导致对象复活
    • finalize()方法的执行时间没有保障,完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会
    • 一个糟糕的finalize()地严重影响GC性能
  • 从功能上来说,finalize()方法与C++的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于C++的析构函数
  • 由于finalize()方法存在,虚拟机中的对象一般有三种可能状态
    • 可触及的:从根节点开始,可以到达这个对象
    • 可复活的:对象的所有引用都被释放,但对象有可能在finalize()中复活
    • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,因为finalize()只会被调用一次
      注:只有对象不可触及时才可以被回收
  • 判断一个对象objA是否可以回收,至少要经历两次标记过程
  • 如果对象objA到GC Roots没有引用链,则进行一次标记
  • 进行筛选,判断此对象是否有必要执行finalize()方法
    ① 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及
    ② 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行
    ③finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记,如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA被移出“即将回收”集合,之后,对象会再次出现没有引用存在情况,此时finalize()方法不会被再次调用,对象会直接变成不可触及的状态,一个对象的finalize方法只会被调用一次
/**
 * 测试object中finalize()方法,即对象finalization
 */
public class CanReliveObj {
    public static CanReliveObj obj;
    //此方法只调用1次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("调用当前类重写finalize方法");
        obj = this;    //当前对象在finalize()方法中与引用链上的一个对象建立联系
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new CanReliveObj();
        //对象第一次成功拯救自己
        obj = null;
        System.out.println("第1次gc");
        System.gc();
        Thread.sleep(2000);
        if (obj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }
        System.out.println("第2次gc");
        obj = null;
        System.gc();
    }
}
//执行结果1次gc
调用当前类重写finalize方法
obj is still alive
第2次gc
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/625793
推荐阅读
相关标签
  

闽ICP备14008679号