深入浅出JVM优化策略

前言

Java号称一次编译,到处运行。这个目标就是通过JVM来实现的。java编译器负责把Java代码编译成.class二进制文件,在Java编译器和OS之间的JVM把.class二进制文件解释成机器码,然后机器码可交给不同的OS执行。在这个过程中,JVM之间如何进行优化,将对程序的执行影响很大,所以JVM调优过程是非常重要的工作。

一 JVM调优的整体思路

JVM运行的时候,涉及到最重要的两部分是

-内存分配
-垃圾回收算法

所以在JVM调优的过程,也是主要从这两方面进行考虑。

二 JVM内存调优的思路

JVM内存分配主要包括线程安全的区域和线程非安全的区域,其中线程安全的区域包括:

–栈
–本地方法栈
–程序计数器

其中,栈是随着是和线程的生命周期是一致的,里边存储的是栈桢,每个方法会产生一个栈桢,方法结束,栈桢会从栈中移除。那么,如果经常发现程序因为栈的OOM异常而停止运行,那么就应该考虑加大栈内存的分配。使用到的JVM参数为:

-Xss

上处参数是指定栈内存的大小。在相同的物理内存空间下,-Xss越小,产生的线程就越多,但是过小的-Xss配置,会经常产生栈的OOM异常,从而导致程序停止运行。但是-Xss过大的话,一是会产生内存资源的浪费,二是系统也就承载较少的线程数量,从而影响程序的并发。按照经验来说,通过调整-Xss的大小,使得线程数量保持在3000-5000比较合适。

在JVM的内存中线程安全区域的调优基本就是堆栈的调优。

在线程非安全区域,主要包括:

–堆
–方法区

先说对方法区的调优,方法区又称为永久代(在1.8中改为metespace,不用考虑该部分的调优),如果在程序的运行中,经常发现出现永久代的OOM异常,那么久可以考虑增大永久代的大小,使用的参数为

–XX:PermSize
–XX:MaxPermSize

这里可以这两个参数分别设置初始永久代大小和最大永久代内存大小。在引入大量的第三方类库的时候,可能会加载很多框架依赖的类,使用过程中可能就会产生因为Perm Gen不足产生的OOM。这时候可以适当调大Perm Gen的大小。

这里可以设置的最大值大于初始值,因为该方法区进行垃圾回收,使得内存波动较小,这样每次垃圾回收就很少触动动态改变方法区内存大小的机制,就不会影响JVM的性能。

接下来就是考虑堆的内存分配问题了,同时堆也是最重要的JVM内存调优部分。堆是JVM的一整块内存,在堆中又可分为新生代和老年代。调整堆的大小可用下边的参数进行调整:

–Xms
–Xmx

上边第一个参数是堆的初始化内存大小,第二个参数是最大堆大小。

在经验上,我们经常将这两个参数设置为相等,目的一是初始内存不足时触发 Full GC 来进行扩展内存,二是防止每次GC之后动态调整Xms的大小,从而影响JVM的性能。

我们在设置JVM参数的过程中,我们的目的有以下三个:

–GC的时间足够短
–GC的次数足够的少
–发生Full GC的周期足够的长

前两个是相悖的,如果想要GC的时间足够的短,那么就需要减少堆空间,如果想要GC的次数足够的少,那么就需要增大堆的空间。针对这两个矛盾,我们必须取其平衡。

堆内存又可分为新生代和老年代。在堆的大小确定了之后,就该对新生代和老年代进行调优了。

年轻代和年老代具体应该设置成什么样的比例,是没有一个固定的答案的,否则也就没有所谓的调优了。我们可以看下二者大小变化对jvm的影响:

当年轻代较大的时候, 对应的minor gc回收周期延长,但是会增大垃圾回收的时间,如果年轻代较小的话,对应的minor gc回收时间减少,但是垃圾回收的会更加频繁。
当老年代较大的时候,会较少full gc的频率,但是会增加每次full gc的时间。年老代较小的时候,会频繁引发full gc。

如何选择应该依赖生成的对象的生命周期的分布情况,如果程序中生成的对象中,生命周期短的对象占据更大的比例,那么就应该增大新生代额大小;如果存在较多的持久对象的话,那么就应该增加老年代的大小。但是一般情况下,程序不具有这样较为明显的特征,在选择的时候应该遵循以下两点:1)本着full gc应尽可能少,让老年代尽可能缓存常用对象,jvm新生代和老年代保持这个比例也是这个道理。2)我们不断通过jmap或者jstat命令查看在峰值时,老年代的内存占用情况,在不影响full gc的情况下,调整老年代的大小,注意在调整后的,应该留给老年代1/3的增长空间。

可通过下边的参数进行调整:

–XX:NewSize
–XX:NewRatio
–XX:MaxNewSize
–Xmn

上边的第一个参数的意思是设置新生代的大小,第二个参数是设置新生代和老年代的比例。Xmn是设置NewSize和MaxNewSize大小相等。

内存调优之后,就是堆垃圾回收算法的选择了。

三 JVM的GC收集器选择

垃圾回收算法主要分为三类,他们都是基于标记-清除(复制)算法实现的。

Serial算法
并行算法
串行算法

垃圾收集器如下图所示:

这里写图片描述

下边每个垃圾收集器分别进行简要说明:

1) Serial: 串行收集器,新生代收集器,使用的复制算法,在单CPU情况下,性能最好。

2) ParNew: 并行收集器,新生代收集器,是Serial的多线程版本,使用多线程进行垃圾回收,单处理器(对于多核处理器是指单核)的情况下,性能没有Serial好,在多个cpu的情况下,性能高于Serial。

3) Parallel Scavenge: 并行多线程收集器,新生代收集器,使用的算法是复制算法。他关注的是如何提高JVM的吞吐量,而其他收集器目标是如何提高JVM每次垃圾回收时候用户线程的停顿时间。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

4) Serial Old: 串行收集器,是Serial收集器的老年代版本,使用单线程进行收集,使用的算法是”标记-整理”算法。主要使用在虚拟机的Client模式下。

5) Parallel Old: 并行收集器,是Pralllel Scavenge收集器的老年代版本,使用的算法是”标记-整理算法”。

6) CMS: 并发收集器,老年代收集器,其基于”标记-清除”算法来实现的,主要包括下边的几个阶段: 1 初始标记 2 并发标记 3 重新标记 4 并发清除 其中需要停止用户线程的阶段是初始标记和重新标记阶段。CMS的详细介绍以后再说,这次不做重点。

7) G1收集器:并发收集器,产生于jdk1.7,可用于新生代和老年代收集器,在老年代使用的“标记-整理算法”, 也就是说不会产生内存碎片。

垃圾回收算法对应到的就是不同的垃圾收集器,具体到在 JVM 中的配置,是使用 -XX:+UseParallelOldGC 或者 -XX:+UseConcMarkSweepGC等 这种不同的收集器来达到选择算法的目的

一般常用到ConcMarkSweepGC, 也称之为 CMS,在使用CMS 进行老年代回收时,新生代默认使用了单线程回收算法,此时可以通过配置 -XX:+UseParNewGC来使用 新生代并行回收。

在JDK1.7 的时候引入了 G1 收集器,可以通过配置-XX:+UseG1GC 来开启。

四 结尾

所谓调优,就是一个不断调整和优化的过程,需要观察、配置、测试再如此重复。有相关经验的朋友欢迎留言补充!

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值