java中将内存的控制交给JVM来实现,在JVM的内存模型中,堆内存是JAVA内存区域中最大的一部分,GC主要就是发生在堆中,用来回收那些无用的对象。我们自己想一想,肯定是没用的对象需要被回收,那么如何判断哪些对象还有用,哪些没用了呢?一个对象被创建,如果被引用了,那这个对象肯定是有用的,如果引用全失效了,那就是没用的对象了,需要被回收。基于这个思想,引用计数法出现了。引用计数算法:这个非常容易理解,给每个对象添加一个引用计数器,对象每被引用一次,引用计数器就+1,引用失效时就-1。那么判断一个对象是否有用的条件就变成了对这个计数器值得判断了,如果为0,那么被回收,如果为>0,那么保留。但是这种方式会产生一个问题,就是对象之间的循环引用无法被识别,即使这两个对象不能被访问,但是它们之间互相引用着对方,故而计数器肯定>0,那么就不能被回收。JVM中并没有使用引用计数算法,而是使用了根搜索算法。
根搜索算法:这个算法也不难理解,通过条件,选择一系列的对象成为“GC Roots"对象,然后将”GC Roots"对象作为起始点开始向下搜索,搜索所有走过的路径成为“引用链”。在这个引用链上的对象就保留,而如果一个或多个互相引用的对象不在这个引用链上,或者说对象到“GC Roots"不可达,那么这些就是无用的对象,都需要被回收。
在java语言中,可作为GC Roots的对象包括下面几种:1) 虚拟机栈(栈帧中的本地变量表)中引用的对象
2) 方法区中类静态属性引用的对象
3) 方法区中常量引用的对象
4) 本地方法栈中JNI(即一般说的Native方法)引用的对象
我们知道了什么样的对象会被GC了吧,那么JVM又是通过什么方式来回收这些内存的呢?
标记-清除算法:试着想一想,如果要你要设计一个算法清除满足收集条件的对象来释放内存的时候你该怎么做呢?最简单的是不是就是把需要回收的对象标记一下,然后直接全部回收就行了?照着这个思路就是”标记-清除算法”的思想了,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。想法很简单,实际也就是这么做的。但是呢,这种方式是不是最好的?有什么缺陷?想到这里,就需要分析一下了。一个个的标记然后清除,效率高吗?当然不。看看下图的标记-清除算法的示意图,可以发现,标记-清除之后会产生大量的内存碎片,如果碎片太多,当程序运行没有足够连续的内存空间来存放大对象的时候,就会不得不提前触发一次GC。概括来说就是有两个缺点:效率不高;内存碎片可能导致提前发生GC。
学习算法的应该都很清楚,效率是很重要的,有时候需要使用空间来换时间提高效率,那么就需要了解一下第二种回收算法了——复制算法,它的思想就是空间换时间,将内存容量划分成相等的两块,当这一块的内存用完了,就将还存活的内存复制到另一块上,然后再把使用过的内存空间一次性清理干净。这样每次都是对其中的一块的内存进行回收,也就不需要考虑内存碎片等复杂情况了,只需要移动堆顶指针,然后按照顺序分配即可,实现简单,运行高效。但是缺点也很明显:内存变成一半了.下图就是复制算法的示意图:
分代收集算法: 通过上述的分析呢,就知道了对于堆中的新生代和老年代会采用不同的垃圾回收算法来回收“死亡”的对象,这种分代回收对象的方法称为“分代收集算法”。这个分代收集算法根据各个年代的特点采用适当的收集算法。在新生代中,每次GC的时候都发现大批的对象死去,只有少量存活,自然选用复制算法;而对于老年代这种存活率高、没有额外担保空间的,就必须使用“标记-清除算法”或者“标记-整理算法“了。 GC设计的理论基础就是这些了,其实原理还是比较容易理解的。GC的具体实现就是垃圾收集器,目前尚没有一个垃圾收集器是完美的,需要配合使用。下面插上一副堆内存划分图。