阅读本文前,你需要知道:
- 对新生代和老年代是什么有初步的了解,了解新生代空间中的存储方式(Eden,Survivor)
- 对jvm的垃圾回收机制有了解 : 传送门
- 对垃圾回收的机制和三种方式有了解:垃圾回收方式
- 知道Minor GC 和 Full GC的基本意思
正文
我们都知道,jvm中的堆空间用来存储对象的实例,而堆空间又被划分为新生代和老年代,当我们创建一个新对象的时候,优先将此对象在Eden上分配空间,如果Eden上的空间不足,就要先进行GC,尝试腾出空间来分配空间,这里进行的GC是Minor GC,也就是仅仅对新生代进行垃圾回收。在jvm垃圾收集机制详解(中) 中我们提到了,如果在回收的时候,发现新生代中的Survivor空间不足以装得下需要进行复制方法回收保留下来的对象,那么就要调用老年代分配担保机制,这里我们详细第说一下这个分配担保机制。
如图:
我们在新生代中本来有四个对象,现在要新建第五个对象,发现新生代中的空间不够用了,要触发垃圾回收,然后发现Survior中的空间也不足以保留下所有的不用被回收的对象(从图上就能看出来不够用),那么就要触发老年代分配担保机制,将四个对象全部转移到老年代中然后在新生代中给第五个对象腾出地方来,如图:
这一步就是提前转移到老年代。
你可能要问了,如果四个对象都转移到老年代空间还是不够用怎么办呢?
事实上,每个对象在新建的时候对象头中就带着它的对象信息了,这个信息中是有对象的大小信息的,所以根本不会出现四个对象都转移完之后才发现空间还是不够的情况,而是早早地读取出来对象的大小信息,如果这个对象的太大,就会直接进入到老年代,这个直接进入老年代的大小值可以由我们用户去自定义地设置。
这样大的对象一般是像很大的数组,或者是很长的字符串那样的东西等等,这种大对象应该尽量避免,更加需要避免的就是创建存活时间非常短的大对象了,那样会导致在内存还剩余不少空间的时候就提前或者是频繁地触发垃圾回收来安置大对象。
对于长期存活的对象,如果我们还把对象留在新生代中,显然不是一种明智的做法,这意味着每次对新生代进行复制算法的垃圾回收时,都要复制一遍这个长期存活的对象,这显然是一种浪费,因此,jvm把长期存活的对象转移到老年代中。那么,这个“长期”的判定标准是什么呢?在新生代中,对象每经历过一次Minor GC存活下来之后,年龄都会增加1岁,在一开始,对象先被创建在Eden区域中,在经历了第一次Minor GC存活下来之后,对象的年龄被设置为1,并且如果对象不太大,能够被Survivor容纳进去的话,这个对象将被从Eden区域移动到Survivor中存储。当对象的年龄增长到15岁的时候(默认,可设置),jvm认为对象已经足够“老”了,此时,这个对象会被转移到老年代中。
但是,可不仅仅是对象的年龄达到了15岁才会被转移到老年代,如果在新生代中,某个年龄的对象大小总和达到了Survivor的一半以上,那么所有大于等于这个年龄的对象都可以直接进入老年代,无需达到15岁。
前面提到了,在发生Minor GC的时候,有老年代分配担保机制,这里再说两句,如果老年代中剩余的空间不足以做分配担保怎么办呢?
在进行Minor GC之前,jvm会先检查老年代中最大可用的连续空间是否大于新生代中的所有对象的总空间,如果大于,那么很显然这次Minor GC可以被老年代担保,是安全的;如果不大于,那么虚拟机会查看设置值HandlePromotionFailure是否开启,这个设置是用来设置是否允许担保失败的。如果允许担保失败,那么就会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC;如果小于,或者HandlePromotionFailure设置关闭,那么就进行一次Full GC,这里的进行Full GC是为了让老年代腾出更多空间来进行担保。
注意,这里取平均值只是保证了大部分情况下不会担保失败,如果某次Minor GC后的对象大小突然激增,依然会出现担保失败,此时只能在失败后重新进行一次Full GC。
虽然允许担保失败的时候进行的操作比较多,但是还是开启担保失败比较好,因为这样可以有效地避免频繁地进行Full GC。