该文章首发于微信公众号“字节流动”
JVM 垃圾回收机制
理解 JVM 垃圾回收机制需要弄清楚两个问题:
- 什么是垃圾,或者怎么判定垃圾?
- 怎么回收,或者通过什么方式回收垃圾?
什么是垃圾,或者怎么判定垃圾?
“垃圾”即不再存活的对象,判定对象是否存活,一般有两种方法:
- 引用计数算法
- 可达性分析算法
引用计数算法
为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当有一个地方引用它,引用计数器就加 1 ;当引用失效,引用计数器就减 1 ;当引用计数器为 0 ,意味这个对象没有被使用,可以认为“对象死去”。
但是,引用计数算法很难检测出对象之间的循环引用,当两个对象互相引用,即时它俩都不被外界任何东西引用,它俩的计数都不为零,因此永远不会被回收。所以, JVM 并没有用此方案来检测对象是否存活。
可达性分析算法
可达性分析算法的基本思路是以一系列 GC Roots 对象为起点出发,然后向下搜索,搜索所走过的路经称为引用链,处在引用链上的对象称为可达对象,或存活对象;而当一个对象到 GC Roots 对象没有任何引用链相连,则被视为不可达对象,或垃圾。JVM 采用可达性分析算法判断对象是否存活。
JVM 中哪些对象可以作为 GC Roots?GC Roots 对象本身一定是可达的,这样从它们出发遍历到的对象才能保证一定可达。一般有如下几种:
- 虚拟机栈(帧栈中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI 引用的对象。
通过什么方式回收垃圾?
常用的垃圾回收方式有:
- 标记-清除算法 (Mark-Sweep)
- 标记-整理算法 (Mark-Compact)
- 复制算法 (Copying)
- 分代收集算法 (Generational Collection)
标记-清除算法 (Mark-Sweep)
基本原理:先标记出需要回收的对象,然后一并回收被标记过的对象。
通过可达性分析算法判定对象不可用,是标记的第一步,如果对象已经被标记一次,它将被进行一次筛选,筛选条件为对象是否重写了 finalize() 方法,或者 finalize() 已经被虚拟机调用。当对象没重写或 finalize() 已经被执行过,就证明对象已经没有可能再回到可用状态,待虚拟机第二次标记它时,就会被放到回收的集合中。
标记-清除算法缺点:
- 效率低,标记和清除两个过程的效率都不高
- 易产生内存碎片,标记清除之后会导致内存中出现大量的不连续的内存碎片,之后分配较大对象的内存空间时,导致没有足够的连续内存,可能会触发另一次的垃圾回收,这个问题也间接的反映出了效率不高。
当然, JVM 并不是只用这一个算法去回收垃圾,而是在对象存活率较高的时候,使用这个算法。
标记-整理算法 (Mark-Compact)
标记-整理算法可以说是标记-清除算法的完善版,标记-清除算法容易产生内存碎片。标记-整理算法是在标记后,将所有存活的对象都向一端移动,然后再清理掉边界以外的内存。
复制算法 (Copying)
复制算法的基本思想是将可用内存容量划分为大小相等的两块,每次只用其中的一块,当这块内存用完了,就将还存活的对象复制到另一块上,然后清除已使用的那块。这样每次固定回收一半内存,实现简单,运行高效,并且不会出现内存碎片。这个算法的代价也很大,就是要牺牲一半的内存。
主流的商业虚拟机一般采用复制算法来回收新生代,因为新生代里的对象 98% 都是“朝生夕死”,所以并不需要按照 1:1 的比例划分内存空间。一般新生代只有 10% (不绝对)左右的对象存活,虚拟机的划分比例往往高于 8:1 。
分代收集算法 (Generational Collection)
目前主流的商业虚拟机均采用分代收集算法,根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。
- 新生代:对象存活率低,每次垃圾回收后都只有少量对象存活,因此适合用复制算法。
- 老年代:对象存活率高,适合使用标记-清除或者标记-整理算法。
联系与交流
微信公众号
个人微信