华山论剑之jvm03 JVM 垃圾回收介绍

来源:互联网 发布:钣金展开软件下载 编辑:程序博客网 时间:2024/05/16 19:45

JVM垃圾回收介绍

垃圾回收算法


  垃圾回收算法的演进思路,  引用计数- >标记-清除 -> 复制-> 标记-整理 -> 分带算法. 
  
  引用计数(Reference Counting):
    比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
  
  标记-清除(Mark-Sweep): 
     此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
  
  复制(Copying)
      此算法把内存空间划分为两个相等的区域,每次只是用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题??。当然,此算法的缺点也是比较明显的,就是需要两倍内存空间。
   
  
   标记-整理(Mark-Compact)
      此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两个阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆中的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

问题思考

如何区分垃圾


       上面说到的“引用计数”法,通过统计控制生成对象和删除对象是的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象,那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的,上面分析的堆和栈的区别,其中栈是真正进行程序执行的地方,所以要获取哪些对象正在被使用,则需要从java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

        同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其它对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收,而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

        因此,垃圾回收的起点是一些根对象(java栈、静态变量、寄存器...)。而最简单的java栈就是java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式。


 如何处理碎片

        由于不同java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中 ,“复制”方式和“标记-整理”方式,都可以解决碎片的问题。

        如何解决同时存在的对象创建和对象回收问题

        垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决二者矛盾的方式。
        但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,相应应用暂停的时间也会相应的增大。一些相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决,以后研究的重点!!!!!!。

为什么要分代

        分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同声明周期的对象可以采取不同的收集方式,以便提高回收效率。

        在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

        是想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是它们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代



如图所示:

         虚拟机中共划分了三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)

         其中持久代主要存放的是java类的类信息,与垃圾收集要收集的java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

         年轻代:

         所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

         年老代:

         在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

         持久代:


         用于存放静态文件,如java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize = <N> 进行设置。

什么情况下触发垃圾回收

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC 和 Full GC

        Scavenge GC

        一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

         Full GC

         对整个堆进行整理,包括Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。

有如下原因可能导致Full GC:
         . 年老代(Tenured)被写满

         . 持久代(Perm)被写满

         . System.gc()被显式调用

         . 上一次GC之后Heap的各域分配策略动态变化

垃圾回收器介绍

    串行 -> 并行 -> 并发.    串行,并行工作原理:  停止用户应用, 进行垃圾回收, 之后再重新启动用户应用.  串行 并行的区别在于串行使用单线程进行垃圾回收. 并行在于使用多线程进行垃圾回收.   并发可以保证大部分时间用户线程不用停止工作, ,垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用.  并发由于垃圾收集和用户线程同时工作, 不适合用于新生代, 因为新生代产生垃圾的速度太快. 

1: 串行收集器.   
    特点:      使用单线程进行垃圾回收. 使用 参数:  -XX:+UseSerialGC打开.     新生代, 老年代都是用串行回收.   新生代使用复制算法  老年代使用标记-压缩算法. 
 
2: 并行收集器 ParNew  
      特点:  专门针对新生代的收集器。 使用参数 -XX:+UseParNewGC .  使用多线程进行垃圾回收.新生代使用复制算法     老年代使用标记-压缩算法. 可以使用  -XX:ParallelGCThreads 限制线程数量 .

3: 并行收集器  Parallel
特点:  类似ParNew.  更加关注吞吐量. 使用  XX:+UseParallelGC  使用Parallel收集器+ 老年代串行; 使用 -XX:+UseParallelOldGC 使用Parallel收集器+ 并行老年代. 另外连个参数: -XX:MaxGCPauseMills  最大停顿时间,单位毫秒, GC尽力保证回收时间不超过设定值. -XX:GCTimeRatio 0-100的取值范围 ,垃圾收集时间占总时间的比,默认99,即最大允许1%时间做GC.  这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优.

4: CMS 收集器 :Concurrent Mark Sweep:
        特点 使用 -XX:+UseConcMarkSweepGC 打开. 标记-清除算法. 老年代收集器.  可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用. 工作原理:





 初始标记: 根可以直接关联到的对象,速度快
并发标记:  标记全部对象. 
重新标记:  由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
并发清理: 基于标记结果,直接清理对象

       并发收集器主要减少年老代的暂停时间,它在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停。在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作

        并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用 k/N 个可用处理器进行回收,一般情况下 1 <= k <= N / 4。
        在只有一个处理器的主机上使用并发收集器,设置为 incremental mode 模式也可获得较短的停顿时间。

        浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。


        Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。


        启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置 -XX:CMSInitiatingOccupancyFraction = <N> 指定还有多少剩余堆是开始执行并发收集。

三个相关参数
        -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理  整理过程是独占的,会引起停顿时间变长
       -XX:+CMSFullGCsBeforeCompaction   设置进行几次Full GC后,进行一次碎片整理
       -XX:ParallelCMSThreads  设定CMS的线程数量

GC 参数整理


JVM 参数;

  追踪参数:   
   -- 打印GC简要信息:
     -verbose:gc  -- 打开GC追踪
-XX:printGC   - 打印GC的简要信息
-XX:+PrintGCDetails  --打印GC详细信息
-XX:+PrintGCTimeStamps  --打印GC发生的时间戳
-X:loggc:log/gc.log     -- 指定gc log的位置,帮助研发人员定位问题。
-XX:+PrintHeapAtGC     -- 每一次GC后,都打印堆信息
-XX:+TraceClassLoading  -- 监控类的加载
-XX:+PrintClassHistogram  -- 按下Ctrl+Break后,打印类的信息


  堆的配置:
    -Xmx –Xms         --指定最大堆和最小堆
-Xmn              -设置新生代大小 , 固定的大小, 如:  32G
-XX:NewRatio      -- 新生代(eden+2*s)和老年代(不包含永久区)的比值 ,4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
-XX:SurvivorRatio  -- 设置两个Survivor区和eden的比,8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
-XX:+HeapDumpOnOutOfMemoryError   --OOM时导出堆到文件
-XX:+HeapDumpPath       --导出OOM的路径
-XX:OnOutOfMemoryError  --在OOM时,执行一个脚本
例如: "-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p“
当程序OOM时,在D:/a.txt中将会生成线程的dump
可以在OOM时,发送邮件,甚至是重启程序


堆分配总结:
  根据实际事情调整新生代和幸存区的大小
  官方推荐新生代占堆的3/8
  幸存区占新生代的1/10
  在OOM时,记得Dump出堆,确保可以排查现场问题




 永久区分配参数:
   -XX:PermSize  -XX:MaxPermSize  设置永久区的初始空间和最大空间
 栈大小的分配:
   -Xss  通常只有几百K 决定了函数调用的深度  每个线程都有独立的栈空间  局部变量、参数 分配在栈上



GC 回收器相关参数:
    -XX:+UseSerialGC:在新生代和老年代使用串行收集器
    -XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
    -XX:NewRatio:新生代和老年代的比
    -XX:+UseParNewGC:在新生代使用并行收集器
    -XX:+UseParallelGC :新生代使用并行回收收集器
    -XX:+UseParallelOldGC:老年代使用并行回收收集器
    -XX:ParallelGCThreads:设置用于垃圾回收的线程数
    -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
    -XX:ParallelCMSThreads:设定CMS的线程数量
   -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
   -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
   -XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
   -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
   -XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
   -XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收


典型设置示例


 堆大小设置 年轻代的设置很关键

        JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bit 还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64位操作系统对内存无限制。在Windows Server 2003系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

         典型设置:

          java -Xmx3550m  -Xms3550m -Xmn2g  -Xss128k

          -Xmx3550m:设置JVM最大可用内存为3550m。
          -Xms3550m:设置JVM初始内存为3550m。此值可以设置与 -Xmx 相同,以避免每次垃圾回收完成后JVM重新分配内存。
          -Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
          -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256k。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。


         java  -Xmx3550m  -Xms3550m  -Xss128k  -XX:NewRatio=4  -XX:SurvivorRatio=4  -XX:MaxPermSize=16m  -XX:MaxTenuringThreshold=0

         -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。
         -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
         -XX:MaxPermSize=16m:设置持久代大小为16m。
         -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代被回收的概率。

         回收器选择

         JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动的时候加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

        吞吐量优先的并行收集器

        如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学计算和后台处理等。

        典型配置:

            java  -Xmx3800m  -Xms3800m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:ParallelGCThreads=20
          -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
          -XX:+ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

          java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
          -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

 
            java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
           -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

            java  -Xmx3550m  -Xms3550m  -Xmn2g  -Xss128k  -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100  -XX:+UseAdaptiveSizePolicy
           -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,此值建议使用并行收集器时,一直打开。 

        响应时间优先的并发收集器

        如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

        典型配置:


        java  -Xmx3550m  -Xms3550  -Xmn2g  -Xss128k  -XX:ParallelGCThreads=20  -XX:+UseConcMarkSweepGC  -XX:+UseParNewGC
        -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
        -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。           


        java  -Xmx3550m  -Xms3550  -Xmn2g  -Xss128k  -XX:+UseConcMarkSweepGC  -XX:CMSFullGCsBeforeCompaction=5  -XX:+UseCMSCompactAtFullCollection 
        -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
        -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。

       常见配置汇总

       堆设置


           -Xms:初始堆大小

           -Xmx:最大堆大小

           -XX:NewSize=n:设置年轻代大小

           -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,
        -XX:SurvivorRatio=3 表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5。

           -XX:MaxPermSize=n:设置持久代大小


        收集器设置


           -XX:+UseSerialGC:设置串行收集器


           -XX:+UseParallelGC:设置并行收集器


           -XX:+UseParalledlOldGC:设置并行年老代收集器


           -XX:+UseConcMarkSweepGC:设置并发收集器


        垃圾回收统计信息


           -XX:+PrintGC


           -XX:+PrintGCDetails


           -XX:+PrintGCTimeStamps


           -Xloggc:filename


         并行收集器设置


           -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。


           -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间


           -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+N)


         并发收集器设置


           -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。


           -XX:+ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


原创粉丝点击