1、垃圾回收
1.1、部分收集(Partial GC):只针对部分区域进行垃圾收集
- 新生代收集(Minor GC/Young GC):只针对新生代的垃圾收集。具体点的是Eden区满时触发GC。Survivor满不会触发Minor GC;
- 老年代收集(Major GC/Old GC):只针对 老年代的垃圾收集(CMS);
注意,很多时候,Major GC 会和Full GC混淆使用,需要具体分辨是老年代的回收还是整堆回收; - 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集(G1)。
1.2、整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
2、垃圾回收算法
2.1、标记-清除算法
标记:遍历内存区域,对需要回收的对象打上标记。
清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
- 效率问题;遍历了两次内存空间(第一次标记,第二次清除);
- 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次触发GC。
2.2、标记-复制算法
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
优点:
- 相对于标记–清理算法解决了内存的碎片化问题;
- 效率更高(清理内存时,记住首尾地址,一次性抹掉)。
缺点:
- 内存利用率不高,每次只能使用一半内存。
研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8:1:1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。
首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。
但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。
老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。
2.3、标记-整理算法
因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。
标记:对需要回收的进行标记
整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存
2.4、分代收集理论
当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
- 强分代假说(Strong Generational Hypothesis) : 熬过越多次垃圾收集过程的对象就越难以消亡。
- 跨代引用假说( Intergenerational Reference Hypothesis) : 跨代引用相对于同代引用来说仅占极少数。
根据跨代引用假说我们就不应再为了少量的跨代引用去扫描整个老年代, 也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用, 只需在新生代上建立一个全局的数据结构( 该结构被称为 “记忆集” , Remembered Set) , 这个结构把老年代划分成若干小块, 标识出老年代的哪一块内存会
存在跨代引用。 此后当发生Minor GC时, 只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。 虽然这种方法需要在对象改变引用关系( 如将自己或者某个属性赋值) 时维护记录数据的正确性, 会增加一些运行时的开销, 但比起收集时扫描整个老年代来说仍然是划算的。
新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。
老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。
缺点:对象不是孤立的, 对象之间会存在跨代引用。 (有解决方案)