垃圾回收机制是java的一个重要的特点,也是java面试中经常被问到的一点。首先考虑GC就要考虑3点:一、哪些内存需要回收;二、内存应该在什么时候回收;三、内存应该被如何回收。
一、哪些内存需要回收
我们知道java中除了8种基本类型(byte(1个字节)、char(2个字节)、int(4个字节)、short(2个字节)、long(8个字节)、double(8个字节)、float(4个字节)、boolean(1个字节)),其他的都是对象类型,而JVM会把对象放到堆内存中,而把系统自动完成分配和释放的数据放在栈中。显然堆中存放的对象数据的内存是需要显式分配的,因而若没有垃圾回收机制就需要手动的显式释放这些内存,而这正是GC需要回收的内存。
二、内存应该在什么时候回收
当对象已经“死亡”了,即没有任何引用了,就表示GC可以执行了。那么如何判断一个对象是否死亡显然就是一个问题。常见的有两种算法。
(1)引用计数算法
引用计数算法就是给对象添加一个引用计数器,每当有一个地方引用该对象,计数器值就加1,当引用消失时,计数器值就减1,任何时刻计数器值为0的对象就是死亡的对象。但是这个算法有个很重要的缺陷就是它很难解决对象之间循环引用的问题。
(2)可达性分析算法
这是比较主流的算法。它是通过一系列“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径即为引用链,当一个对象到“GC Roots”没有任何引用链可达的话,那么就说明该对象死亡了。
但是事实上当前通过算法说明该对象死亡了,并不表示它一定会被回收。系统有个finalize()方法,如果该对象覆盖了该方法的话,那么该对象至少还要经历2次标记过程,但是当对象没有覆盖该方法或者该方法已经被JVM调用过了,那么将回收该对象。
所谓的2次标记,第一次即判断该对象是否要执行finalize(),要执行就标记出来,并将其放到F-Queue的队列中,然后会执行finalize()方法。而GC会对F-Queue中的对象进行第二次标记,这时候如果对象在执行finalize()方法中重新与引用链上任一对象建立联系,那么第二次标记时就将它移出即将回收的集合。finalize()方法也是对象逃脱回收命运的最后机会,而且一个对象的finalize()方法最多只会被系统调用一次。
三、内存应该被如何回收
垃圾回收有好些不同的算法
(1)标记-清除算法,该算法分为“标记”和“清除”两个阶段:先标记出需要清除的对象,在标记完成后统一清除所有被标记的对象。该算法有两点不足:1,算法效率不高,标记和清除效率都不高;2,空间问题,标记清除后会产生大量不连续的内存碎片,导致以后在分配较大对象时会因无法找到足够的连续内存而提前触发垃圾回收动作。
(2)复制算法,该算法可以将内存分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次性清除掉,这样就不用考虑内存碎片了。但是显然这种算法的代价是让内存变成原来的一半了。
一般这种算法都是来回收新生代的,而新生代中的对象98%都是很快就死亡的,所以不需要按照1:1的比例来划分内存空间,而是分为一个较大的Eden空间和两个较小的Survivor空间,用的时候用Eden和一个Survivor空间。回收时将还存活的对象复制到剩余的Survivor上,将使用的Eden和Survivor清空。当Survivor内存不够,还要依赖老年代内存进行分配担保。
因为当前JVM的垃圾回收大都采用“分代回收”算法,所以有新生代和老年代的划分。这里面jvm给每个对象定义一个年龄计数器。就是对象没熬过一次GC年龄就加1,这里有个年龄阈值,当一个对象的年龄超过这个阈值就划分到老年代,低于则在新生代。
(3)标记-整理算法,根据老年代的特点,一般采用该算法。标记跟前面的标记一样,整理是将所有存活的对象都向一端移动,然后回收端边界以外的内存。
参考:《深入理解java虚拟机》,《java程序员面试宝典》