Java8之JVM调优初探

做了五年Java开发,一直有了解jvm的调优知识点,但在实际项目中确很少去对jvm进行调优,今天就下个决心,好好研究一下jvm调优相关的知识点。现在最常用的还是Java8 , 那就以Java8为例来做调优实践。

以下是Java虚拟器启动时内存条的大致结构图:
在这里插入图片描述
在对jvm进行优化时,最主要的就是对堆内存和Java虚拟机栈的大小进行优化。

首先还是看一下oracle官方给的调优说明文档:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html#defaults_survivor_space

  • Java虚拟机栈调优配置
    Java虚拟机栈是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建。随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
    设置Java虚拟机栈的核心配置是:-Xss 。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程。
    Java虚拟机栈的大小直接影响到方法的调用深度,比如递归调用。因此需要根据自身的应用场景来设置这个值,这个很关键!。这里我设置为512k 。对应配置为: -Xss512k

  • Heap堆内存调优

以下为堆内存结构图:
堆内存结构图
这里设计到两个大区域:

  1. young generation 新生代
    是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

  2. Tenured Generation 老年代
    主要存放应用程序中生命周期长的内存对象。
    老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

当OOM异常可以使用dump来记录异常的详细信息。jvm配置参数为:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/app/log/dump

  1. 元数据空间
    元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 nativememory, 字符串池和类的静态变量放入 Java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

Heap的大小涉及到四个主要配置参数:

参数 说明 默认值
MinHeapFreeRatio GC后Heap空闲的最小比例 40
MaxHeapFreeRatio GC后Heap空闲的最大比例 70
-Xms Heap的初始化大小 6656k
-Xmx Heap的最大大小 calculated

官方给出的建议:
The following are general guidelines regarding heap sizes for server applications:
Unless you have problems with pauses, try granting as much memory as possible to the virtual machine. The default size is often too small.
Setting -Xms and -Xmx to the same value increases predictability by removing the most important sizing decision from the virtual machine. However, the virtual machine is then unable to compensate if you make a poor choice.
In general, increase the memory as you increase the number of processors, since allocation can be parallelized.

意思是建议将Heap的初始值和最大值设置为相同。

The default maximum heap size is half of the physical memory up to a physical memory size of 192 megabytes (MB) and otherwise one fourth of the physical memory up to a physical memory size of 1 gigabyte (GB).
这意思是当物理内存小于1G时,默认的最大Heap的大小为物理内存的一半。当物理内存大于1G时,默认的堆最大值为物理内存的四分之一。

也就是说,当我们服务器的内存为4G时,推荐使用如下配置:

-Xms1024m -Xmx1024m

当确定了Heap的取值,现在就要考虑到Young Generation 和 Tenured Generation比例的分配啦。

参数 说明 默认值
NewRatio the ratio between the young and tenured generation.新生代与老年代的比例 2
NewSize the young generation size . 新生代的大小 1310M
MaxNewSize the young generation max size.新生代最大值 not limited
SurvivorRatio the ratio between eden and a survivor space. eden区与survivor区的比例 8
  • NewRatio
    从结构关系中可以看到: heap = young + tenured
    当Heap的大小确定后,可以通过NewRatio来调整young和tenured的大小。
    NewRatio = tenured / young ; 默认值为2
    得出: young = 1/3 heap ; tenured = 2/3 heap

因此当Heap大小确定之后,直接指定NewRatio的值就可以确定新生代young和老年代tenured的大小了,因此得出如下jvm配置:

-XX:NewRatio=3

  • NewSize 和 MaxNewSize

理论上,如果指定了NewRatio就不需要再去配置NewSize了。因为NewSize的值基本上确定了。以Heap=1G为例,NewSize=1/3G

The parameters NewSize and MaxNewSize bound the young generation size from below and above. Setting these to the same value fixes the young generation, just as setting -Xms and -Xmx to the same value fixes the total heap size. This is useful for tuning the young generation at a finer granularity than the integral multiples allowed by NewRatio.

官方说明:建议将NewSize 和 MaxNewSize设置相同。这两个参数配置比使用NewRatio控制的颗粒度更精细。也就是说建议使用这两个参数来指定young generation所占内存空间大小。

因此得出jvm设置:
-XX:NewSize=360M -XX:MaxNewSize=360M

  • SurvivorRatio
    eden区与survivor区的比例 ,由于Survivor区包含 from survivor 和 to survivor 。from和to的大小是相同的。
    举个例子:
    SurvivorRatio默认值是8
    意思是: survivor = 1/8 eden = from + to ;
    from = 1/10 young ;
    to = 1/10 young ;
    eden = 8/10 young ;

因此得出jvm配置:

-XX:SurvivorRatio=8

对于堆内存的分配会直接影响Java的垃圾回收机制。因此jvm调优的核心逻辑还是控制其垃圾回收的方式。

打印jvm垃圾回收日志命令如下:

-XX:+PrintGCDetails
-Xloggc:/var/app/loggs/gc.log

  • 垃圾回收器的选择

除非您的应用程序有非常严格的暂停时间要求,否则请先运行您的应用程序并允许VM选择收集器。如有必要,请调整堆大小以提高性能。如果性能仍然不能达到您的目标,请使用以下准则作为选择收集器的起点。

1 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行收集器。

2 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则让VM选择收集器,或通过选项-XX:+ UseSerialGC选择串行收集器。

3 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受1秒或更长时间的暂停,则让VM选择收集器,或使用-XX:+ UseParallelGC选择并行收集器。

4 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持小于1秒,那么请使用-XX:+UseConcMarkSweepGC或-XX:+ UseG1GC选择并发收集器。

这些准则仅为选择收集器提供了一个起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。暂停时间对这些因素特别敏感,因此前面提到的1秒阈值仅是近似值:在许多数据大小和硬件组合上,并行收集器的暂停时间将超过1秒。相反,在某些组合上,并发收集器可能无法将暂停时间保持在1秒以内

我比较看着响应时间,因此这里选择使用标记清除算法的CMS垃圾回收器,使用配置:-XX:+UseConcMarkSweepGC

其他的暂时使用实现默认配置。到这里就得出了jvm的配置如下:
-Xms1024m
-Xmx1024m
-Xss512k
-XX:NewRatio=3
-XX:NewSize=360M
-XX:MaxNewSize=360M
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-Xloggc:/var/app/loggs/gc.log
-XX:+UseConcMarkSweepGC
-XX:+PrintTenuringDistribution
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/app/log/dump

-XX:+PrintFlagsFinal 可以打印jvm的详细配置信息 -XX:+PrintTenuringDistribution 可以打印新生代到老年代的阀值和对象的寿命。