做了五年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堆内存调优
young generation 新生代
是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
Tenured Generation 老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
当OOM异常可以使用dump来记录异常的详细信息。jvm配置参数为:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/app/log/dump
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 |
因此当Heap大小确定之后,直接指定NewRatio的值就可以确定新生代young和老年代tenured的大小了,因此得出如下jvm配置:
-XX:NewRatio=3
理论上,如果指定了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
因此得出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
-XX:+PrintFlagsFinal 可以打印jvm的详细配置信息
-XX:+PrintTenuringDistribution 可以打印新生代到老年代的阀值和对象的寿命。
5 元数据空间大小的分配
在Java8中,移除了之前的永久代,将类的元数据信息存放在一块非堆的本地内存中,这一块区域被称之为元数据区(Metaspace),不设置的话元数据空间的大小有机器内存的大小来决定。在实际情况最好是设置一个合适的值。
做如下配置:
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
6 直接内存大小的分配
直接内存顾名思义就是机器上的一块内存。也是一块非堆的内存。直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
这篇文章对直接内存有详细说明:https://blog.csdn.net/y3over/article/details/88791958
jvm设置参数如下(直接内存默认大小为64m):
-XX:MaxDirectMemorySize=1024m
-XX:+DisableExplicitGC
-XX:+DisableExplicitGC 禁止在代码中显示调用System.gc() 。因为在使用直接内存时会使用DirectByteBuffer对象。它在分配直接内存时会经常调用System.gc(),为了减少gc此时。线上环境建议禁止显示调用System.gc()
综上得到如下配置:
-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/java_heapdump.hprof
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=1024m
-XX:+DisableExplicitGC