Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
通过判断这些对象之中哪些还“存活”,哪些已经“死去”(“死去”即不可能再被任何途径使用的对 象)了,来决定是否回收内存。主要有应用计数算法以及可达性分析算法
1.引用计数算法
在创建的每一个对象中都添加一个一个引用计数器,这个计数器记录着该对象被使用的次数。每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,同时将该对象引用的其他对象的计数器进行减一操作
优点:
-判断效率高
-回收及时:引用数为0,立即回收。如果引用的对象活跃,能够及时回收的指标就显的比较重要。
-卡顿优化:不会被频繁触发而引起卡顿
-域对象:空间作用域只是指定的对象,不会扫描全对象
缺点:
-循环引用无法释放
C c;
}
class C {
B b;
}
public class A {
public static void main(String[] args) {
C c=new C();
B b=new B();
c.b=b;
b.c=c;
b=null;
c=null;
}
}
如上述代码中,创建b和c对象之后,其引用计数器的值分别为1,当c对象和b对象相互引用对方后,其引用计数器的值分别增加为2,那么当b和c均赋值为null之后,引用计数器的值分别减1,变为1,虽然已经不会再被使用,但不会被垃圾回收
2.可达性分析算法
当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
这个算法的基本思路就是设置一系列被称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,从GC Roots到这个对象不可达,则此对象是不可能再被使用的。如图中的对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。
在Java语言中,可以作为GC Roots的对象包括以下几种
-虚拟机栈(栈帧中的本地变量表)中引用的对象。
-方法区中类静态属性引用的对象。
-方法区中常量引用的对象。
-本地方法栈中JNI(即一般说的Native方法)引用的对象。
-Java虚拟机内部的引用,如基本数据类型对应的Class对象,常驻的异常对象、还系统类加载器。
-所有被同步锁(synchronized关键字)持有的对象。
-反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
优点:
-可达性分析可以解决引用计数器所不能解决的循环引用问题:即便对象a和b相互引用,只要从GC Roots出发无法到达a或者b,那么可达性分析便不会将它们加入存活对象合集之中。
缺点:
-在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为null)或者漏报(将引用设置为未被访问过的对象)。
-漏报会导致Java虚拟机损失了部分垃圾回收的机会。
-误报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存,导致对象消失。 一旦从原引用访问已经被回收了的对象,则很有可能会直接导致Java虚拟机崩溃。