浅谈Java垃圾收集
尽管这是一个被大家说烂了的话题,但是,还是写一写,主要是为了理清自己的思路而已 ~
内存管理是很多程序的关键因素,以前写 C 的时候,无数的 malloc , free ,老师强调了无数遍,有一个 malloc 就应该对应一个 free ,否则,内存泄漏就会像噩梦一样缠绕着你。后来写 C++ ,换成了 new 和 delete ,最后学 Java 的时候,老师讲了, Java 做的比较好,不用程序员去管内存的申请和释放。因为 Java 里内置了一个 GC ( Garbage Collector ,垃圾收集器),他会负责所有 Java 的内存管理。但是,当时不太明白 GC 的原理是什么, Java 中有个 new ,但是没见到有 delete , free ,或者 remove 之类的表示释放内存的啊。后来自己研究了一下,下面是一些研究心得,有错误轻拍啊 ~
一. 什么是 GC ?
首先说,什么是 GC ?在这之前,先搞清楚什么是堆,什么是栈,学过编译原理的可会能比较清楚些。
C 语言中,内存主要划分为数据段,代码段,堆,以及栈,数据段,代码段比较容易理解,数据段就是保存全局变量或常量,或者静态变量的地方,代码段顾名思义就是保存程序代码的,这两个区域在程序加载执行后是固定不变的,而内存的动态管理主要涉及堆,以及栈。
栈中通常保存的是局部变量,以及函数调用,比如 A 函数中声明了一个局部变量, varA ,这个变量就存在栈中, A 函数调用了 B 函数,那么,就会将 A 函数的运行状态入栈, B 函数入栈,栈顶是 B ,执行 B 。
堆中保存的就是动态申请的内存,在 C 中就是通过 malloc 获得的内存区域,使用完毕后需要 free 掉,通常所说的内存管理也是管理这部分内存,因为栈中通过函数调用,变量的生存期等自动的申请释放了。
上面所说的是 C 中的内存分配,那么在 Java 中会是什么样呢?
Java 中:
实例变量和对象驻留在堆上。
局部变量驻留在栈上。
这只是书上说的,但是好像还是不是很清楚,要想搞清楚,我们必须知道当我们在执行一句
AnObject anObj = new AnObject();
的时候,到底发生了什么。事实上, anObj 只是一个类似于 C 中指针(注意,只是类似,不完全一样)的变量,他实际上只是一个地址值(或者其它的什么,看 JVM 了),指向 AnObject ()在内存中的实际位置——堆中的某个位置。而所谓的局部变量驻留在栈上,指的是 anObj 这个类似于指针的值是保存在栈上的。可以这么说,所有的对象都是驻留在堆上,栈中保存的只是局部变量对堆上某个对象的引用。
不过有一点要注意,对象的实例变量也是保存在堆中的(不会有人不知道实例变量是什么吧)。
简单的说了堆和栈,回归正题, GC , GC 的主要任务就是在 new 时,申请一份空间,当该对象没用时(一会说什么时候叫做“没用”)删除它,为其他对象腾地方。
GC 的工作原理:
好像有一种说法, GC 使用一种叫做标记和清除的算法,对于某种 Java 的实现,可能是这样,但是, Java 规范中并没有规定任何特定的实现(我理解,就是说,你怎么实现都行,只要能够实现申请和释放的功能就行)。我没有更深入的研究它的实现,因为我没想过要实现一个 JVM ,有兴趣的可以尝试一下。对于 Java 应用开发,需要知道什么时候一个对象到达被收集的标准了(也就是“没用了”),就可以了。
二. 收集条件
下面就来说一下比较重要的什么时候满足收集条件?
每个 Java 程序都会有一到多个线程,每个线程都具有自己的线程栈。通常,在一个 Java 程序中,至少会运行一个线程(也就是位于栈低的 main() )。每个线程都有自己的生命期,(不太清楚线程的最好还是看看 OS 的书,或这比较好的 Java 书对线程也有介绍)。
收集条件就是:
没有任何活的线程能够访问一个对象时,该对象就符合垃圾收集条件。
根据这一定义,当 GC 发现一个对象不能被任何活线程访问时,就会认为该对象符合收集条件,在某一时刻将其删除(不确定的某个时刻,一会说什么时候 GC 执行)。什么叫做一个对象被线程访问呢,就是说,该线程中有一个引用变量指向的是该对象在堆中的位置。
这也就涉及到一个问题,一直以为 Java 中不会出现内存耗尽,因为有 GC 在,不会出现内存泄漏,事实上不是这样的,内存耗尽是可能的,例如你创建并保持访问太多的对象,就会出现内存耗尽。
可能通过上面的解释还是不能够具体的理解什么时候满足收集条件,下面举几个具体的例子:
1. 空引用
这是最简单的一种满足收集条件的情况:
StringBuffer sb = new StringBuffer(“GC”);
// 其它操作
Sb = null;
// 此时如果没有其它对象引用 StringBuffer(“GC”) (也就是没有进行过类似 xx=sb; 操作),那么 StringBuffer(“GC”) 在堆中的对象就满足收集条件。
2. 重新为引用变量赋值
StringBuffer sb = new StringBuffer(“GC”);
StringBuffer sbx = new StringBuffer(“Another”);
// 其它操作
Sb = sbx;
// 此时, sb 指向了 sbx 所指向的 Another 对象,没有引用变量再指向 GC 对象,那么该对象也满足收集条件。
3. 隔离引用
这一条可能是比较难理解的,简单说,就是尽管某个对象被其他人引用,但是引用者也在堆上,没有堆外的引用,那么引用者与被引用者都满足收集条件。
上面说过,对象的实例变量也保存在堆上,如果某个对象 A 的实例变量 i 引用另外一个对象 B ,但是除此之外没有其他人引用他们中的任何一个,那么他们两个都符合收集条件。示例如下:
public class A
{
A i;
public static void main(String[] args)
{
A a1 = new A();
A a2 = new A();
A a3 = new A();
a1.i = a2;
a2.i = a3;
a3.i = a1;
a1 = null;
a2 = null;
a3 = null;
//此时尽管有对堆上对象的引用,但是引用者也在堆上
}
}
三. 什么时候执行 GC
下面来说一下什么时候 GC 会执行?
GC 受 JVM 控制,由 JVM 决定什么时候运行 GC 。但是 Java 规范并没有规定什么时侯执行 GC ,尽管我们可以请求执行 GC ,但是只是提建议,最终执行与否,完全看 JVM ,人家心情好,说不定会立即相应你,心情不好时,那你就等着吧。因此,绝对不能依赖于 JVM 请求 GC 来完成某种行为。
下面写个简单的请求 GC 的例子:
public static void main(String[] args) {
// TODO Auto-generated method stub
Runtime rt = Runtime.getRuntime();//获得Runtime
System.out.println(rt.totalMemory());//全部内存
System.out.println(rt.freeMemory());//空余内存
Date[] sa = new Date[100000];
for(int i=0; i<100000; i++){
sa[i] = new Date();
}
System.out.println(rt.freeMemory());
for(int i=0; i<100000; i++){
sa[i] = null;
}
sa = null;
System.out.println(rt.freeMemory());
System.gc();//请求GC
System.out.println(rt.freeMemory());
}
该程序的输出结果为:
5177344
4935344
2221696
2221696
5033840
尽管这里显示的是 JVM 确实响应了请求,但是,再次强调,这是没有保证的。
OK ,就这些,继续努力 ~
blog comments powered by Disqus