菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
313
0

GC算法基础

原创
05/13 14:22
阅读数 26586

寻找垃圾对象的算法:1. 引用计数(无法处理循环引用) 2. 根寻法(被广泛引用在gc算法中)

清理垃圾的算法: 1. 标记复制  2. 标记清理  3. 标记整理

分代算法的好处:

   1. 分代处理,可以减少一次处理的内存大小,减少停顿时间。  

   2. 不同的代有不同的特点,再加上有针对性的gc算法和代码优化,整体的性能更好:

        年轻代特点是分配快,对象存活时间短,所以采用标记复制时间消耗不会太高,效率高。优化手段就是减少分配需求的速度,减少对象的存活时间,这样使得youngGc频率和停顿都降低,也减少了老年代的压力,是很有效的优化手段。

        老年代的特点是内存较大,对象存活时间久,所以相对来说比较年轻代要复杂。所以有各种各样不同的gc算法来对老年代进行处理,考量点主要是吞吐量和停顿时间。

       老年代担任的一个很重要的角色是给youngGC做担保,保证对象的晋升可以成功。所以这里的一个重要的指标是,FullGC发生的频率。FullGC的发生,一般是由于老年代的内存清理速度更不上晋升的速度,或者是老年代的内存碎片,所以保证对象的存活时间尽可能的短是有很多好处的。

 

GC算法: 

1. 串行GC

2. ThroughPutGC(并行GC)—目的:提高吞吐量

3. ConcurrentGC— 目的:降低停顿

 

垃圾收集器:

Serial收集器:

   年轻代的收集器+串行GC+标记复制

ParNew收集器:

   年轻代的收集器+并行GC+标记复制

Parallel Scavenge收集器:

   年轻代的收集器+并行GC+标记复制。 和ParNew的区别可能是引进了用户设定目标,gc自己调整大小的机制

Serial Old收集器:

   老年代的收集器+串行GC+标记整理

Parallel Old收集器:

   老年代的收集器+并行GC+标记整理

CMS收集器:

   老年代的收集器+并发GC+标记清理

G1收集器:

   年轻代+老年代的收集器

   年轻代=并行GC+标记复制

   老年代=并发GC+标记整理

 

GC算法选择:

  Gc算法的选择考虑的方向主要是停顿时间和吞吐量俩个方面。需要根据程序的特征,CPU资源来进行权衡。

  1. 先从吞吐量来说,其实考虑的主要是可用的cpu资源有多少(即有多少空闲的cpu资源)。如果空闲的cpu资源较少,使用throughput类的gc会更好,因为它本身的收集效率就比并发的gc好。而并发的gc算法会和应用程序抢夺cpu资源。就会造成吞吐量下降。而如果空闲的cpu资源很多,那么并发的gc可以利用这些空闲的cpu来并发的完成gc,而且对应用程序的影响是很小的。反正那些cpu空着也是空着。如果这时使用throughput收集器,这些空闲的cpu会被白白浪费,而且还会stw来独占cpu进行gc,如果gc本身无法完全利用这些cpu资源,而且还会影响应用程序,就得不偿失了。

  2. 在衡量停顿时间上,一般来说,throughput的收集器的90%的响应时间都要比并发gc的快,因为90%的时间是没有gc的,只有在发生full-gc时才会导致响应时间过长。 但是,当full-gc发生的更频繁的时候,这个百分比也就会下降。当然,当发生这种情况是,并发gc也许也会难以避免full-gc。

 

 

CMS和G1的选择:

   一般认为,当堆比较大时(大于4G),使用G1比较好,否则CMS的性能会更好一点。原因在于,gc消耗的时间和堆的大小是有很大的关系的,比如标记需要扫描整个堆。但是G1对堆进行了进一步的划分,在清理周期中,可以只清理垃圾最多的一部分分区,所以在大堆的情况下,性能是比CMS要好一些。另外,由于G1是标记复制的算法,所以不容易造成内存碎片。 

   反过来,由于G1的算法和数据结构要更加复杂,所以它的内存和cpu的消耗也就更大。如果堆本身就比较小的话,使用G1会造成应用程序可以使用的堆大小会更小。

    

 

GC调优基础:

1. 调整堆的大小:

     A.堆过小,导致gc频率变高。 堆太大,导致单次gc的时间变长。所以,要通过调整对的大小来找到一个我们认为相对合适的目标。目标主要从停顿时间和吞吐量俩个方面来考虑。

     B.堆的大小,不要超过机器的物理内存,因为jvm对底层的swap并无感知,swap带来的gc停顿变长,可能会导致并发失效,从而导致full-gc

     C.-Xms4096m -Xmx4096m 这种最大和初始一样时,堆的大小就不会因为自适应调整而改变。当我们知道最合适的堆的大小时,这种设置是有好处的,因为jvm不需要再进行堆的大小计算和调整。但是当我们不知道合适的堆的大小应该是多少时,我觉得还是不要这么搞吧。

 

2. 调整代的大小:

    新生代太小,会导致youngGc频率高,对象晋升快,对老年代的压力增大。 新生代太大,新生代的gc时间可能会变长,对象晋升变慢,老年代的大小受限,可能会导致full-gc。

    老年代太小,可能会导致full-gc变多。老年代太大,可能会导致老年代的gc太慢。

    -XX:NewRatio=

    -XX:NewSize=

     -XmnN
    -XX:MaxNewSize=N

3. 调整永久代和元空间的大小:

     永久代和元空间也会触发fullgc,所以也需要关注一下大小是否合适。

 

4. 控制并发:

    gc的线程任务是计算密集型

    -XX:ParallelGCThreads=N 来控制并并GC的线程数,比如:

    a.使用-XX:+UseParallelGC收集新生代空间
    b.使用-XX:+UseParallelOldGC收集老年代空间

    c. CMS的“STW”阶段(不包括Full-GC)

    d. G1的“STW”阶段(不包括Full-GC)

    e.使用-XX:+UseParNewGC收集的新生代空间  

    f.使用-XX:+UseG1GC收集的新生代空间

5. 自适应调整:

    用户设定目标:响应时间和gc消耗时间占比 -XX:MaxGCPauseMillis=N 和 -XX:GCTimeRatio=N ,这俩个配置会同时影响年轻代和老年代

    自适应调整会调整堆的大小,新生代和老年代的大小来尽量适用目标值。如果堆的初始值和最大值一样,则自适应调整不会对堆的整体大小进行调整。如果堆,年轻代的初始值和最大值一样,则自适应就完全失效了,不过survivor还是会进行自适应调整。

   -XX:-UseAdaptiveSizePolicy  关闭自适应(默认是开)。 -XX:+PrintAdaptiveSizePolicy 在gc时会打印调整的信息。

   对精细化计算过的jvm内存比例,可以把各个区域的大小进行限定,这样可以减少堆自适应的消耗。  

 

 

Throughput收集器:

   Gc分为youngGc和fullGC。正常的FullGc不会对永久区进行回收,当回收区满时,会触发fullGc,这时会对永久区进行回收。

   调优:

     1.自适应调整:首先会调整堆的大小来达到目标停顿时间,然后慢慢增大堆的大小来尽可能达到目标吞吐量。然后又开始降低堆的大小来减少jvm的内存占用。目标值的设定一定要合理,太极端会导致极端的问题。

     2.调整并行线程数,因为是stw,而且gc的任务是计算密集型的,所以线程数根据cpu和计算密集型的公式就好。

 

CMS:

    CMS有三个gc动作:1. yooung区的gc(STW)  2.老年代的gc(并发gc+标记清除)  3.full-gc(串行full-gc)

    老年代的GC过程:  

  1. 初始标记(STW): 标记那些直接被Root对象引用的对象

                 2.并发标记: 并发的进行Root Tracing的过程,时间较长。无法处理浮动垃圾。

                 3.重新标记(STW): 主要是对步骤2中可能出现的遗漏进行补充,时间比1长,但是远比2短

                 4.并发清理:并发的进行垃圾的清理,不会进行内存压缩,所以可能会造成内存碎片

    一 YGC   89.853: [GC 89.853: [ParNew: 629120K->69888K(629120K), 0.1218970 secs]

  •                             1303940K->772142K(2027264K), 0.1220090 secs]
  •                         [Times: user=0.42 sys=0.02, real=0.12 secs]
  •                       user的时间表示cpu时间,real表示时钟的时间。对于并发gc,real时间一般比user时间少。
  •   二初始标记  89.976: [GC [1 CMS-initial-mark: 702254K(1398144K)]
  •                             772530K(2027264K), 0.0830120 secs]
  •                             [Times: user=0.08 sys=0.00, real=0.08 secs]

   三并发标记   90.059: [CMS-concurrent-mark-start]

                            90.887: [CMS-concurrent-mark: 0.823/0.828 secs]

                             [Times: user=1.11 sys=0.00, real=0.83 secs]

   四预清理       90.887: [CMS-concurrent-preclean-start]

                             90.892: [CMS-concurrent-preclean: 0.005/0.005 secs]

                             [Times: user=0.01 sys=0.00, real=0.01 secs]

   五重新标记    90.892: [CMS-concurrent-abortable-preclean-start]

                             92.392: [GC 92.393: [ParNew: 629120K->69888K(629120K), 0.1289040 secs]

                                          1331374K->803967K(2027264K), 0.1290200 secs]

                                          [Times: user=0.44 sys=0.01, real=0.12 secs]

                             94.473: [CMS-concurrent-abortable-preclean: 3.451/3.581 secs]

                                           [Times: user=5.03 sys=0.03, real=3.58 secs]

                            94.474: [GC[YG occupancy: 466937 K (629120 K)]

                                          94.474: [Rescan (parallel) , 0.1850000 secs]

                                          94.659: [weak refs processing, 0.0000370 secs]

                                          94.659: [scrub string table, 0.0011530 secs]

                                                       [1 CMS-remark: 734079K(1398144K)]

                                                       1201017K(2027264K), 0.1863430 secs]

                                         [Times: user=0.60 sys=0.01, real=0.18 secs]

                           这个过程执行了多个步骤,首先是可中断的预清理。由于jvm为了避免俩次连续的停顿(刚发生一次YGC后,立马发生重新标记),所以在发生YGC后,jvm预测下一个YGC发生的时间周期,在这个周期中间停止了可停顿预清理,然后开始了重新标记。

              这也意味了,老年代的并发周期和YGC是并发进行的。

     六并发清理阶段   94.661: [CMS-concurrent-sweep-start]

                                    95.223: [GC 95.223: [ParNew: 629120K->69888K(629120K), 0.1322530 secs]

                                                                       999428K->472094K(2027264K), 0.1323690 secs]

                                                                        [Times: user=0.43 sys=0.00, real=0.13 secs]

                                    95.474: [CMS-concurrent-sweep: 0.680/0.813 secs]

                                                  [Times: user=1.45 sys=0.00, real=0.82 secs]

                              这里日志表示,在并发清理阶段,发送了YGC,也表明老年代的并发周期和YGC是并发进行的。而且在并发周期中至少会发生一次YGC,就是可中断预清理的过程中。

      七并发重置阶段   95.474: [CMS-concurrent-reset-start]

                                     95.479: [CMS-concurrent-reset: 0.005/0.005 secs]

                                                   [Times: user=0.00 sys=0.00, real=0.00 secs]

                               这是并发周期的最后一个阶段,但是我们没法中日志中得知这个阶段回收了多少内存,只能从上下的GC日志中进行推测。

      八并发模式失败(concurrent mode failure)

                                     267.006: [GC 267.006: [ParNew: 629120K->629120K(629120K), 0.0000200 secs]

                                                    267.006: [CMS267.350: [CMS-concurrent-mark: 2.683/2.804 secs]

                                                    [Times: user=4.81 sys=0.02, real=2.80 secs]

                                                   (concurrent mode failure):

                                                  1378132K->1366755K(1398144K), 5.6213320 secs]

                                                    2007252K->1366755K(2027264K),

                                                 [CMS Perm : 57231K->57222K(95548K)], 5.6215150 secs]

                                                  [Times: user=5.63 sys=0.00, real=5.62 secs]

                                这是在发生YGC时,老年代没有足够的空间来容纳晋升的对象,就退化成了FULL-GC。这个过程是串行的,所以很慢。

         九晋升失败       6043.903: [GC 6043.903:

             [ParNew (promotion failed): 614254K->629120K(629120K), 0.1619839 secs]

             6044.217: [CMS: 1342523K->1336533K(2027264K), 30.7884210 secs]

             2004251K->1336533K(1398144K),

             [CMS Perm : 57231K->57231K(95548K)], 28.1361340 secs]

             [Times: user=28.13 sys=0.38, real=28.13 secs]

                                    这是在发生YGC后,而且JVM判断old区有足够的空间容纳晋升对象,但是在晋升的过程中,由于old区的内存碎片,导致的晋升失败,然后进行了FULL-GC. 由于这个过程是发送YGC失败导致的,所以耗时比并发模式失败还要多。

        十永久代或者元空间用尽导致的full-gc

                                            279.803: [Full GC 279.803:

                     [CMS: 88569K->68870K(1398144K), 0.6714090 secs]

                     558070K->68870K(2027264K),

                     [CMS Perm : 81919K->77654K(81920K)],

                     0.6716570 secs]

 

 

G1:  G1对堆内存进全部行了分区(region),一个代的空间里的分区并不是连续的。有些分区属于老年代,有些分区属于年轻代。年轻代的gc和其他回收器是一样的,只所以也进行分区是因为可以更方便的进行分区的大小调整。老年代的gc是对垃圾最多的分区进行清理,这样可以使用更少的时间来达到最好的清理效果。所以对于比较大的堆,G1的停顿时间是要比CMS好的。 G1的老年代清理是把一个region

      G1的4个操作:

      1.新生代垃圾的收集 :并行收集

      2.后台收集,并发周期:对垃圾最多的分区进行标记,期间至少会发生一次YGC

      3.混合式垃圾收集

      4.必要的full-gc 

 

      并发周期:

            a. 50.541: [GC pause (young) (initial-mark), 0.27767100 secs]

             [Eden: 1220M(1220M)->0B(1220M)

             Survivors: 144M->144M Heap: 3242M(4096M)->2093M(4096M)]

         [Times: user=1.02 sys=0.04, real=0.28 secs]

        并发周期的开始—初始化标记。这个动作是STW的,这里使用了YGC来进行STW,

    b. 50.819: [GC concurrent-root-region-scan-start] 

                     51.408: [GC concurrent-root-region-scan-end, 0.5890230]

扫描根分区,这个过程是并发进行的。但是,这个阶段中,不能进行YGC,如果这个时候触发了YGC,YGC必须等待这个动作的结束,然后再进行。这导致了YGC时间变长,意味着需要进行调优了,所以就会有下面这种日志。

    350.994: [GC pause (young)

              351.093: [GC concurrent-root-region-scan-end, 0.6100090]

              351.093: [GC concurrent-mark-start],

              0.37559600 secs]

    c.  350.994: [GC pause (young)

              351.093: [GC concurrent-root-region-scan-end, 0.6100090]

              351.093: [GC concurrent-mark-start],

              0.37559600 secs]

并发标记,这个过程是可以中断的,中间是可以有YGC并发进行的。

    d.   120.910: [GC remark 120.959:

              [GC ref-PRC, 0.0000890 secs], 0.0718990 secs]

              [Times: user=0.23 sys=0.01, real=0.08 secs]

      120.985: [GC cleanup 3510M->3434M(4096M), 0.0111040 secs]

              [Times: user=0.04 sys=0.00, real=0.01 secs]

重新标记和清理阶段。这俩个阶段是STW的,而且这个清理阶段只会清理很少的垃圾,主要还是对垃圾的标记。

  e.   120.996: [GC concurrent-cleanup-start]

               120.996: [GC concurrent-cleanup-end, 0.0004520]

一个并发清理,同样回收的垃圾很少。

       

        混合式垃圾收集周期:

           这个周期会进行YGC和老年代的并发清理。这是stw的

           a.  79.826: [GC pause (mixed), 0.26161600 secs]

     ....

        [Eden: 1222M(1222M)->0B(1220M)

           Survivors: 142M->144M Heap: 3200M(4096M)->1964M(4096M)]

        [Times: user=1.01 sys=0.00, real=0.26 secs]

          混合式垃圾回收周期会进行多轮,直到满足我们设定的指标。然后整个老年代的回收周期就结束了。

 

        FULL-GC的发生:

1. 并发模式失败:老年代开始了标记周期,但是在标记周期完成之前就被填满了。这种情况下,G1日志里会有放弃标记周期的日志:

                  51.408: [GC concurrent-mark-start]

         65.473: [Full GC 4095M->1395M(4096M), 6.1963770 secs]

          [Times: user=7.87 sys=0.00, real=6.20 secs]

         71.669: [GC concurrent-mark-abort]

2. 晋升失败:老年代已经开始了mix-gc周期,但是老年代在垃圾回收释放出足够的内存之前就被耗尽了。这种情况是mix-gc之后马上就有一次Full-Gc

      2226.224: [GC pause (mixed)

               2226.440: [SoftReference, 0 refs, 0.0000060 secs]

               2226.441: [WeakReference, 0 refs, 0.0000020 secs]

               2226.441: [FinalReference, 0 refs, 0.0000010 secs]

               2226.441: [PhantomReference, 0 refs, 0.0000010 secs]

               2226.441: [JNI Weak Reference, 0.0000030 secs]

                       (to-space exhausted), 0.2390040 secs]

       ....

           [Eden: 0.0B(400.0M)->0.0B(400.0M)

               Survivors: 0.0B->0.0B Heap: 2006.4M(2048.0M)->2006.4M(2048.0M)]

           [Times: user=1.70 sys=0.04, real=0.26 secs]

       2226.510: [Full GC (Allocation Failure)

               2227.519: [SoftReference, 4329 refs, 0.0005520 secs]

               2227.520: [WeakReference, 12646 refs, 0.0010510 secs]

               2227.521: [FinalReference, 7538 refs, 0.0005660 secs]

               2227.521: [PhantomReference, 168 refs, 0.0000120 secs]

               2227.521: [JNI Weak Reference, 0.0000020 secs]

                       2006M->907M(2048M), 4.1615450 secs]

           [Times: user=6.76 sys=0.01, real=4.16 secs]

3.疏散失败:在新生代的回收中,survivor区和老年代没有足够的内存来容纳晋升和存活的对象,这时会有一个比较特别的日志:

60.238: [GC pause (young) (to-space overflow), 0.41546900 secs]

4.巨型对象分配失败:没有什么好的办法来排查这种问题,如果出现了比较奇怪的full-gc,可以考虑这种情况。

 

发表评论

0/200
313 点赞
0 评论
收藏