还记得运行时数据区的区域划分吗?再来回顾一下。
我们现在知道了方法区和堆是所有线程共享的区域,并且它们的生命周期跟jvm相同。存储结构我们现在懂了,那么不禁会想一个问题:万一数据存满了怎么办?一些垃圾数据怎么清理的?
这就是通常我们所了解到的垃圾回收了。因为大部分对象都是存储在堆中的,所以我们通常了解的垃圾回收也都是指的堆内存。
我们现在可以将方法区和堆单独拎出来,然后站在设计者的角度来分析堆内存应该是个什么样子的。于是乎,就有以下这幅图。
设计1
首先,设计之前一定是这幅图的样子。
设计2
于是我就应该要往堆里存放数据了,假设现在堆里存放了很多数据。
弊端:内存不够放了会进行垃圾回收,需要扫描所有数据,把扫描结果为垃圾的数据清理掉。如果堆中数据特别多,这样全局扫描耗费时间太长,性能很差。
设计3
根据对象的年龄来划分,年龄划分的依据就是:每进行一次垃圾回收岁数就加1。
将堆划分为新生代和老年代,岁数小的放新生代,岁数大的放老年代。
每次来一个新对象,它的岁数为0,所以一定是放在新生代。当岁数达到某个值(比如15岁)了,就将该对象从新生代转移至老年代。
如果一个新来的对象在新生代放不下,那么它就直接放入老年代。
弊端:每来一个新对象就放入新生代,如果空间不够用了就垃圾回收。回收完后,空间够用了继续存放对象。假设有种情况如下图:新生代一共可以放入9个单位的数据,垃圾回收完后还剩下3个单位空余,这时候又来了一个占用3个单位的数据,结果放不下,然后又进行垃圾回收。不对呀,空间明明够用,为啥还给我垃圾回收?我并不希望垃圾回收过于频繁,因为它会占用系统资源,并且加快新生代数据的老年化。空间不够用的原因是:空间不连续,我们内存空间的数据都是连续存储的。这里很明显,新生代的空余位是4 5 9。
设计4
上面分析了空间明明够但是不连续从而浪费了空间,也增加了垃圾回收频率。
现在将新生代再去划分一下,分为Eden区和Survivor区,并且按照9比1的比例来分配。
从业务角度分析,大部分对象从创建到使用完毕经历的周期都特别短。比如一个web请求,浏览器接收到数据后,期间产生的对象就已经没有意义了,因为他们的使命完成了。所以,绝大多数对象在新生代就被清理掉了。
来了新对象都放入Eden区,当Eden区满了后进行垃圾回收,90%对象都被清理掉了,剩余的10%对象转移至Survivor区,这样的好处是,Eden区连续了,不会因为之前少量存活对象而造成空间不连续。
弊端:随着对象的不断存入,Eden区满了,又需要垃圾回收。垃圾回收是针对整个新生代,所以Eden+survivor区都需要进行垃圾回收。这时候会发现,survivor区由于少量的存活对象存在,也会造成空间不连续,导致Eden区里存活的对象放不下。
设计5
这么一折腾发现,空间碎片的问题还是没能从根本上解决。
这里再将survivor区一分为二,分为s0和s1区,比例为1比1。因此,Eden:s0:s1就是:8:1:1。
随着对象的增加,Eden区满了,进行垃圾回收(回收Eden区,s0和s1为空无意义),将存活的对象放入s0区。(此时s1区还是空的)
对象继续增加,Eden区又满了,进行垃圾回收(回收Eden区和s0区,s1为空无意义),将存活的对象放入s1区。(此时,Eden区和s0区空了)
对象继续增加,Eden区又满了,进行垃圾回收(回收Eden区和s1区,s0为空无意义),将存活的对象放入s0区。
周而复始,始终能保证s0区或s1区某一块区域为空,空间不连续的问题得以解决。
思考1:假设将Eden区和s1区的存活对象放入s0区时,发现空间不够放不下怎么办?这种情况就需要找老年代借点空间了,也就是所谓的担保机制。
思考2:如何知道对象自己的年龄?对象的对象头Mark Word里会存储分代年龄。