1、前言
jvm的内存模型分为:堆、本地方法栈、虚拟机栈,方法区、程序计数器。
其中,gc(垃圾回收)主要集中在堆,堆又划分为2个区域:新生代、老年代。
当新生代空间不足时,会触发 minor gc,回收新生代的垃圾。
当老年代空间不足时,会触发 full gc,回收老年代的垃圾,同时,full gc 会触发 minor gc。
2、新生代
新生代分为 1 个 Eden 区和 2 个 survivor区(From,to),当创建一个对象时,一般都分配在 Eden 区(除非对象很大,超过了jvm参数 -XX:PretenureSizeThreshold,则直接分配在老年代), Eden 区会越来越大,当 Eden 区空间不足时,会STW,进行 minor gc(gc算法是标记复制算法),标记存活的对象,并将其复制到 From 区,然后将 Eden 区清空。由于大部分对象的生命周期很短,朝生夕死,所以 Eden 区存活的对象很少,因此 minor gc 的工作量较小,执行时间短,STW的影响非常小。
Eden 区清空后,继续分配对象,直到 Eden 区再次空间不足,此时,触发 minor gc,将 Eden 、From 区中存活的对象复制到 To 区,并将 Eden、From 区清空。注意,此时要将 From 和 To 的身份互换,也就意味着 To 区变成了 From 区。
Eden 区又空了,继续分配对象,直到空间不足,再次 minor gc,将 Eden、From区存活的对象复制到 To 区,再次将 Eden、From 区清空,From 和 To 身份互换。
如此循环下去。
几个关键点:
1、如果 Eden 太小
会导致频繁的 minor gc,虽然单次 minor gc 的耗时短,但是频率很高的话,也会对服务性能造成影响。
2、如果 Eden 太大
会挤压 From 和 To 区的空间,导致minor gc 时,To 区容易没有足够的空间存储活下来的对象,就会直接存到老年代。
3、From 和 To 的大小一样。
4、三个区的大小比例默认:Eden :From:to = 8:1:1
但是现在JDK8中,默认是开启了自动调整,根据程序运行情况,动态调整三个区的空间比例,如果想要自己设置固定的比例,一定要先关闭自动调整,-XX:-UseAdaptiveSizePolicy,再设置比例:-XX:SurvivorRatio=8, 8 表示 Eden 是 From 和 To 的8倍。
5、新生代的大小,可由jvm参数设置:-Xmn1024m,表示1024M大小。
6、有些对象的生命周期较长,所以在多次 minor gc 后,新生代存活的对象会越来越多,那怎么办?
3、老年代
老年代就是用来存储生命周期长、体积大的对象。
哪些情况下,对象会进入老年代呢?
1、每一个对象都是有年龄的,当对象经历过一次 minor gc 并活下来后,年龄就 + 1,当年龄达到阈值 -XX:MaxTenuringThreshold 时(默认15),就会挪到老年代。
2、minor gc后,To 区容不下存活的对象,此时,这些对象会进入老年代。
3、新生代的标记-复制算法有个缺点,对于大对象,算法效率会降低,为避免这一缺点,直接将大对象存到老年代,大对象的判断标准是,当大于 -XX:PretenureSizeThreshold 参数值时,为大对象。
4、To 区存活的对象中,相同年龄的对象大于空间的一半时,大于等于此年龄的对象直接进入老年代。
一说老年代,就不得不提 “担保机制”。
在 minor gc 之前,如果老年代可用空间 > 新生代所有对象,则 minor gc 后,老年代一定够用,则直接 minor gc 就行,否则,就看 jvm 是否有 “担保机制”(-XX:HandlePromotionFailure 此参数控制,JDK8默认开启),如果没有,则进行 full gc,把老年代的空间腾出来,如果有 “担保机制”,则判断 “老年代可用空间” 是否大于 “每次minor gc进入老年代的对象的平均大小”,如果大于,说明大概率老年代够用,则执行 minor gc,否则,说明老年代大概率不够用,则进行full gc。
最后,如果 full gc 之后,老年代空间还不够用,就会报 OOM。