一、垃圾收集算法
1.1分代收集理论
分代收集理论建立在三个分代假说上:
- 弱分代假说:绝大多数的对象都是朝生夕灭
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数 ,因为存在相互引用的对象更倾向于同时生存或者同时消亡,比如说,某个新对象存在跨代引用,由于老年代难以消亡,该引用会使得新生代再收集的时候同样得以存活,进而年龄增长后进入老年代,这时跨代引用的关系随之被消除。
弱分代和强分代假说,共同奠定了多款垃圾收集器设计的一致的原则:收集器应该将JAVA堆分出不同的区域,然后将其回收的对象,按照垃圾回收过程中熬过的次数来划分年龄,并且分配到对应的区域中存储管理。比如说,堆里面的新生代,里面存储的对象基本都是朝生夕灭的对象,基本上new出来的对象都会放在新生代这里(如果创建的对象没有方法逃逸的话,亦或者不是特别大)。把它们几种放在一起,每次回收只关注如何保留少量的存活对象,而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间。所以新生代大多数的垃圾收集器一般都是采用“标记-复制”的算法,简单来说就是在Monir GC的时候将Eden和S0区还存活的对象标记,并复制到S1区间,当然也有其他情况,在此不做展开。而对于那些难以消亡的对象,也把它们集中存放在一起,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾回收的时间开销和内存的空间有效利用,比如说老年代,熬过几次垃圾回收的对象会进入到老年代,当然这只是其中一种情况,具体不做展开了,老年代采用的垃圾回收算法一般是“标记-清除”。
把分代收集理论放到JVM堆里面,就是所谓“年轻代”、“老年代”。但是分代收集并非只是简单的划分一下内存区域那么简单,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。
假如为了做一次年轻代的垃圾回收,但新生代里面完全有可能被老年代所引用的对象,为了找出该区域的对象,不得不在固定的GC Root之外,再额遍历整个老年代中所有的对象来确保可达性分析结果是正确,反过来老年代GC,也要遍历整个年轻代。这样无疑会给内存回收带来很大的性能负担,为了解决这个问题,便有了跨代引用的假说。
依据跨代假说,我们就不必为了少量的跨代对象 而去扫描整个老年代,只需要在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分成若干小块,表示出老年代的那一块内存会存在跨代引用。此后当发生Minor GC的时候,只有包含了跨代引用的小块内存里面的对象才会被加入到GC Root里面。虽然这种方法需要在对象改变引用关系的时候,去维护记录数据的正确性,会增加运行时的开销,但比起收集时扫描整个老年代来说,仍然比较划算。
1.2标记-清除算法
定义:
标记-清除算法分为两个阶段:首先,标记存活对象,即遍历所有GC Root,将所有GC Roots可达的对象标记为存活的对象;其次,统一回收所有未标记的对象。标记的过程属于垃圾判定的过程。
优点:执行速度快
缺点:容易产生较多的内存碎片,使得当需要分配较大的对象时,又不得不触发一次GC。

1.3标记-复制算法
定义:
为了解决标记-清除算法会产生内存碎片的问题,标记-复制算法,把可用内存按容量划分为大小相等的两块,每次只是用其中一块,当这一块的内存用完后,就将还存活的对象复制到另外一块上面,然后再将已使用过的内存一次性全部清理掉。如果内存中大多数对象都是存活着的话,这种算法会产生大量的内存间复制的开销,因此这种算法不适用于老年代的回收,更适合年轻代的垃圾回收。而将对象复制到另外一半内存的时候,按顺序分配即可,因此也不会有内存碎片化的问题。
优点:不会产生内存碎片化。
缺点:可用内存缩小为原来的一般。

1.4分代-整理算法
定义:
当对象的存活率比较高的话,如果继续使用标记-复制算法的话,效率将会降低。针对老年代对象存活的特征,提出了另外一种有针对性的“标记-整理”算法,其中标记过程和“标记-清除”算法一样,但是后续的步骤不是直接对可回收对象进行清理,而让所有存活的对象都想内存空间一端移动,按照内存地址依此排列,而未被标记的内存会被清理掉。如此一来,当我们给内存对象分配内存时,JVM只需要持有一个对象的内存地址即可,这比维护一个空闲列表显然少了很多开销,接着直接清理掉边界意外的内存。“标记-复制”于“标记-整理”本质差异在于前者是一种非移动式的回收算法,而后者是移动式的,是否移动回收后的存活对象是一项优缺点并存的风险决策:
- 如果移动存活对象,尤其是老年代这种每次回收都有大量对象的区域,移动对象并更新所有引用这些对象的地方,将会是一种极为负重的操作,而且这种对象移动操作必须全程咱暂停用户引用程序才能进行,像这种停顿的操作被称为"Stop The World",简称STW。
- 如果跟“标记-清除”一样,完全不考虑内存碎片的话,那便只能依赖更为复杂的内存分配器和内存访问器来解决。内存的访问是用户程序最频繁的操作,假如在这个环节上增加额外的负担,势必会直接影响应用程序的吞吐量。
基于以上两点,是否移动对象,都存在弊端,移动内存,则回收时会更复杂,不移动则内存分配会更复杂。从垃圾回收停顿的角度上来看,不移动对象停顿的时间更短,但是从整体程序的吞吐量来看,移动对象
吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
会更划算。虽然移动对象,会增加内存回收的时长,吞吐量的下降,但是和不移动对象相比,因内存分配和访问要比垃圾收集频率高很多,所以导致吞吐量收到的影响会比移动对象更大。
另外还有一种兼顾的方法,可以不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时大多数时间采用“标记就-清除”算法,暂时容忍内存碎片化的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用“标记-整理”算法收集一次,以获得规整的内存空间。CMS收集器面临空间碎片化过多的时候,采用的就是这种处理办法。
优点:1.消除了复制算法当中,内存减半的高额代价。2.消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,即分配内存很快,只需要将指针移动到下一块连续的空闲区域地址。如果是分散的内存,可能还需要,一张列表用来记录被占用的地址值。
缺点:1.从效率上来说,标记-整理算法要低于复制算法;2.移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。3.移动过程中,需要全程暂停用户应用程序,即:STW。
