垃圾回收
年轻代(1/3)和老年代(2/3)
- 年轻代和老年代是堆的结构,垃圾回收也主要回收堆
- 年轻代 对象创建之后很快被回收
- 老年代 对象长期存在
触发条件
- 新生代里的对象太多,空间满了,就会触发垃圾回收,把没人引用的对象回收
回收条件
- 用可达性分析法
- 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
- 可以作为GC Root的对象
- 静态变量(这里的变量也指引用)
- 局部变量(这里的变量也指引用)
- 总结
- 只要你的对象被方法的局部变量和类的静态变量引用了,对象就不会被回收
- 案例
- 下述代码中,如果垃圾回收,会回收ReplicaFetcher对象吗?为什么?
- 不会的,因为ReplicaFetcher对象被ReplicaManager对象中的实例变量引用了,然后ReplicaManager对象被Kafka类的静态变量给引用了
- 重点:经过可达性算法计算,主要一个对象的GC ROOT是静态变量或者局部变量,它就不会被回收
- 下述代码中,如果垃圾回收,会回收ReplicaFetcher对象吗?为什么?
引用类型
- 强引用 必须用的,不会被回收
- 软引用 可有可无,内存实在不够,就回收它
- 弱引用
- 虚引用
finalize()方法(了解)
- 属于object方法
- 要被回收了,会调用finalize()方法,看看是否把这个对象给了某个GC ROOT变量,如果有GC ROOT重新引用了自己,就不会被回收
- 总结
- finalize可以监听一个对象被回收,但是不能保证调用了finalize的对象一定会被回收,同时一个对象在第二次标记回收时是不会触发finalize的!如果想绝对监听一个对象是否被回收,只有在JVM里面添加参数-XX:+PrintGCDetails分析GC日志
- 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频繁发生