因为JVM的GC(垃圾回收)机制,所以使java程序员不用再受内存分配及内存回收的烦恼(其实还是会有这些烦恼,我们以后再进行探讨),而可以将注意力集中在程序的设计上,虽然我们不用再关心这些过程,但是GC的垃圾回收我们还是要了解一下的。
对于内存的六大区域,对于线程私有的程序计数器,java虚拟机栈,本地方法栈三个区域,它们的生命周期都是随线程开始而生,线程结束而灭,并且这三个区域的内存分配和回收具有确定性,所以这部分不是GC所关心的。
所以,GC关心的只能是剩下的java堆与方法区了,其中99%的垃圾回收发生在java堆,而剩下的发生在方法区。
1. 如何判断对象已’死’
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经”死去”。判断对象是否已”死”有如下几种算法。
1.1 引用计数法
引用计数描述的算法为:
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数
器为0的对象就是不能再被使用的,即对象已”死”。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采用引用计数法进行内存管理。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。
/**
* JVM运行参数: -XX:+PrintGC 打印GC日志
* @author xucc
*/
public class Test {
public Object instance = null;
/**
* 内存占位用,方便垃圾回收时从日志观察效果
*/
private static int _1MB = 1024*1024;
private byte[] bigSize = new byte[2*_1MB];
public static void testGC() {
// 如果按照引用计数法,这里test1,test2创建时分别被引用一次,所以count1,count2都为1
Test test1 = new Test();
Test test2 = new Test();
// 又被引用一次,count1,count2都为2
test1.instance = test2;
test2.instance = test1;
// 分别取消引用一次,count1,count2都为1
test1 = null;
test2 = null;
// 强制JVM进行垃圾回收
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
如果采用引用计数判断对象,那么上面这种循环应用,对象的计数器永远不会为0,对象也就永远不会被GC回收,所以实际上GC并没有采用这种判断策略,运行结果如下:
内存占用从9M -> 1M,证明占位用的数组确实被回收掉了。
![](/qrcode.jpg)
1.2 可达性分析算法
Java并不采用引用计数法来判断对象是否已”死”,而采用”可达性分析”来判断对象是否存活。
同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言。
此算法的核心思想为 : 通过一系列称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为”引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:
对象Object5-Object7之间虽然彼此还有关联,但是它们到GC Roots是不可达的,因此他们会被判定为可回收对象。
在java中,可以作为GC Roots的对象有如下几种:
- 虚拟机栈中引用的对象(局部变量)
- 方法去中类静态属性引用的对象(static对象实例)
- 方法区中常量引用的对象(常量实例)
- 本地方法引用的对象