Java语言最大的优势就是垃圾自动回收,不需要开发者去关心内存管理的事情,可以专注于业务逻辑开发。垃圾回收并不是Java所特有的,实际上关于垃圾回收的语言诞生要早于Java。关于内存回收,基本上都要考虑三件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
内存自动回收的核心是垃圾收集算法,每一种垃圾收集器(Gabage Collection)都是基于不同的算法理论来实现的,因此算法对GC的性能、回收等都起着重要作用。从如何判定对象消亡的角度出发,垃圾收集算法可以划分为引用计数式垃圾收集
(Reference Counting GC)和追踪式垃圾收集
(Tracing GC)两大类,这两类也常被称作直接垃圾收集
和间接垃圾收集
。
常见垃圾收集算法
Java虚拟机中常用到的垃圾收集器基本都是采用追踪式垃圾收集算法,而引用计数法并未实际被应用到某个垃圾收集器。
引用计数算法
这个算法的原理很简单,在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。但是在主流的垃圾收集器中都没有使用过这种算法,当出现两个对象之间互相引用,但是这两个对象又没有实际被使用的场景,会导致引用计数都不能清零,成为无法回收的垃圾。
可达性分析算法
这是主流虚拟机都在使用的一种垃圾回收算法,通过一些GC Roots来向下搜索,形成一个个引用链,如果某个对象没有到达GC Roots的引用链,那么就判定这个对象可回收,也就解决了引用计数算法里面判定的问题。因此也定义了一些常见的GC Roots。
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
标记-清除算法
Mark-Sweep,算法分为标记清除两阶段,首先对需要回收的对象进行统一标记,在标记结束后,统一回收,也可以统一标记存活对象,统一回收未标记对象。这个算法是最基础的,但是也有很多问题,比如大量的标记和清除,会导致两个阶段的效率都不同程度的降低,其次是标记清除后空间的不连续性,不利于大对象的分布,并由此触发下一次GC。
标记-复制算法
Semispace-Copying,使用的是半区复制的思路,与清除所不一样的是,复制算法,将新生代既定内存区域按照1:1分为两部分,把存活对象迁移至其中一个半区,然后清理已使用过的空间,这种算法复制过程通过指针移动来实现,因此带来的开销是内存空间开销,会消耗大量的内存。从某种程度上标记-复制解决了标记-清理在处理大量可回收对象时遇到的效率问题,但是空间消耗是其要面对的另一个问题。因此对于这种情况,考虑到大部分场景的对象都是朝生夕灭,存活周期较短,将新生代按照比例1:8划分为Survivor:Eden区,Survivor又划分为From和To两个区域,每次分配内存只使用Eden和其中一块Survivor(From)。发生垃圾收集时,将Eden和Survivor(From)中仍然存活的对象一次性复制到另外一块Survivor(To)空间上,然后直接清理掉Eden和已用过的那块Survivor(From)空间,清空后存在一个From->To的转变,出现From和To的角色互换,总是保证在发生GC之前保证To区域是空闲的。当Survivor不足以承担一次MinorGC时就需要通过分配担保(Handle Promotion)机制来将一些对象迁移到老年代。
标记-整理算法
Mark-Compact,相当于是标记-清除,标记-复制算法的优势整合,标记阶段通标记-清除算法的标记阶段一致,但是在标记结束后不直接清理,而是让存活对象向内存空间另一端移动,这点类似于复制的处理。标记-整理算法是建立在是否移动回收后的存活对象这个风险点之上的,移动对象会加大延迟,STW现象更为显著,但是带来的好处是移动对象后整片的内存区域可以提高吞吐量,因此在HotSpot VM中关注吞吐量的Parallel Scavenge GC是基于标记-整理算法实现的,关注延迟的CMS是基于标记-清除算法实现的。