薛定谔的风口猪

站在巨人的肩膀上学习,猪都能看得很远

JVM垃圾回收算法

GC的原理是,找到不再被使用的对象(没有被引用),然后回收这些对象所占的内存。通常使用的是收集器的方式实现GC,主要有引用计数收集器和跟踪收集器

  • 引用计数收集器

顾名思义,引用计数收集器做的事是记录每个对象现在被引用的数量,而当计数器下降到0的时候,证明已经没有被引用了。就可以被回收了:如下图所示

引用计数收集器

当A释放了B的引用后,就可以回收B的所占用的内存。

但是引用计数需要对每一个对象赋值时进行计数器的增减,这有一定的消耗。更重要的是,他无法实现循环引用的场景。例如如果B和C相互引用,那么即使A释放了B和C的引用,也无法回收B和C。

- 跟踪收集器

采用集中的管理方式,全局记录数据的引用状态。基于既定的条件触发(如定时或者空间不足),执行时候需要从跟集合扫描对象的引用关系,这要有复制(Copying)、标记-清除(Mark-Sweep)和标记-压缩(Mark-Compact)三种实现算法。

  • 复制(Copying) 复制收集器从根集合开始扫描所有存活的对象,把能达到的所有对象(存活对象)复制到一块新的未被使用的空间中,没有到达的对象则证明没有被引用,可以回收。这样就解决了循环引用不可回收的问题,如下图所示:

复制算法

好处:仅需扫描一次所有存活的对象,当存活对象较少时(垃圾对象多),比较高效。适合于新生代区

坏处:需要额外的内存空间,不适合老年代(大部分是存活对象)

  • 标记-清除(Mark-Sweep)

采用从根集合开始扫描,对能到达的对象(存活)进行标记,标记完成后,再扫描整个空间中未标记的对象,并进行回收。

标记-清除

好处:仅需扫描一次所有存活的对象,不需要移动对象,仅需要对不可达对象(垃圾对象)进行处理。适合于垃圾对象比较少的情况。

坏处:容易造成内存碎片

  • 标记-压缩(Mark-Compact)

和标记-清除一样对可达对象(存活对象)进行标记,但是回收垃圾对象的后,会对其他存活的对象往左端空闲空间进行移动,并更新引用对象的指针。适合老年代。如下图:

标记-压缩

好处:对比标记-清除,避免了内存碎片

坏处:增加了移动对象的成本。


  • 增量算法

由于大部分的回收算法在回收时候,只有垃圾回收线程在运行,其他线程都会被挂起,直到垃圾回收结束。

这将导致在垃圾回收很久时候,其他线程一直不工作,而导致系统无响应。例如在Android应用中,如果UI进程5秒不响应就会出现ANR(Application Not Responding)错误。所以一个可行的方法是让垃圾进程与其他线程交替执行,每次垃圾回收仅进行部分内存的回收,减小系统的停顿时间。但由于线程的来回切换,提高了垃圾回收的开销。


总结:

一个对象新创建的时候进入到新生代,然后垃圾回收几次后,该对象依然存活,于是就放入老年代。所以,新生代适合使用复制算法(大多是垃圾对象,复制开销小),老年代多用标记-压缩算法(大多是存活对象,回收对象少)。然后整个垃圾回收器的工作线程是用增量算法,时间轮转片执行。