Bruce Eckel的《Thinking in java》前两章中多次强调了这一点:C++中的memory leak在java
中是不会发生的,开发人员根本不用去考虑这个问题,这大大减少了开发的时间。这是因为java有
一个垃圾回收器,用来监视用new创建的所有对象,并辨别那些不会再被引用的对象,随后,释放
这些对象的内存空间,以便供其他新的对象使用。但是java的垃圾回收器是怎么辨别的呢?Bruce
并没有做相应的解释。下面我们就这一点做一个深入的探究。
首先我们要说明一下java中为对象分配内存的方式。在java中一切都是对象,并且共享一个超类
Object。尽管一切都被看做对象,但是操纵的标识符实际上是对象的一个“引用”。像String s
= new String("hello");我们得到的s是一个引用,然后我们对它进行了初始化。引用可以独立
存在,就像遥控器可以独立于电视机存在一样,但是你不能向它发送消息,否则便会返回常见的运
行时错误NullPointerException。java中用new操作符创建的对象都是分配在堆区,这里的堆与
数据结构中的堆是两回事,分配方式倒是类似于链表。堆的好处是:编译器不需要知道存储的数据
在堆里存活多长时间,但是在堆栈中所有项的确切生命周期都必须被知道。C++看重的是效率,为
了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象置
于堆栈和静态存储区域内实现。但如果是在堆中那么是动态地,可能需要大量的时间在堆中查找和
分配。但是java认为对象趋向于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成
大的冲击。所以java完全采用了动态分配。但是这其中有一个特例,那就是基本类型。因为根据上
面的描述。显然在堆中用new创建一个小的、简单的变量往往不是很有效。因此对于这些基本类型
java采用了与c++相同的方法,同样将他们存储在堆栈中。但是为了在堆中也能存储他们,也为了
在容器中能存储基本数据类型(容器中存放的都是对象的引用,但是堆栈区的“对象”没有引用)
,java还为基本类型提供了包装器类,这或许能解决很多同学对于包装器类的疑惑吧。
Java的堆更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配
速度相当快。Java的“堆指针”只是简单地移动到尚未分配的领域。也就是说,分配空间的时候,
“堆指针”只管依次往前移动而不管后面的对象是否还要被释放掉。如果可用内存耗尽之前程序就
退出就再好不过了,这样的话垃圾回收器压根就不会被激活。但是由于“堆指针”只管依次往前移
动,总有一天内存会被耗尽,垃圾回收器就开始释放内存。
怎么判断某个对象该被回收呢?答案就是当堆栈或静态存储区没有对这个对象的引用时,就表示程
序(员)对这个对象没有兴趣了,它就应该被回收了。有两种方法来知道这个对象有没有被引用:
第一种是遍历堆上的对象找引用;第二种是遍历堆栈或静态存储区的引用找对象。前者的实现叫做
“引用计数法”,意思就是当有引用连接至对象时,引用计数加1,当引用离开作用域或被置为
null时,引用计数减1,这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该
被回收,但引用计数却不为零”的情况。Java采用的是后者,在这种方式下,Java虚拟机采用一
种“自适应”的垃圾回收技术,如何处理找到的存活对象(也就是说不是垃圾),Java有两种方式
:
一、停止—复制(stop-and-copy):先暂停程序的运行,然后将所有存活的对象从当前堆复制到另
一个堆,没有复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,紧凑的。效
率很低:首先,得有两个堆空间占用率200%;其次,垃圾较少时,复制大量的活着的对象,是很大
的浪费。
二、 标记—清扫(mark-and-sweep):从对战和静态存储区出发,遍历所有的引用,进而找出所有
存活的对象,如果活着,就标记。只有全部标记完毕的时候,清理动作才开始。在清理的时候,没
有标记的对象将会被释放,不会发生任何肤质动作。但是剩下的对空间是不连续的,垃圾回收器要
是希望得到连续空间的话,就得重新整理剩下的对象。
【注意】“停止-复制”和“标记-清扫”无非就是:“在大量的垃圾中找干净的东西和在大量干净
的东西里找垃圾”。不同的环境用不同的方式,这样做完全是为了提高效率。“停止—复制”的意
思是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会被暂停。有
人将垃圾回收视为低优先级的后台进程,而事实上并不是这样,当可用内存数量比较低的时候,
Sun版本的垃圾回收器就会暂停运行程序。同样,“标记-清扫”工作也必须在程序暂停的情况下才
能进行。
【注】 在java虚拟机中,内存分配是以较大的块为单位的。每个块内都用相应的代数
(generation count)来记录它是否还存活。代数随着引用的次数而增加。垃圾回收器将对上次回
收动作之后的新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进
行完整的清理动作——大型对象仍然不会被复制(只是代数增加),内涵小型对象的那些块则被复制
并整理。Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到
“标记—清扫”方式;同样,java虚拟机会追踪“标记—清扫”的效果,要是堆空间出现很多碎片
,就会切换到“停止—复制”方式。这就是“自适应”技术。
所以,Java垃圾回收器是一种“自适应的、分代的、停止—复制、标记-清扫”式的垃圾回收器。
【注】文章内大部分为网上摘录,地址较分散,作者也多次参考thinking in java的描述。总结
得甚是不足。如有高见,望不吝赐教。
|