Java垃圾回收浅谈

什么是Java垃圾回收机制?

Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。

需要注意的是:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身,很多人来我公司面试时,我都会问这个问题的,70%以上的人回答的含义是回收对象,实际上这是不正确的。

System.gc()

Runtime.getRuntime().gc()

上面的方法调用时用于显式通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始发生动作这同样是不可预料的,这和抢占式的线程在发生作用时的原理一样。

Java垃圾回收器如何工作?

在你以前所用过的程序语言中,如果在堆上分配对象的代价十分高昂,你自然会觉得Java中所有对象(基本类型除外)都在堆上分配的方式也非常的高昂。然而,垃圾回收器对于对象的创建,却具有明显的效果。听起来很奇怪——存储空间的释放竟然会影响存储空间的分配——但这确实是某些Java虚拟机工作的方式。这意味着,Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。

打个比方,你可以把C++里的堆想象成一个院子,里面的每个对象都负责管理自己的地盘。一段时间后,对象可以被销毁,但地盘必须被重用。在某些Java虚拟机中,堆的实现截然不同:它更像是一个传送带,你每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中还存在诸如薄记工作的少量额外开销,但不会有像查找可用空间这样的大动作。

也许你已经意识到了,Java中的堆未必完全像传送带那样去工作。要真是那样的话,势必会导致频繁的内存页调度(这将极大影响性能),并最终耗尽资源。其中的秘密在于垃圾回收器的介入。当它工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。

你得更好地理解不同垃圾回收器模式的工作机制,才能明白上述方式如何工作。“引用记数”是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用记数器,当有引用连接至对象的时候,引用计数加1。当引用离开作用域或被置为null时,引用计数器减1。虽然管理引用记数的开销不大,但需要在整个程序生命周期中持续地开销。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器而言,定位这样存在交互引用的对象组所需的工作量极大。引用记数常用来说明收集的工作方式,似乎从未被应用于任何一种Java虚拟机实现中。

在一些更快的模式中,垃圾回收器并非基于引用记数技术。它们依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果你从堆栈和静态存储区之中的引用,遍历所有的引用,就能找到所有“活”对象。对于发现的每个引用,你必须追踪它所引用的对象,然后是此对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问过的对象必须是“活”的。注意,这就解决了“存在交互引用的整体对象”的问题,这些对象根本不会被发现,因此也就被自动回收了。

在这种方式下,Java虚拟机将采用一种“自适应”的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的Java虚拟机实现。有一种作法名为“停止——复制”。这意味着,先暂停程序的运行,(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全部都是垃圾。当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单、直接地分配新空间了。

当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于堆或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到。你可以想象有个表格,将旧地址映射到新地址,这样就可以在遍历的同时进行修改了。

对于这种所谓的“复制式回收器”而言,有两个原因会降低效率。首先,你得有两个堆,然后你得在这两个分离的堆之间来回倒腾,从而得维护比实际需要多一倍的空间。某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块比较大的内存,复制动作在这些大块内存之间。

第二个问题在于复制。你的程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制式回收器仍然会将所有内存自一处复制到另一处,这很浪费。为了避免这种情形,一些Java虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种工作模式(此即”自适应“)。这种模式称为”标记——清扫“,Sun公司早期版本的Java虚拟机使用了这种技术。对一般用途而言,”标记——清扫“方式速度相当慢,但是当你知道你只会产生少量垃圾甚至不会产生垃圾时,它的速度就很快了。

”标记——清扫“所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所以存活的对象。每当它找到一个存活对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成的时候,清除动作才会开始。在清处过程中,没有标记的对象将被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器是希望得到连续空间的话,就得重新整理剩下的对象。

”停止——复制“的意思是这种垃圾回收方式不是在后台进行的;相反,垃圾回收动作的发生的同事,程序将被暂停。在Sun公司的文档中你会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但事实上垃圾回收器并非以这种方式实现——至少Sun公司早期版本的Java虚拟机中并非如此。当可用内存数量较低时,Sun版中的垃圾回收器才会被激活,”标记——清扫“工作也必须在程序暂停的情况下才能进行。

如前文所述,这里讨论的Java虚拟机,内存分配单位是较大的”块“。如果对象较大,它会占用单独的块。严格来说,”停止——复制“要求你在释放旧有对象之前,必须先把所有存活对象从旧堆复制到新堆,这将导致大量内存复制行为。有了块之后,垃圾回收器在回收的时候就可以往废弃的块里拷贝对象了。每个块都用相应的”代数“记录它是否还存活。通常,如果块在某处被引用,其代数会增加;垃圾回收器将对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清除动作——大型对象仍然不会被复制(只是其代数会增加),内含小型对象的那些块被复制并整理。Java虚拟机会进行监控,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到”标记——清扫“方式;同样,Java虚拟机会注意”标记——清扫“的效果,要是堆空间出现很多碎片,就会切换回“停止——复制”方式。这就是“自适应”技术。你可以给它个啰嗦的称呼:“自适应的、分代的、停止——复制、标记——清扫”式垃圾回收器。

Java虚拟机中许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”编译的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得意提升。当需要装载某个类(通常是在你为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可以选择。一种就是让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。另一种做法称为“惰性编译”,意思是即时编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许就压根不会被“即时”编译所编译。新版JDK中的Java HotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。

此文段摘自《Thinking in Java》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值