分代假说
1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数
解释1:
解释2:
时间开销 ——> 支取扫描1%,比扫描99%快得多;
内存空间有效利用 ——> 1%移动时不会直接移动,而是复制一份,将复制移动过去,成功后,删除原有的。
新生代 —— 存放朝生夕死的对象
老年代 —— 存放新生代回收后还存活的对象,不容易死的对象
解释3:
当年龄达到13时,就进入老年代
跨代引用
分代收集有一个问题:跨代引用
比如说新生代中有一个对象A引用老年代的一个对象 B,只要老年代的对象不回收,新生代的这个对象也不会被回收。每次新生代回收时还是要去扫(判断A是否需要回收,此时也需要去判断A引用的B是否被回收),也不能直接将A放入老年代,因为可能在A的年龄为12时,对象B就被回收了。
解决跨代引用的问题
跨代引用只占极少数,就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set)。这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。
一旦出现跨代引用,就会出现在老年代的跨代引用区域。当发生新生代收集时,只有包含了跨代引用的小块内存里的对象会进行扫描。
虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
垃圾回收算法
一、标记-清除算法
标记:哪些对象可以存活或者哪些对象可以回收
它的主要缺点有两个:
-
第一个是执行效率不稳定。
如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低; -
第二个是内存空间的碎片化问题。
标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
基本不使用
二、标记-复制算法
根据IBM研究,新生代中的对象有98%熬不过第一轮收集(普通场景下,无法保证每次都是这样),因此不需要按照1:1的比例来划分新生代的内存空间,而是使用了8:1:1分配内存空间。
每次进行收集时,将前8:1空间的存活对象一次性复制到另外的1比例,然后清理掉前8:1空间的朝生夕死的对象。
标记-复制算法虽然弥补了标记-清除算法的缺点,但标记-复制算法还是有自己的缺点:
首先就是在对象存活率较高时要进行较多的复制操作,效率将会降低;
还有一种情况,就是在十分之九的的对象里超过十分之一存活,那么复制到右边区域时空间大小就不够了。
为了解决这些,出现了标记-整理算法。
三、标记-整理算法
标记过程仍与标记-清除算法相同,但后续让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
弊端:
-
像在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序(解释4)才能进行 ——> 宕机 stop the world
但不移动的话,碎片化问题严重,得不到解决也会导致性能、存储量降低。
解释4:
如果不暂停的话,刚标记的时候只占据一块,标记完以后整理时可能就变成了占据了两块。
于是混合式出现了,让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。