jvm垃圾回收算法

135 阅读8分钟

上次在jvm内存结构介绍的时候有说过,在堆空间,分为老年代和新生代,新生代又分为Eden space,survive From,Survive to这三部分,那垃圾对象是如何在新生代和老年代进行清理的呢?

引入

我们知道,堆内存的空间是固定的,可以将内存空间理解为一个定长的数组,每次新建对象的时候,都会根据对象的大小,在数组中,按顺序选取一段长度来存储,这样的话,虽然最终是按顺序排列下来了,但是我们知道,每个对象的大小不一样,可能第一个对象占的是0,但是第二个对象会占用1-5, 我们想要删除那些无用的对象,需要有两个步骤,一是需要知道哪些对象是无用的,然后就是删除对象。但是在删除对象的过程中,如果0位置的对象删除了,后续对象没有0能放下的,只能一直往后排,这样的话会出现很多空的位置,这就是内存碎片,会占用大量的内存,而且这些位置无法进行使用。

所以我们在了解垃圾回收的时候,要了解的部分有

  • 如何知道哪些对象是需要被回收的
  • 如何最大化利用内存空间,避免因为内存碎片浪费空间
  • 不管是写代码还是做事情都要考虑的部分,那就是如何提高效率

判断对象可用算法

一般来说,我们知道,一个对象不再被使用,就是无用对象,对于jvm的角度就是去判断对象的引用,如果这个对象没有被引用,就没有了作用。在java中,引用的类型影响了一个对象被回收的时机,所以java引用也是我们在了解垃圾回收的时候必须要了解的一个东西

java引用类型

java的引用类型一共分为了四种,由强到弱有强引用,软引用,弱引用,虚引用四种类型,越弱,说明越容易被回收

强引用

强引用是我们最常接触到的类型,我们new 一个对象的时候,默认就是强引用,强引用的意思是说,只要这个对象,哪怕只还有一个被引用的地方,也不会被当做要被回收的对象

软引用
new SoftReference(T); //T需要传入一个对象

软引用需要我们手动去使用,意思是说,如果jvm内存不够用了,除了清理无用的对象,还会清理所有的软引用对象,来保证空间的容量,这个一般可以用于做对象的缓存使用

弱引用
new WeakReference(T);//T需要传入一个对象

弱引用也是需要手动创建,意思是说,如果一个对象,只被弱引用引用着,那么这个对象在下次垃圾回收的时候,会被当做无用的对象回收掉,这里有一个比较重要的地方就是,只有被弱引用引用着,才会被回收,如果弱引用里包着一个强引用,那么这个弱引用就不会被回收,等强引用被回收之后,弱引用才会被回收,所以弱引用这块有一个非常重要的部分就是,弱引用在ThreadLocal中的使用,具体弱引用和ThreadLocal之间的关系,后续会新增一篇专门来介绍ThreadLocal,更新之后,会增加到这部分的引用中

虚引用
ReferenceQueue referenceQueue = new ReferenceQueue(); //引用队列
new PhantomReference(T,referenceQueue);//T需要传入一个对象

虚引用的意思是是说,无论在什么情况下,执行GC就会把它回收,唯一和其他引用不一样的就是,虚引用的对象在被回收之前,会记录到引用队列里,我们可以根据引用队列是否有值,来判断对象是否会回收。

判断可用算法

引用标记法

引用标记法的意思是说,使用引用计数器来记录每个对象被引用的次数,如果增加引用,就+1,减少引用就-1,最终当这个引用计数为0的时候,就说明这个对象可以被回收了。这个是最基础的算法,但是这个算法有一个问题就是,有被循环引用的风险,如果一个对象A引用了B,B引用了C,C引用了A,这样最终单独一个计数器无法解决这种问题,会造成内存泄漏,还需要增加另外的逻辑来处理这种情况,所以大部分都不会采用这种算法

可达性判断算法

可达性算法的意思是说,只保留可以追溯到GC roots的对象,GC roots是说引用的根节点,它可以是类静态变量或者常量,可以是栈帧的局部变量,本地方法栈引用的对象,也就是说,只要这个对象没有被实际引用,哪怕在堆里面,他们还互相引用着,只要外部没有指向他们的指针,那么说明他们就是不可达对象,是需要被清除的

垃圾回收算法

标记清除

标记清除法的意思是说,先用判断对象可用算法,将所有不可用对象标记出来,之后进行整体清除。这样有个不好的地方就是我们上边说过的,清除之后,会出现很多的内存碎片想要完全解决,还需要重新排列一下内存(整理一下)

标记整理

标记整理算法,其实就是标记整理清除算法,流程大概是,先将所有存活的对象标记出来,然后将这些存活的对象进行整理(有序的排列到一个位置),然后将其他的空间全部清除。这样是不会产生内存碎片了,但是对象的实际地址改变了,需要改变对象的引用

标记复制

标记复制算法,就是说先将存活对象复制到一个区域,然后清除原本的区域。这样的做法不会产生内存碎片,但是会占用很大的内存空间,因为存活的对象在清除之前,会保留双份,如果对象特别大,是不是就占用的内存会特别多?

分带假设

对于所有的对象,jvm给他们分成了两类,一类是死的快,一种是活的旧,也就是说

  1. 大部分对象很快就不会在使用了
  2. 如果几次没有被回收,那么这个对象很可能还会存在很长时间

所以针对这种情况,jvm堆空间才会有明确的空间划分,来使用不同的回收算法,处理不同场景(年龄段)的对象,提高处理的效率

年轻代

年轻代使用的算法是标记复制算法,这就是为什么有了上篇文章说的流程,通过不停的循环from,to进行复制清除,并且通过对象的年龄(循环的次数)和大小,判断是否还需要保存在年轻代,因为如果大对象真的保存在年轻代,那么在复制的时候,占用的空间会非常大,影响垃圾回收整个流程的效率,而年龄如果大了,那么可能这个对象还会存在很长时间,没必要放在这么频繁的操作中。

老年代

老年代使用的算法是标记整理算法,因为我们知道老年代存的都是一些又老又大的对象,很难被清理,如果使用标记复制算法,根本不会产生太大的效果,因为标记复制算法本来就是因为能清理百分之90的对象,这种情况下只复制少量的存活对象,进行清理和复制效率都会很高。而老年代,可能更重要的任务是保证整个空间足够,不会影响年轻代的发挥,所以采用标记整理的算法,哪怕牺牲一些时间,但是换取更充裕的空间

小结

  1. 年轻代: 标记复制算法,用空间换时间,优化速度
  2. 老年代: 标记整理算法,用时间换空间,优化空间
  3. 各司其职

总结

本片文章主要介绍了jvm如何判断对象的存活、垃圾回收的几种算法,还有就是年轻代和老年代使用的算法是什么,有什么优缺点,为什么这么做。了解这些内容可以让我更加清楚在jvm中是如何管理对象的,让我对jvm有一个更深层次的认识。