聊聊垃圾回收算法( 二 )


 
缺点:内存使用率低 。我们一次只能使用整个内存的一半,内存使用率最多只会达到 50% 。
 
两种综合的算法 
好,探讨完三种基础的垃圾回收算法之后,再来探讨 Java 里面两种相对综合的垃圾回收算法 。
 
第一种是分代收集算法 。分代收集算法的思路是把一个内存分成多个区域,不同的区域使用不同的回收算法去回收 。代收集算法比较复杂,而且细节极其之多 。我们将在下面详细讨论 。
 
第二种是增量算法 。你想,如果你的内存非常的大,如果一次收集所有垃圾,那么需要耗费的时间就会比较的长,有可能会造成系统长时间的停顿 。那么增量算法的思想是每次只收集一小片区域内存空间的垃圾,这样就可以减少系统的停顿 。
 
分代收集算法 
目前各种商业虚拟机,它的堆内存的回收基本上都采用了分代收集的方式 。所以可想而知,分代收集算法有多么重要 。
 
现在设计算法的思想是根据对象的存活周期,把内存分成多个区域,然后不同的区域使用不同的垃圾回收算法去回收对象 。Java 把堆分成了新生代和老年代 。下图前面探讨 Java 内存结构的时候已经详细介绍过了 。

聊聊垃圾回收算法

文章插图
 
那么经过分代之后啊,垃圾回收可以分成以下几类:
 
一是新生代的回收(Minor GC 或者 Young GC) 。
 
二是老年代的回收(Major GC) 。
 
三是整个堆的回收(Full GC) 。
 
那么由于执行 Major GC 的时候,一般也会伴随着一次 Minor GC,所以可以认为 Major GC ≈ Full GC。那么很多时候,程序员在聊 Major GC 以及 Full GC 的时候,聊的就是一件事儿 。
 
下面来看一下对象是怎么样分配到堆内存的,我们还是对照堆内存的结构去讲解 。对象在创建的时候会先存放到 Eden 。当 Eden 满了之后就会触发垃圾回收,这个回收的过程是把 Eden 里面存活的对象拷贝的存活区里面的 From Survivor 或者是 To Survivor 里面去 。
 
比如第一次回收 From Survivor 里面去了,那么下一次回收就会把 From Survivor 里面存活的对象拷贝到 To Survivor 里面去,再下一次就会把 From Survivor 面里面存活的对象拷贝的 From Survivor 里面去,周而复始 。
 
不难发现这个回收的过程使用了复制算法,这也是为什么新生代要有两个 Survivor 的原因 。因为复制算法需要把一个内存分成两块 。那么对象每经历一次垃圾回收之后,如果还存活的话,它的年龄就会增加 +1 。当对象的年龄达到阈值的话,默认情况下是 15,就会晋升到老年代 。
 
老年代里面的对象存活率一般是比较高的,因为你想,都回收 15 次了,还是没能回收得了,所以后面继续存活的可能性依然是比较大的 。
 
那么老年代的对象一般会使用标记-清除算法或者是标记-整理算法去进行回收 。这里需要说明一下,这个对象分配的过程只是一个典型的分配流程,实际情况是存在例外的:
 
一是,一个新建对象,并利率总是会分配到 Eden,也可能会直接进入到老年代 。主要有两种场景 。第一,如果你的对象大小,大于这个阈值就会直接分配到老年代 。当然了,默认情况下这个参数的值是零,也就是说不做限制,所有的对象都会优先在 Eden 里面分配 。第二,如果你的对象非常的大,比方说一个超大的数组,新生代的空间根本都不够,那么这个时候也会直接把这个对象放到老年代去担保 。之所以要允许对象直接分配到老年代,主要是因为新生代采用的是复制算法,在 Eden 里面分配大对象的话,将会导致 Eden 和两个 Survivor 区之间大量的内存拷贝 。
 
二是,对象也不一定要达到年龄阈值,才会进入到老年代 。虚拟机有一个动态年龄的概念,就是说如果存活区里面,所有相同年龄对象的大小的总和已经大于 Survivor 空间的一半了 。这个时候,如果某个对象的年龄大于这个年龄的话,会直接进入老年代,比如有一堆的对象,它的年龄值是 9,年龄都是 9 的所有对象它的总和以及大于 Survivor 空间的一半了,那么年龄大于 9 的对象就会直接进入老年代 。
 
触发垃圾回收的条件 
下面我们来看一下不同区域触发垃圾回收的条件:
 
先来看一下新生代回收的触发条件,Eden 空间不足就会进行 Minor GC 回收新生代 。


推荐阅读