垃圾回收

年轻代(1/3)和老年代(2/3)

  • 年轻代和老年代是堆的结构,垃圾回收也主要回收堆
  • 年轻代 对象创建之后很快被回收
  • 老年代 对象长期存在

触发条件

  • 新生代里的对象太多,空间满了,就会触发垃圾回收,把没人引用的对象回收

回收条件

  • 用可达性分析法
    • 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
  • 可以作为GC Root的对象
    • 静态变量(这里的变量也指引用)
    • 局部变量(这里的变量也指引用)
  • 总结
    • 只要你的对象被方法的局部变量和类的静态变量引用了,对象就不会被回收
  • 案例
    • 下述代码中,如果垃圾回收,会回收ReplicaFetcher对象吗?为什么?
      • 不会的,因为ReplicaFetcher对象被ReplicaManager对象中的实例变量引用了,然后ReplicaManager对象被Kafka类的静态变量给引用了
      • 重点:经过可达性算法计算,主要一个对象的GC ROOT是静态变量或者局部变量,它就不会被回收

引用类型

  • 强引用 必须用的,不会被回收
  • 软引用 可有可无,内存实在不够,就回收它
  • 弱引用
  • 虚引用

finalize()方法(了解)

  • 属于object方法
  • 要被回收了,会调用finalize()方法,看看是否把这个对象给了某个GC ROOT变量,如果有GC ROOT重新引用了自己,就不会被回收
  • 总结
    • finalize可以监听一个对象被回收,但是不能保证调用了finalize的对象一定会被回收,同时一个对象在第二次标记回收时是不会触发finalize的!如果想绝对监听一个对象是否被回收,只有在JVM里面添加参数-XX:+PrintGCDetails分析GC日志

垃圾回收算法

  • 标记清除算法
  • 复制算法
  • 标记整理算法
  • 分代收集算法

标记清除算法(新生代用)

  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
  • 优缺点
    • 容易造成内存碎片
    • 效率慢,标记和清除两个过程的效率都不高

复制算法(新生代用)

  • 现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。 Survivor from 和Survivor to ,内存比例 8:1:1。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1, 也就是每次新生代中可用内存空间为整个新生代容量的 90% (80%+10%),只有 10% 的内存会被“浪费”。当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
  • 过程
    1 把Eden区中的存活对象都标记出来,然后全部转移到Survivor1去,接着一次性清空掉Eden中的垃圾对象
    2 当Eden再次塞满的时候,就又要触发Minor GC了,此时已然是垃圾回收线程运行垃圾回收器中的算法逻辑,也就是采用复制算法逻辑,去标记出来Eden和Survivor1中的存活对象
    3 然后一次性把存活对象转移到Survivor2中去,接着把Eden和Survivor1中的垃圾对象都回收掉
    4 循环以上过程
  • 优点
    • 只有10%的内存空间是被闲置的,90%的内存都被使用上了

新生代对象什么时候进入老年代?

  • 经过15次垃圾回收,该对象还没有被回收进入老年代;默认是15次,可通过-XX:MaxTenuringThreshold参数设置
  • 动态年龄判断
    • 年龄1 + 年龄2 + ... + 年龄n 的多个对象大小总和超过survivor区的50%,此时就会把年龄大于等于n的对象都放入老年代
  • 大对象直接进入老年代
    • 有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB
    • 他的意思就是,如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去。压根儿不会经过新生代

Minor GC老年代分配担保原则

  • Minor GC时,新生代存活的对象,survivor区放不下了,这些对象直接放入老年代
  • 在任何一次Minor GC之前,jvm都会检查老年代的可用空间,是否大于新生代对象的总大小
    • 如果老年代可用空间大于新生代对象总大小,并且Minor GC存活的对象,大于survivor区大小,minor gc存活的对象直接进入老年代
    • 如果发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个-XX:-HandlePromotionFailure的参数是否设置了,如果有这个参数,那么就会继续尝试进行下一步判断。下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小
      • 如果大于,就进行Minor GC
      • 如果参数没设置,或者老年代可用空间小于之前每一次Minor GC后进入老年代的对象的平均大小,此时就会直接触发一次Full GC,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC
  • Minor GC可能发生三种情况
    • 存活对象小于survivor区,直接进入survivor区
    • 存活对象大于survivor区,小于老年代可用空间,进入老年代
    • 存活对象大于老年代,会发生OOM内存溢出(这种情况发生之前,一定判断了是否有-XX:-HandlePromotionFailure参数,可能发生了FULL GC

老年代回收算法(标记整理算法)

  • 标记-整理算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

FULL GC出现时机

  • Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC,这里有两种情况
    • 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开
    • 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC
  • Minor GC之后,发现剩余对象太多放入老年代都放不下了

FULL GC比新生代垃圾回收慢10倍,jvm优化就是调整参数,不让FULL GC频繁发生



本文参考救火队长jvm专栏

posted @ 2020-09-07 11:26  队长别开枪 是我  阅读(287)  评论(0编辑  收藏  举报