JVM调优总结

来源:互联网 发布:不上班的23种活法 知乎 编辑:程序博客网 时间:2024/06/16 14:35

  • 堆内存模型
    • 传统堆模型
    • G1收集器下的堆模型
  • GC算法
      • 标记清除Mark-Sweep
      • 标记复制HotSpot新生代采用的算法
      • 标记整理老年代采用的算法
    • HotSpot中的GC
      • 新生代GC
        • Serail GC串行GC
        • ParNew GC并行GC
        • Parallel Scanvenge GC吞吐量优先GC
      • 老年代GC
        • Serail Old GC
        • Parallel Old GC
        • Concurrent Mark SweepCMS GC
        • G1 GC
  • JVM参数设置
  • 调优总结

堆内存模型

大家应该熟悉JVM的内存模型了,因为本文讲JVM的调优所以有必要复习一下JVM的堆内存模型.

传统堆模型

这里写图片描述

  • Young
    • Eden 存放新创建的对象
    • Survivor0 存放经过YGC还存活的对象
    • Survivor1 和S0一样
  • Old 存放Survivor区经过YGC n次后还存活的对象
  • Perm 永久代,存放Class等对象

为什么JVM要采用这么复杂的内存管理,直接放一块不好吗?之所以会采用分代的方式,完全是为了方便快速高效GC而设计。

G1收集器下的堆模型

G1堆模型

因为本文都是讲New Parallel 配合CMS GC的方式,所以不展开讨论G1,但是G1收集器是未来GC的方向,而且现在已经商业成熟了,感兴趣同学可以去详细了解

GC算法

标记清除(Mark-Sweep)

最基础的GC算法,首先标记,然后清除
特点:1.算法简单,2.效率低,2.会产生大量内存碎片

标记复制(HotSpot新生代采用的算法)

将内存分为两块,在GC时从一块复制到另一块
特点:1.算法简单且高效,2.会浪费二分之一的空间,3.在对象存活率低的时候高效,在对象存活率高的时候就变得低效

标记整理(老年代采用的算法)

老年代的对象存活率较高,复制算法非常耗时.针对这种情况,标记整理在标记清除上进行优化,让存活对象移到一端,然后从边界位置直接清理.

HotSpot中的GC

串行: 单线程,只有一个GC线程执行任务
并行: 多线程,多个GC线程执行任务,不能与用户线程同时进行
并发: 能与用户线程同时进行的GC

新生代GC

Serail GC(串行GC)

顾名思义,单线程作战,所以比较低效,优点是GC使用线程少,所以在client模式下是比较好的选择

ParNew GC(并行GC)

并行GC,对比串行GC即有多个线程同时执行GC任务

Parallel Scanvenge GC(吞吐量优先GC)

它的关注点在于如何达到一个可控制的吞吐量.

老年代GC

Serail Old GC

Parallel Old GC

Concurrent Mark Sweep(CMS GC)

我们现在最常用的老年代GC,他的目标就是尽量减少”Stop the world(所有用户线程终止)”的情况发生,尽可能的提高性能.

缺点:
1. 对CPU非常敏感,因为为了提高GC效率,会使用多线程进行收集,就占用了CPU的资源,导致在垃圾收集时,JVM的性能降低比较多,尤其是在CPU核心数比较少的机器上,默认启用线程数(CPU数量+3)/4.
2. 无法处理浮动垃圾, 因为GC时是与用户线程并发的,所以有一部分垃圾会在GC过程中产生,无法在本次GC中回收.还有因为是在GC时必须预留内存资源给用户使用.CMS提供了一个GC时已经使用内存的参数来设置比例.
3. 内存碎片,CMS是基于标记清除算法,所以CMS必须进行碎片整理,不过为了减少”stop the world”的时间,CMS允许在多次GC以后再进行碎片整理.

G1 GC

G1是目前最新的收集器,它的堆内存结构虽然也有new\old之分,但不同的是他没有物理上的分区,这种分区完全是逻辑上的分区

G1的堆内存结构将分为n多小块(Region),根本原理就是减小单次GC时锁定内存块的大小,从而可以增加GC时与用户线程的并发性,减少停顿时间.
总的来说,G1能减少”stop the world”的时间,但是因为执行GC的总时间总次数(虽然是并发的)增加了,所以可能会降低一般情况下的吞吐量

GC算法有这么多,我们到底选用什么样的算法才合适?sun公司已经给出了答案,如果真的存在这种全能型选手(前面提到的G1可能会成为这种全能选手),也就不会有这么多的GC给我选择了。IBM的研究表明98%的对象都是朝生夕死,所以对于不同的对象Hot Spot提供了不同的GC算法,以在高效和性能上进行平衡。

这就是为什么要对堆内存进行分代的原因

JVM参数设置

参数 描述 Xmx JVM最大可用内存 Xms JVM初始化内存,一般将与Xmx设置一致,避免GC后内存重新分配 Xmn 新生代分配大小,官方建议是占总的3/8,但是我们的一般应用,都是无状态应用,对象会被及时回收,所以可以将新生代设置更大一些 Xss 线程栈大小,默认1024k(有的地方说默认128,那是老早的版本了),其实还是比较大的,除非你做了一个非常离谱的递归调用,不然绝对够用 XX:PermSize 永久代大小,存储方法及常量,一般应用128M够用了,除非你有很多Proxy生成类 XX:MaxPermSize 永久代的最大值 XX:ParallelGCThreads 并行垃圾收集的线程数 XX:SurvivorRatio Eden区S区的比例,一般为8,怎eden:s0:s1=8:1:1,在实践中,对于我们的普通无状态应用,设置66536,即s区接近0,可以减少Monitor GC次数 XX:CMSInitiatingOccupancyFraction 如果GC使用CMS GC,触发FullGC时,Old区已经使用大小百分比,如=80,则表示已经使用80% XX:+UseCMSCompactAtFullCollection 因为老年代也是标记-清除算法,所以会产生内存碎片,这个参数设置每次FGC后进行内存整理压缩,显然会降低性能,延长GC时间。 XX:CMSFullGCsBeforeCompaction 由于上一个原因,所以JVM提供了另一个参数,经过几次FGC才进行内存整理,这样就能平衡性能了

调优总结

我们之前设置的SurvivorRatio非常大,也就是说年轻代只有Eden区而没有S区用来做对象复制,因为理论上我们都是无状态对象,经过GC后不存在对象需要反复GC而进入老年代的情况,但是通过实际测试来看,当高并发时,发生YGC时,大量对象被引用而无法被GC掉,这时候不存在S区,对象只能直接进入Old区,导致Old区瞬间爆满,从而发生FGC。所以最后我们将s区保留,同时减少Old区。增加s区自然会减小Eden区,从而增加YGC的频率,但是换来的却是高吞吐量。

最后的参数设置

-Xms6g -Xmx6g -Xmn4g -Xss1024K -XX:PermSize=128m -XX:MaxPermSize=256m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=3 -XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15 -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=80 
0 0
原创粉丝点击