JVM系列(一)垃圾回收算法
Java是运行在虚拟机中的编程语言,虚拟机有自己的垃圾回收算法,对内存进行管理,因此一般情况我们不用考虑回收无用的内存,但是并不代表永远不关心,往往当程序上线后很多问题才暴露出来,特别是内存问题。在这总结一下Java虚拟机的垃圾回收算法,垃圾回收算法是解决哪些是垃圾的问题,以及如何回收垃圾的问题。
哪些是垃圾?
在进行垃圾回收之前,Java虚拟机是需要先确定哪些是垃圾的,这里有两种方法:引用计数器法和可达性分析法 引用计数器法 ,另外我们还可以根据引用的类型来决定是否是垃圾。
- 引用计数法
介绍:维护一个表,记录每个对象被引用的次数,当对象刚被创建时,每个对象拥有自己的引用计数器,引用计数器数值为0,每有一个引用指向该对象时,则计数器加一,减少引用减一,认为引用计数为0的对象是垃圾,应该被回收。
这种算法是有缺陷的,无法解决循环引用的问题。
若一个对象A 引用对象B,同时B 引用 A ,A和B 在内存中不被其他对象引用,也没有被栈引用。此时这两个对象在我们认知中是垃圾,但是按照引用计数器,他们的引用计数是1(不为0),JVM每次进行GC时是不会回收他们的。这就产生了内存泄露。因此这种算法不被Java虚拟机所接受。 - 可达性分析
这里特别注意的是哪些对象是GC ROOT 根,因为这在以后的分析堆Dump 的时候非常有用。 - 根据引用的类型来决定哪些需要被回收
引用分为强引用,软引用,弱引用,虚引用。虽然我们在进行可达性分析之后确定的哪些需要被回收,但是并不代表剩下的对象是不被回收的,剩下的对象需要根据引用的类型以及当前系统的状态来确定哪些还需要被回收。强引用是 平时写的 new 对象 的引用,一定不会被回收,软引用是在内存将要出现溢出的时候才会去回收,弱引用和虚引用只要是GC发生就会被回收。
通过上面的算法我们能很好的确定,哪些是需要被回收的对象。这里仅仅是确定需要被回收,能不能回收掉还要看对象是否能自我拯救。
- 关于自我拯救
需要回收的对象并不是一定会被回收,在回收之前如果他们再次建立了里引用链,那么将不会回收,这也是JVM降低对象创建开销的一种方式吧。
上面确定了哪些回收,那么下面我们就需要确定如何回收这些对象。
如何回收垃圾?
由于垃圾对象在内存中的分布是不连续的,若我们分别回收这些对象,则回收后的内存仍然是不连续的,对应大对象仍然无法在内存中分配。所以一般在垃圾回收之前都是需要整理内存的。因此,有这么几种方法来进行垃圾的回收:标记清除法,复制法,标记整理法。
- 标记清除法
介绍:通过可达性分析,以及引用类型分析得知哪些是需要回收的对象,把这些回收的对象进行标记,清除掉被标记的对象。
这种算法的缺点也很明确,就是会产生很多的碎片空间,同时他需要标记和清除两个过程,算法复制速度慢。为了好理解上个图:
标记清除算法,产生很多不连续的空闲空间,若此时需要分配一个大对象,比如上面图中三个连续空间的大小的对象,那么此时空间无法分配,我总不能把对象拆开放吧,JVM做不到。这样就引起了OOM,实际中这种算法还是适合于内存占用比较小的时候进行垃圾回收的。
- 复制法
为了克服标记清除算法的缺点,出现了复制算法,这种算法是把内存等分为两个部分,每次只使用一个部分 50%空间,当使用的一部分内存满了的时候,把这一部分中垃圾标记出来,有用的复制到没有使用的50%空间内,这时候复制过去的时候,是开辟的连续空间。然后把刚才使用的空间全部清理,只需要复制,不需要标记了。
还是上个图
这种算法的好处是,不会产生碎片空间,算法简单效率高,但是空间利用率低,垃圾回收发生的次数增加了。
实际中HotSpot虚拟机的年轻代就是使用这种算法进行垃圾回收的,但是他不是1:1的空间而是默认8: 1:1的空间,因为年轻代中的垃圾对象占比非常大。真正不需要清理的很少。
- 标记整理法
介绍:每次使用全部空间,标记出垃圾对象,把存活的对象移动到内存区域的一起,移动的过程直接把垃圾覆盖掉了一部分,形成连续的空间,剩余的空间直接清理掉。这种算法一般用于老年代回收,因为老年代中的对象存活的多,每次被回收的垃圾很少。
图解:
分代回收
实际中,JVM虚拟机是区分年轻代,老年代,永久代的。每个“代”根据自身的特点使用不同的垃圾回收算法。