GC优化是必要的吗?
或者更准确地说,GC优化对Java基础服务来说是必要的吗?答案是否定的,事实上GC优化对Java基础服务来说在有些场合是可以省去的,但前提是这些正在运行的Java系统,必须包含以下参数或行为:
- 内存大小已经通过-Xms和-Xmx参数指定过
- 运行在server模式下(使用-server参数)
- 系统中没有残留超时日志之类的错误日志
换句话说,如果你在运行时没有手动设置内存大小并且打印出了过多的超时日志,那你就需要对系统进行GC优化。
不过你需要时刻谨记一句话:GC tuning is the last task to be done.
现在来想一想GC优化的最根本原因,垃圾收集器的工作就是清除Java创建的对象,垃圾收集器需要清理的对象数量以及要执行的GC数量均取决于已创建的对象数量。因此,为了使你的系统在GC上表现良好,首先需要减少创建对象的数量。
俗话说“冰冻三尺非一日之寒”,我们在编码时要首先要把下面这些小细节做好,否则一些琐碎的不良代码累积起来将让GC的工作变得繁重而难于管理:
- 使用
StringBuilder
或StringBuffer
来代替String
- 尽量少输出日志
尽管如此,仍然会有我们束手无策的情况。XML和JSON解析过程往往占用了最多的内存,即使我们已经尽可能地少用String、少输出日志,仍然会有大量的临时内存(大约10-100MB)被用来解析XML或JSON文件,但我们又很难弃用XML和JSON。在此,你只需要知道这一过程会占据大量内存即可。
如果在经过几次重复的优化后应用程序的内存用量情况有所改善,那么久可以启动GC优化了。
笔者总结了GC优化的两个目的:
- 将进入老年代的对象数量降到最低
- 减少Full GC的执行时间
将进入老年代的对象数量降到最低
除了可以在JDK 7及更高版本中使用的G1收集器以外,其他分代GC都是由Oracle JVM提供的。关于分代GC,就是对象在Eden区被创建,随后被转移到Survivor区,在此之后剩余的对象会被转入老年代。也有一些对象由于占用内存过大,在Eden区被创建后会直接被传入老年代。老年代GC相对来说会比新生代GC更耗时,因此,减少进入老年代的对象数量可以显著降低Full GC的频率。你可能会以为减少进入老年代的对象数量意味着把它们留在新生代,事实正好相反,新生代内存的大小是可以调节的。
降低Full GC的时间
Full GC的执行时间比Minor GC要长很多,因此,如果在Full GC上花费过多的时间(超过1s),将可能出现超时错误。
- 如果通过减小老年代内存来减少Full GC时间,可能会引起
OutOfMemoryError
或者导致Full GC的频率升高。 - 另外,如果通过增加老年代内存来降低Full GC的频率,Full GC的时间可能因此增加。
因此,你需要把老年代的大小设置成一个“合适”的值。
影响GC性能的参数
正如我在系列的第一篇文章《理解Java GC》末尾提到的,不要幻想着“如果有人用他设置的GC参数获取了不错的性能,我们为什么不复制他的参数设置呢?”,因为对于不用的Web服务,它们创建的对象大小和生命周期都不相同。
举一个简单的例子,如果一个任务的执行条件是A,B,C,D和E,另一个完全相同的任务执行条件只有A和B,那么哪一个任务执行速度更快呢?作为常识来讲,答案很明显是后者。
Java GC参数的设置也是这个道理,设置好几个参数并不会提升GC执行的速度,反而会使它变得更慢。GC优化的基本原则是将不同的GC参数应用到两个及以上的服务器上然后比较它们的性能,然后将那些被证明可以提高性能或减少GC执行时间的参数应用于最终的工作服务器上。
下面这张表展示了与内存大小相关且会影响GC性能的GC参数
表1:GC优化需要考虑的JVM参数
类型 | 参数 | 描述 |
---|---|---|
堆内存大小 | -Xms |
启动JVM时堆内存的大小 |
-Xmx |
堆内存最大限制 | |
新生代空间大小 | -XX:NewRatio |
新生代和老年代的内存比 |
-XX:NewSize |
新生代内存大小 | |
-XX:SurvivorRatio |
Eden区和Survivor区的内存比 |
笔者在进行GC优化时最常用的参数是-Xms
,-Xmx
和-XX:NewRatio
。-Xms
和-Xmx
参数通常是必须的,所以NewRatio
的值将对GC性能产生重要的影响。
有些人可能会问如何设置永久代内存大小,你可以用-XX:PermSize
和-XX:MaxPermSize
参数来进行设置,但是要记住,只有当出现OutOfMemoryError
错误时你才需要去设置永久代内存。
还有一个会影响GC性能的因素是