垃圾回收器如何工作
垃圾回收器是如何工作的?首先要明确几点:
-
Java是在堆上为对象分配空间的
- 垃圾回收器只跟内存有关,
- 只有内存用完了,垃圾回收器起作用,垃圾回收器才会被激活。
- 对象可能不被垃圾回收。
只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,垃圾回收器一直没有释放你创建的对象的存储空间,则随着程序退出,那些资源也会交还给操作系统。因为垃圾回收本身也要开销,如果不使用它,那就不用支付这部分开销了。
1、JVM内存模型
1.根据Java虚拟机规范,JVM将内存划分为:
- New(年轻代)
- Tenured(年老代)
- 永久代(Perm)
其中New和Tenured属于堆内存,堆内存会从JVM启动参数(-Xmx:3G)指定的内存中分配,Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。
- 年轻代(New):年轻代用来存放JVM刚分配的Java对象
- 年老代(Tenured):年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代
- 永久代(Perm):永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间。
New又分为几个部分:
- Eden:Eden用来存放JVM刚分配的对象
- Survivor1
- Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。
2.垃圾回收算法
垃圾回收算法可以分为三类,都基于标记-清除(复制)算法:
JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如如果机器多于1个核,会对年轻代选择并行算法,
- Serial算法(单线程)
- 并行算法
- 并发算法
3、垃圾回收动作何时执行?
- 当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
- 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
- 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
一、Java分配对象的方式
在垃圾回收器回收垃圾之前,我们先来了解一下Java分配对象的方式,Java的堆更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度相当快。Java的“堆指针”只是简单地移动到尚未分配的领域。也就是说,分配空间的时候,“堆指针”只管依次往前移动而不管后面的对象是否还要被释放掉。如果可用内存耗尽之前程序就退出就再好不过了,这样的话垃圾回收器压根就不会被激活。由于“堆指针”只管依次往前移动,那么你肯定会想,总有一天内存会被耗尽,垃圾回收器就开始靠回收对象释放内存。
二、怎么判断某个对象该被回收呢?
答案就是当堆栈或静态存储区没有对这个对象的引用时,就表示程序(员)对这个对象没有兴趣了,它就应该被回收了。有两种方法来知道这个对象有没有被引用:第一种是“引用计数法”,;第二种是遍历堆栈或静态存储区的引用找对象。Java采用的是后者.
2.1引用计数法:
每个对象都有一个引用计数器,当有引用连接至对象时,引用计数加1,当引用离开作用域或者被置null时候,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期里持续发生。垃圾回收器会在有全部对象的列表上遍历,当发现某一个对象的引用计数为0时候,就释放其占用的空间。(但是引用计数模式经常会在数值为0时立即释放对象)这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对于垃圾回收器而言,定位这样的交互自引用的对象组所需的工作量极大。
2.2.Java的垃圾回收技术依据的思想:
对于任何‘活’的对象,一定能够追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有的‘活的对象’。对于发现的每个引用,必须追踪他所引用的对象,然后是此对象包含的所有的引用。如此反复进行,直到‘根源于堆栈和静态存储区的引用’所形成的网络全部被访问为止。你访问过的对象都是活的,这就解决了‘交互自引用的对象组的问题’——这种现象根本不会被发现,因此也就被自动回收了。
三、‘自适应的、分代的、停止-复制、标记-清扫’垃圾回收技术
Java虚拟机采用一种“自适应的,分代的、停止-复制、标记-清扫”的垃圾回收技术。
停止-复制
理论上是先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。当对象被复制到新堆上时,它们是一个挨着一个的,所以新堆保持紧凑排列(这也是为什么分配对象的时候“堆指针”只管依次往前移动)。然后就可以按前述方法简单、直接地分配内存了。
当把对象从一处搬到另一处时,所有指向它的那些引用都必须修正。位于栈或者静态存储区的的引用可以直接被修正,但可能还有其他指向这些对象的引用。它们在遍历的过程中才能被找到、
对于这种所谓的复制式回收器而言,效率会降低,这有两个原因:
首先:得有两个堆,然后得在这两个分离的堆之间来回倒腾,从而得维护比实际需要多一倍的空间。
其次:产生的第二个问题在于复制。进入稳定状态之后,可能只会产生少量的垃圾,甚至没有垃圾,尽管如此,垃圾回收器仍然会将所有内存从一处复制到另一处,这很浪费。
标记-清扫
对于原因2,一些Java虚拟机会进行检查,演示没有新垃圾产生,就会转换到另外一种工作模式。这种模式成为标记—清扫。它的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象一个标记。这个过程中不会回收任何对象。只有全部标记完成时,没有标记的对象将被释放,不会发生任何复制工作,所以剩下的堆空间是不连续的,然后垃圾回收器重新整理剩余的对象,使它们是连续排列的。
分代的
对于原因一,某些Java虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。内存分配是以较大的“块”为单位的。有了块之后,垃圾回收器就可以不往堆里拷贝对象了,直接就可以往废弃的块里拷贝对象了。内存分配以较大的“块”为单位。如果对象较大,它会占用单独的块。每个块有相应的代数(generation count)来记录它是否还存活。通常如果块在某处被引用,其代数会增加;垃圾回收器对上次回收动作之后新分配的块进行整理。这对出来大量短命的临时对象很有帮助。垃圾回收器会定期的进行完整的清理——大型对象仍然不会复制(只是其代数会增加),内含小型对象的那些块则会被复制并整理。
自适应
当垃圾回收器第一次启动时,它执行的是“停止-复制”,因为这个时刻内存有太多的垃圾。然后Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;同样,Java虚拟机会跟踪“标记-清扫”效果,要是堆空间出现很多碎片,就会切换到“停止-复制”方式。这就是所谓的“自适应”技术。
其实仔细想一下,“停止-复制”和“标记-清扫”无非就是:“在大量的垃圾中找干净的东西和在大量干净的东西里找垃圾”。不同的环境用不同的方式,这样做完全是为了提高效率,要知道,无论哪种方式,Java都会先暂停程序的运行,所以,垃圾回收器的效率其实是很低的。
参考:
http://www.cnblogs.com/chen77716/archive/2010/06/26/2130807.html