垃圾判断算法
垃圾回收主要发生在堆和方法区中,而虚拟机栈、本地方法栈、程序计数器都是线程私有的,随着线程的开启和销毁而分配空间和释放,所以我们不需要考虑这个部分的内存回收和释放。方法区和堆中的内存的分配和回收都是动态的,是正式垃圾回收需要重点关注的部分。
引用计数法
当一个对象被其他对象引用时,就会使得其计数器加一,引用两次,则加二…当其他引用不在引用此对象时,则计数器减一,当计数器为0时,此对象就可以作为垃圾进行回收
- 缺点:当存在循环引用时,则可能会发生内存泄漏
循环引用:
对象A引用了对象B,对象B引用了对象A,但是没有其他对象引用对象A和对象B,此时,对象A、B引用计数器都不为0,无法进行回收。
可达性分析算法(Java虚拟机使用)
GC roots:垃圾收集器对象,GC只会收集不是GC roots并且没有被GC roots引用的对象(可以简单理解为不会被回收的对象)
- 垃圾回收之前,对堆内存中的对象进行扫描,确定哪些不是GC roots直接或间接引用的对象,这些对象就可以作为垃圾被回收。
- 一般有一下几种情况的对象会作为GC roots存在:
虚拟机栈中引用的对象:一般我们会在堆中为创建的对象开辟内存空间,并将内存空间地址作为引用保存在虚拟机栈中,当对象的生命周期结束,就会在出栈。
全局的静态对象(static修饰),此种对象的引用在方法区中
常量引用(static final修饰)。初始化之后不会修改,方法区常量池中的对象
本地方法栈对象
- 需要注意的是,即便是GC roots不可达的对象也不一定会被回收,一个对象正式被回收需要经历两次标记的过程,如果对象经过可达性分析没有和GC roots相连的引用链,则会对对象进行第一次标记和筛选,如果对象没有重写finalize()方法或者系统已经调用过一次finalize()方法,此时对象会被回收。否则进行第二次标记和筛选。
- 如果对象需要调用finalize()方法,则把对象放入F-Queue的队列之中,由虚拟机建立低优先级的Finalizer线程去执行finalize()方法,"执行"指的是虚拟机会触发这个方法,但并不承诺会等待它执行结束,这样做的原因是:如果一个对象在finalize中执行缓慢或者发生了死循环,将有可能导致队列中的其他对象永久处于等待,甚至导致整个内存回收系统崩溃。如果在执行方法过程中该对象和GC roots重新建立起新的引用链,对在队列中的对象进行第二次标记时该对象将会被移出回收的集合,否则对象被回收。
垃圾回收算法
标记清除
标记清除算法时最基础的清除算法,主要分为标记和清除两个部分:从GC roots开始扫描,将需要回收的对象进行标记,标记完成后,将标记的对象进行回收。
- 缺点:
(1)标记和清除过程效率很低
(2)会产生大量的内存碎片,当分配连续内存时,可能会触发另一次垃圾回收。 - 优点:
简单方便
标记整理
标记整理算法中的“标记”过程和标记清除算法中的标记过程相同,从GC roots开始扫描,将需要清除的对象进行标记。然后让所有的存活对象向一端移动,然后清除掉边界以外的内存。
- 优点:
(1)解决了内存碎片的问题。
(2)规避了内存只能使用一半的弊端。 - 缺点:
需要调动所有存活对象的地址,效率很差
复制
“复制”算法是为了解决“标记-清除”算法的效率问题,复制算法将内存分为大小相等的两块,每次只使用一块,当使用的这块内存满了之后,就会将存活的对象复制到另一块内存上去,将原来使用的内存清空,保证了可以有连续使用的内存。
- 优点:
解决了内存碎片的问题。 - 缺点:
内存的使用空间变小。
分代回收算法
分代回收算法并不是新的思想,而是对上面垃圾回收算法的适时利用,融合了以上三种基础算法思想的智能算法。根据对象的生命周期划分区域,使用不同的回收算法。
JVM将内存划分为新生代、老年代、永久代。
- 新生代和老年代存在于堆中,可以使用收集算法
- 永久代:主要存放静态文件,回收算法对永久代没有显著作用。
新生代回收机制 - 在新生代中主要使用了复制算法,新生代中的对象大多都是刚刚创建的或者一些使用周期较短的对象,在进行minor Gc 回收时,有大量的对象被回收,使用复制算法效率比较高,并且Minor GC发生的频率较高。
- 老年代中存放的对象生命周期较长,每次只有少量对象被回收,使用标记整理或者标记清除算法效率比较高。
总结