堆的空间是用来存储我们new出来的对象的。当new的对象填充满堆区的话,就会导致内存爆掉,我们的程序就会OOM。jvm 的 GC是清理堆上的对象的。
首先我们应该判断这个对象应不应该被删除。那判断的标准是什么呢?有一个标准叫GCRoot。被
- 栈
- 本地方法栈
- 方法区(全局变量)
直接或者间接引用的对象,是不能被删除的。
就引出了一个以GCRoot为根的树结构。
思路1:标记需要被删除,在扫描一遍,再删除。这就是标记清理。
缺点:产生内存碎片。(导致虽然有内存,但申请不了大内存)
思路2:标记整理。在清除之后,后面的对象要补上来,后面的对象往前顶,减少内存碎片。
缺点:代价太大。所有的对象都要前移。
思路3:复制算法。将整个内存一分为二。在1区标记是否删除,等到快满了。往2区进行复制,需要删除就不复制,不需要删除的就复制过来,并且是紧凑的复制。这样既避免了内存碎片问题,整个的开销也不大。
缺点:需要两倍的内存。
实际的GC。
对堆区进行了划分,一部分叫年轻代(Young区),另一部分叫老年区(Old区)。对年轻代又进行了划分,有3个区。eden区,survive0区, survive1区。Old区只有一块。new对象都会在eden区。当eden区快满的时候就会触发GC。这个GC只是Young这个区域的GC,所以这个GC又叫做YoungGC。YoungGC的过程采用的是上面的复制算法。不需要删除的依次复制到survive0区。eden比较大的原因是,对象的生命周期往往较短,所以在产生它们的地方区域会比较大,但是幸存下来的比较小。survive0、survive1、eden的比例大概是1:1:8,默认设置下。那为什么需要两块survive区呢?这两个S区是交替工作的。幸存到S0区之后,会将E和S1区全部删除。然后等下一次E区快满了之后,再将S0和E所有对象标记,然后全部复制到S1区。S0和S1交替使用,作为幸存下来的区域。E+S1复制到S0,E+S0复制到S1,E+S1复制到S0,如此往复。结合了对象的"朝生夕死"的特征进行设计的。每一次YoungGC之后,活下来的对象的年龄就会加1,直到对象满了6岁,不再往survive区中复制了,就直接到Old区中。这个原因是因为,如果一个对象在6次GC的清理中都没有被清理掉,那很有可能60次GC都不会清理掉,它可能会永远存在,或者存在很长一段时间。所以我们直接把它放到Old区中维护,这样就省得每次都在这边复制了。另外Old区除了存了年龄是大于等于6岁的这样的一些对象,同时它还存一些大的对象。大对象的原因是如果我们在Eden进行复制的时候,大对象的消耗是比较大的。大对象主要是什么呢?比如说有一个1000万大小的int数组,它就是一个大对象,这个大对象就会直接存到Old区,不进行Young区的存储。那在Old区同样存在它快满了的问题,快满了就会触发GC。OldGC一般会同时伴随着YoungGC,所以它又叫FullGC。FullGC会引起stop the world。stop the world就是说整个java程序直接暂停,然后全力地进行垃圾回收,(因为已经没有内存可用了)。垃圾回收主要采用的是标记清理的算法,或者是标记整理的算法。那我们就明确了,1和2思路(标记清理和标记整理)主要是用于FullGC的,也就是Old区的GC,复制算法主要是Young区的GC。
这里举几个比较有名的垃圾收集器。
年轻代的垃圾收集器可能是ParNew。
老年代比较有名的可能是CMS。
分别用了复制算法和标记清理算法,来进行垃圾回收。
最新版的JDK已经不建议用以前的垃圾收集器了,而采用了一种全新的G1垃圾收集器。它有一种全新的理念。这里就不展开讲了。