walterwing  
日历
<2008年11月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
统计
  • 随笔 - 12
  • 文章 - 1
  • 评论 - 7
  • 引用 - 0

导航

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜

 

本篇内容主要转载自http://blog.csdn.net/calvinxiu/archive/2007/05/18/1614473.aspx,作者“江南白衣”

结合自身的学习,加入了《Thinking in Java 3rd Edition》中的部份相关内容


 


一. 引子

首先需要明确的一点是:Java中的所有对象(基本类型除外)都在堆上进行分配。

然而,Java语言的速度并不比其他那些在堆栈上分配空间的语言慢,其原因就在于Java的垃圾回收机制对于对象的创建具有非常明显的效果。

我们可以把C++的对想像成一个院子,里面每个对象都负责管理自己的底盘。一段时间以后,对象可能被销毁,但地盘必须被重用。

而Java中的堆更像一个传送带:你每分配一个对象,它就往前移动一格。这意味着对象存储空间的分配速度非常快。Java的“堆指针”只是简单地移动到尚未分配的区域,其效率比得上C++在堆栈上分配空间的效率。当然,实际过程中还存在诸如簿记工作的少量额外开销,但不会有像查找空间这样的大动作。

当然,Java中的堆并非完全像传送带那样工作。要真是那样的话,势必会导致频繁的内存页面调度(这将极大影响性能),并最终耗尽资源。

其中的秘密在于垃圾回收器的介入。当它工作时,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。

Java通过垃圾回收期对对象重新排列,从而实现了一种高速的、有无限空间可分配的堆模型。

二. 垃圾回收算法

1. 引用计数

首先介绍一种最直观最简单但却相当不实用(实际上也并没有被JVM采用)的回收算法——“引用计数”。我们介绍它是为了让大家对垃圾回收有个初步的概念,再通过与其他算法的对比,了解到其他算法的精华与优越性。

所谓“引用计数”,是指每个对象都有一格引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被设置为null时,引用计数减1。虽然管理引用计数的开销不大,但需要在整个生命周期中持续地开销。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时,就释放其占用的空间

这个算法除了低效外,还有个致命的缺陷:如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为零”的情况。对垃圾回收器而言,定位这样存在交互引用的对象组所需的工作量极大。

 

2. 理论依据

在正式介绍JVM中常用的几种垃圾回收算法之前,我们先来看一下JVM判断待回收对象的基本思想:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果你从堆栈和静态存储区开始,遍历所有引用,就能找到所有“活”的对象。

即对于发现的每个引用,你必须追踪它所引用的对象,然后是此对象包含的所有的引用,如此反复的执行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。你所访问过的所有对象必须都是“活”的。

注意,这就解决了“存在交互引用的整体对象”的问题,这些对象根本不会被发现,因此也就被自动回收了。

3. “停止——复制”算法

“停止——复制”算法是本篇将要介绍的三种JVM垃圾回收算法之一。顾名思义,这个算法需要先暂停程序的运行(因此它不属于后台回收模式),然后将所有“活”的对象从当前堆(堆A)复制到另一个堆(堆B),然后一次性回收整个堆A。

该算法的优点在于:当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑队列,然后就可以按照前述方法简单、直接地分配新空间了。

该算法主要有三个缺点:

缺点1:需要两个堆,然后需要在两个堆之间来回倒腾,从而使得维护比实际需要多一倍的空间。
缺点2:复制。当程序进入稳定状态后,可能只会产生少量的垃圾,甚至没有垃圾。尽管如此,该算法仍然会将所有内存自一处复制到另外一处,这很浪费。
缺点3:需要暂停程序的运行。当需要操作的堆空间较大时,耗费的时间是很可观的。

4. “标记——清扫”算法

“标记——清扫”算法主要适用于垃圾较少的情况。

该算法同样是要找出所有“活”的对象。每当它找到一个“活”对象,就会给对象设一个标记,这个过程中不会回收任何对象。只有全部标记工作完成时,清楚动作才会开始。在清除过程中,再次遍历整个内存区域,把所有没有标记的对象进行回收处理。

相对于“停止——复制”算法,“标记——清扫”算法具有如下优点:

优点1:支持用户线程与垃圾收集线程并发执行(后台回收模式),一开始会很短暂的停止一次所有线程来开始初始标记根对象,然后标记线程与应用线程与应用线程一起并发运行,最后又很短的暂停一次,多线程并行地重新标记之前可能因为并发而漏掉的对象,然后就开始与应用程序的并发清除过程。可见,最长的两个遍历过程都是与应用程序并发执行的,比“停止——复制”算法改进很多

优点2:当垃圾较少时,运行效率要比“停止——复制”方法高很多

但该算法也有其自身的缺点:

缺点:在清除过程中,释放没有被标记的对象,导致剩下的堆空间不是连续的,产生很多碎片。

5. “标记——整理”算法

综合了上述两种的做法和优点,先标记活跃对象,然后将其合并成较大的内存块


三. 分代

分代是Java垃圾收集的一大亮点,根据对象的生命周期长短,把堆分为3个代:Young,Old和Permanent,根据不同代的特点采用不同的收集算法,扬长避短也。

1. Young(Nursery),年轻代

研究表明大部分对象都是朝生暮死,随生随灭的。因此所有收集器都为年轻代选择了复制算法。

复制算法优点是只访问活跃对象,缺点是复制成本高。因为年轻代只有少量的对象能熬到垃圾收集,因此只需少量的复制成本。而且复制收集器只访问活跃对象,对那些占了最大比率的死对象视而不见,充分发挥了它遍历空间成本低的优点。

Young的默认值为4M,随堆内存增大,约为1/15,JVM会根据情况动态管理其大小变化。

-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,但实际上young启动时远低于这个比率?如果信不过JVM,也可以用-Xmn硬性规定其大小,有文档推荐设为Heap总大小的1/4。

Young里面又分为3个区域,一个Eden,所有新建对象都会存在于该区,两个Survivor区,用来实施复制算法。每次复制就是将Eden和第一块Survior的活对象复制到第2块,然后清空Eden与第一块Survior。Eden与Survivor的比例由-XX:SurvivorRatio=设置,默认为32。Survivio大了会浪费,小了的话,会使一些年轻对象潜逃到老人区,引起老人区的不安,但这个参数对性能并不重要。

2. Old(Tenured),年老代

年轻代的对象如果能够挺过数次收集,就会进入老人区。老人区使用标记整理算法。因为老人区的对象都没那么容易死的,采用复制算法就要反复的复制对象,很不合算,只好采用标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。

-XX:MaxTenuringThreshold=设置熬过年轻代多少次收集后移入老人区,CMS中默认为0,熬过第一次GC就转入,可以用-XX:+PrintTenuringDistribution查看。

3. Permanent,持久代

装载Class信息等基础数据,默认64M,如果是类很多很多的服务程序,需要加大其设置-XX:MaxPermSize=,否则它满了之后会引起fullgc()或Out of Memory。 注意Spring,Hibernate这类喜欢AOP动态生成类的框架需要更多的持久代内存

4. minor/major collection

每个代满了之后都会促发collection,(另外Concurrent Low Pause Collector默认在老人区68%的时候促发)。

GC用较高的频率对young进行扫描和回收,这种叫做minor collection

而因为成本关系对Old的检查回收频率要低很多,同时对Young和Old的收集称为major collection
   
System.gc()会引发major collection,使用-XX:+DisableExplicitGC禁止它,或设为CMS并发-XX:+ExplicitGCInvokesConcurrent

5. 小结

Young  -- 复制算法

Old(Tenured)  -- 标记清除/标记整理算法

四. 收集器

1.古老的串行收集器(Serial Collector)

使用 -XX:+UseSerialGC,策略为年轻代串行复制,年老代串行标记整理。

2.吞吐量优先的并行收集器(Throughput Collector)

使用 -XX:+UseParallelGC ,也是JDK5 -server的默认值。策略为:

    1).年轻代暂停应用程序,多个垃圾收集线程并行的复制收集,线程数默认为CPU个数,CPU很多时,可用–XX:ParallelGCThreads=减少线程数。
    2).年老代暂停应用程序,与串行收集器一样,单垃圾收集线程标记整理。

   
所以需要2+的CPU时才会优于串行收集器,适用于后台处理,科学计算。

可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 来调整GC的时间。

3.暂停时间优先的并发收集器(Concurrent Low Pause Collector-CMS)

使用-XX:+UseConcMarkSweepGC,策略为:

    1).年轻代同样是暂停应用程序,多个垃圾收集线程并行的复制收集。
    2).年老代则只有两次短暂停,其他时间应用程序与收集线程并发的清除。

注意并行与并发的区别:并行指多条垃圾收集线程并行;并发指用户线程与垃圾收集线程并发,程序在继续运行,而垃圾收集程序运行于另一个个CPU上


五. 其他

Java虚拟机中有许多附加技术用以提升速度。尤其是与加载器操作有关的,被称为“即时”(Just-In-Time,JIT)编译的技术。这种技术可以把程序全部或部份翻译成本地机器码(这本来是Java虚拟机的工作),程序运行速度因此得以提升。

当需要装载某个类(通常是在你为该类创建第一个对象)时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择:

一种是就让即使编译器编译所有代码。但这个做法有两个缺陷:这种加载动作散落在整个生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。

另一种做法称为“惰性编译(lazy evaluation)”,意思是即使编译器只在必要的时候才编译代码。这样,从不会被执行的代码也许压根就不会被JIT所编译。JDK 1.4中的Java HotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度越快。

posted on 2008-11-02 22:43 This is Wing 阅读(501) 评论(0)  编辑  收藏 所属分类: Java基础

只有注册用户登录后才能发表评论。


网站导航:
 
 
Copyright © This is Wing Powered by: 博客园 模板提供:沪江博客