在Java中,内存是动态分配以及垃圾也是自动回收的。了解GC和内存分配,可以用来排查各种内存溢出、内存泄漏的问题,垃圾收集一般会成为系统瓶颈,所以需要对这些技术实施监控和调节。
垃圾收集器对堆进行收集前,要确定这些对象是否还活着。看哪些对象可以被回收,何时回收这些对象,采用什么样的方式回收。一般有一些算法。
引用计数算法,
一种比较简单直观算法,效率比较高,当使用引用时,则+1,删除引用时,则-1,那么就只用收集计数为0的对象。有点类似于入栈和出栈的计算。但也存在一些缺点:需要编译器的配合使用,会生成额外的代码;另外一个是无法处理循环引用问题,如A.b =B和B.a=A,若没有其他的引用,那么再不可能被访问,但是计数算法也无法回收它们。实际上JVM并没有使用它们。
public class Object {
Object field = null;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
Object objectA = new Object();
Object objectB = new Object();//1
objectA.field = objectB;
objectB.field = objectA;//2
//to do something
objectA = null;
objectB = null;//3
}
});
thread.start();
while (true);
}
}
根搜索算法
设立若干种根对象,当其中某个根对象不可到达到达另外某个对象时。那么这个对象则是可以被回收的。可以当作GC roots的对象一般有几种情况:1.虚拟机栈中的引用对象;2.方法区中的类静态属性引用的对象;3.方法区中的常量引用的对象。4.本地方法栈中JNI的引用的对象。根搜索算法解决的是垃圾搜集的基本问题,即整个垃圾回收过程中的最基本问题,也是最关键的问题。解决什么时候回收和如何回收需要另外的算法。垃圾搜集的算法主要有标记-清除算法、复制算法、标记-整理算法。
标记/清除算法:
此算法的基本思想就是:当程序再运行期间、若没有了可以利用的内存,那么GC线程就会触发让程序暂停,随后将依旧存活的对象标记一遍,而堆中没有被标记的对象就全部清除掉,然后唤醒停止的程序线程。这个过程不难理解,但是也存在致命性的问题,比如刚刚创建一个对象,还没有来得及标记,然后被清理了,那么就会影响程序后面的工作。另外来说,要停止程序运行,那么会导致程序变慢,效率降低,清理出来的内存空间也是不连续的。
复制算法:
此算法在于将内存划分为两个区间,而所有的动态分配的对象都分配在一个空间中,另外一个空间是空闲的。当一个空间内存消耗完了时,便会开启复制算法GC线程,会将所有存活的对象都复制到空闲区间中,复制方式是将内存地址引用,那么空闲区间与活动区间进行了交换,那么原来已经用了的还没复制的就是垃圾,会被清理掉。这个算法的要求是要对象的存活率要非常低才好,而且会克服50%的浪费。复制的工作花费的时间会比较长。
标记/整理算法:
标记/整理算法与标记/清除算法非常相似,也分为两个阶段,前一阶段与标记/清除一样,遍历GC Roots,然后将存活的对象标记。标记完成后,将存活的地址依次排序,然后将末端内存地址以后的内存全部回收。当需要给新的对象分配内存时,JVM只需要有一个内存的起始地址即可,这比维护一个空闲列表要少许多开销。
可以看到:
效率:复制算法>标记/整理算法>标记/清除算法(针对时间复杂度)
内存整齐度:复制算法=标记/整理算法>标记清除算法
内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法