建议 先看目录, 对内容有个整体把握。
概述
JVM调优 是指对 Java虚拟机(JVM) 进行性能优化和资源管理的过程。
JVM是 Java程序 运行的环境,负责解释和执行Java字节码,提供内存管理、垃圾回收、线程管理等功能。
JVM调优的 目标 是提高Java程序的性能和稳定性,包括优化内存使用、减少垃圾回收时间、提高程序的响应速度等。
调优的 具体操作 包括调整JVM的堆大小、设置垃圾回收器的参数、优化线程池的配置等。
通过对JVM进行调优,可以提升Java程序的运行效率,降低资源消耗,提供更好的用户体验。
调优参数
先决说明
版本说明
- 本参数以
JDK1.8
为基础进行整理,目前默认参数大概有660个左右,使用下面命令可以输出所有参数的名称及默认值
java -XX:+PrintFlagsFinal -version
- 从Java 8开始,元空间取代了永久代(PermGen space),用于存储类的元数据。
即 JDK8之后把-XX:PermSIze 和 -XX:MaxPermGen移除了,取而代之的是
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
JDK8开始 把类的 元数据
放到 本地化的 堆内存(native heap)中,这一块区域就叫Metaspace
,中文名叫元空间
。
使用 本地化的内存 即 元空间 有什么好处呢?最直接的表现是java.long.OutOfMemonyError:PermGen空间问题将不复存在,因为默认的类的元数据分配只受本地内存大小的限制,也就是说本地内存剩余多少,理论上Metaspace就可以有多大,这解决了空间不足的问题。
名词解释
- -XX:开头的参数代表虚拟机 非稳定参数
- -XX:+ 开启option参数
- -XX:- 关闭option参数
- -XX:= 将option参数的值设置为value
非稳定参数:
- 此类参数的设置很容易引起JVM 性能上的差异,使JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM 的性能及稳定性。
- 非标准选项(不能保证被所有的 JVM 实现都支持),并且在 JDK 的后续版本中 随时可能被更改 或者 删除。
配置参数
把内存模型图放在这里,方便对下面参数的理解。
1、内存管理参数
堆参数:
- -Xmx
最大堆内存,如果初始堆空间不足的时候,最大可以扩展到多少。
默认值:默认的最大堆大小是 物理内存 的 二分之一
- -Xms
初始堆大小,JVM 启动的时候,给定堆空间大小。
默认值:无默认值
- -Xmn
用于设置年轻代(Young Generation)的大小。年轻代是 JVM 堆内存中的一部分,主要存放新创建的对象。
年轻代又被划分为 Eden 区、两个 Survivor 区(S0 和 S1),这是基于分代收集(Generational Collection)的垃圾回收策略。
如果年轻代设置得 太小 ,可能会导致频繁的 Minor GC(年轻代垃圾回收),这会影响应用程序的响应时间。
如果年轻代设置得 太大,可能会减少 Minor GC 的频率,但会增加每次 GC 的持续时间,这同样可能对性能产生负面影响。
设置年轻代大小。
整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小
。
持久代一般固定大小为 64M,所以增大年轻代后,将会减小年老代大小。
此值对系统性能影响较大,Sun 官方 推荐配置 为整个堆 的 3/8
- -XX:SurvivorRatio
Eden 区所占比例,默认是 8,也就是 80%
- -XX:MaxHeapFreeRatio
当Xmx值比Xms值大时,堆可以动态收缩和扩展,这个参数控制当前堆空闲大于指定比率时 自动收缩
默认值70
- -XX:MinHeapFreeRatio
当Xmx值比Xms值大时,堆可以动态收缩和扩展,这个参数控制当前堆空闲小于指定比率时 自动扩展
默认值70
栈参数
- -Xss
设置 每个
线程 栈内存
的大小,设置的栈的大小决定了函数调用的最大深度,减少这个值能生成更多的线程,但是 函数调用的最大深度 会有所减少。
-Xss 设置的大小决定了函数调用的深度,如果函数调用的深度大于设置的Xss大小,那么将会抛 栈溢出“java.lang.StackOverflowError“
异常。
在操作系统对单个进程中 线程数量 有限制,无法无限制生成,经验值在 3000~5000 左右。
JDK5后默认为1MB,一般来讲设置成 256K 就足够了。
Metaspace 参数
- -XX:MaxMetaspaceSize
设置 元空间 最大值
默认 大小不受堆限制,仅受系统内存限制,用于保存系统的类信息,比如类的字段、方法、常量池等。
但是线上环境建议设置一下。
- -XX:MetaspaceSize
指定 元空间 的 初始空间 大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
如果不设置的话,默认是20.79M,这个初始大小是触发首次 Metaspace Full GC 的阈值,
- -XX:MinMetaspaceFreeRatio
最小空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)小于此值,就会触发 Metaspace 扩容。
默认值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40
- -XX:MaxMetaspaceFreeRatio
最大空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)大于此值,就会触发 Metaspace 释放空间。
默认值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70
代参数
- -XX:NewRatio
设置年轻代和年老代的比值。如为 3,表示年轻代与年老代比值为 1:3
老年代默认64MB,其余的为年轻代
- -XX:MaxTenuringThreshold
年轻代 晋升到 老年代 的对象年龄。每个 年轻代 在 经历过一次 GC之后,年龄就+1,当 年龄 超过这个参数时就进入 老年代。
默认值15
- -XX:PretenureSizeThreshold
对象所占内容 超过这个值的时候,对象直接在老年代区分配内存。
默认值是0,意思是不管多大都是先在eden中分配内存。
- -XX:+UseAdaptiveSizePolicy
动态调整Java堆中各个区域的大小及进入老年代的年龄
默认开启
- -XX:MaxPermSize
永久代的最大值
大部分情况下默认值时64MB
- -XX:+HandlePromotionFailure
是否允许 分配失败,
即老年代的 剩余空间 不足以 容纳 新生代的整个Eden和Survivor区的所有对象 都存活 的 极端情况 。
JDK1.5以前默认关闭,之后默认开启
其他
- -XX:MaxNewSize
新生成 对象 能 占用内存 的 最大值
- -XX:+UserTLAB
优先在本地线程缓冲区中分配对象,避免分配内存时的锁定过程
Server模式默认开启
2、收集器设置
- -XX:+DisableExplicitGC
忽略来自System.gc()方法触发的垃圾收集
默认关闭
- -XX:+ExplicitGCInvokes Concurrent
当收到System.gc()方法提交的垃圾收集申请时,使用CMS收集器进行收集
默认关闭
- -XX:+UseSerialGC
虚拟机在Client模式下的默认值,打开此开关后,使用Serial + Serial Old 的收集器组合进行内存回收
Client模式的虚拟机默认开启,其它模式关闭
- -XX:+UseParNewGC
打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
默认关闭
- -XX:+UseConcMarkSweepGC
打开此开关后,使用ParNew + CMS +Serial Old的收集器组合进行内存回收。如果CMS收集器出现Concurrent Mode Failure,则Serial Old收集器左右后备(备用)收集器
默认关闭
- -XX:+UseParallelGC
虚拟机运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行内存回收
Server模式的虚拟机默认开启,其它模式关闭
- -XX:+UseParalledlOldGC
打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合内存回收
默认关闭
- -XX:+UseConcMarkSweepGC
设置 并发 收集器
-XX:+UseG1GC
设置 G1 收集器
默认关闭,JDK1.7中新的收集器
- -XX:+UseAdaptiveSizePolicy
设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例
- -XX:CMSInitialingOccupancyFraction
默认值为68
设置CMS收集器在老年代空间被是哟个多少后触发垃圾收集,仅在使用CMS收集器时生效
- -XX:+UseCMSCompactAtFullCollection
默认开启
设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理。仅在CMS收集器时生效
- -XX:CMSFullGCsBeforeCompaction
无默认值
设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效
- -XX:+ScavengeBeforeFullGC
默认开启
在Full GC发生之前触发一次 Minor GC
- -XX:+UseGCOverheadLimit
默认开启
禁止GC过程无限制地执行,如果过于频繁,就直接发生OutOfMemory异常
- -XX:ParallelGCThreads
少于或等于8个CPU时默认值为CPU数量值,多余8个时比CPU数量值小
设置并行GC时进行内存回收的线程数
- -XX:MaxGCPauseMillis
无默认值
设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。仅在使用Parallel Scavenge收集器时生效
- -XX:GCTimeRatio
默认值99
GC时间占总时间的比率,默认值99,即允许1%的GC时间。仅在使用Parallel Scavenge收集器时生效,公式为1/(1+n)
- -XX:+CMSIncrementalMode
设置为增量模式。适用于单CPU情况
3、行为参数(功能开关)
- -XX:+UseThreadPriorities
启用本地线程优先级
- -Dcom.sun.management.jmxremote
默认未开启
开启jmx远程监控
4、垃圾回收统计信息
- -verbose:gc
输出虚拟机中GC的详细情况
- -XX:+PrintGC
与-verbose:gc 一样,同样效果
- -XX:+PrintGCDetails
打印GC详细信息
- -XX:+PrintGCTimeStamps
打印CG发生的时间戳
- -Xloggc:filename
将每次GC事件的相关情况记录到一个文件中
5、 即时编译参数
- -XX:CompileThreshold
Client模式下默认值是1500,Server模式下是10000
触发方法即时编译的阈值
- -XX:OnStackReplacePercentage
Client模式下默认值是933,Server模式下是140
OSR比率,它是OSR即时编译阈值计算公式的一个参数,用于代替BackEdgeThreshold参数控制回边计数器的实际溢出阈值
- -XX:ReservedCodeCacheSize
大部分情况下默认值是32MB
即时编译器编译的代码缓存的最大值
6、 类型加载参数
- -XX:+UseSplitVerifier
默认开启
使用依赖StackMapTable信息的类型检查代替数据流分析,以加快字节码校验速度
- -XX:+FailOverToOldVerifier
默认开启
当类型校验失败时,是否允许回到老的类型推导校验方式进行校验,如果开启则允许
- -XX:+RelaxAccessControlCheck
默认关闭
在校验阶段放松对类型访问性的限制
7、多线程参数
- -XX:+UseSpinning
JDK1.6默认开启,JDK 1.5默认关闭
开启自旋锁以避免线程频繁的挂起和唤醒
- -XX:PreBlockSpin
默认值为10
使用自旋锁时默认的自旋次数
- -XX:+UseThreadPriorities
默认开启
使用本地线程优先级
- -XX:+UseBiasedLocking
默认开启
是否使用偏向锁,如果开启则使用.
- -XX:+UseFastAccessorMethods
默认开启
当频繁反射执行某个方法时,生成字节码来加快反射的执行速度
8、性能参数
-Xverify
默认开启
设置为none,可以加快来的加载速度,提高启动速度
- -XX:+AggressiveOpts
JDK1.6默认开启,JDK1.5默认关闭
使用激进的优化特性,这些特性一般是具备正面和负面双重影响的,需要根据具体应用特点分析才能判定是否对性能有好处
- -XX:+UseLargePages
默认开启
如果可能,使用大内存分页,这项特性需要操作系统的支持
- -XX:LargePageSizeInBytes
默认为4MB
使用指定大小的内存分页,这项特性需要操作系统的支持
- -XX:+StringCache
默认开启
是否使用字符串缓存,开启则使用
9、调试参数
- -XX:OnOutOfMemoryError
无默认值
当虚拟机抛出内存溢出异常时,执行指定的命令
- -XX:OnError
无默认值
当虚拟机抛出ERROR异常时,执行指定的命令
- -XX:+PrintClassHistogram
默认关闭
使用[ctrl]-[break]快捷键输出类统计状态,相当于jmap -histo的功能
- -XX:+PrintConcurrentLocks
默认关闭
打印JU.C中锁的状态
- -XX:+PrintCommandLineFlags
默认关闭
打印启动虚拟机时输入的非稳定参数
- -XX:+PrintCompilation
默认关闭
打印方法即时编译信息
- -XX:+PrintGC
默认关闭 打印GC信息
- -XX:+PrintGCDetails
默认关闭
打印GC的详细信息
- -XX:+PrintGCTimeStamps
默认关闭
打印GC停顿耗时
- -XX:+PrintTenuringDistribution
默认关闭
打印GC后新生代各个年龄对象的大小
- -XX:+TraceClassLoading
默认关闭
打印类加载信息
- -XX:+TraceClassUnloading
默认关闭
打印类卸载信息
- -XX:+PrintInlining’
默认关闭
打印方法的内联信息
- -XX:+PrintCFGToFile
默认关闭
将CFG图信息输出到文件,只有DEBUG版虚拟机才支持此参数
- -XX:+PrintIdcalGraphFile
默认关闭
将ldeal图信息输出到文件,只有DEBUG版虚拟机才支持此参数
- -XX:+UnlockDiagnosticVMOptions
默认关闭
让虚拟机进入诊断模式,一些参数(如PrintAssembly)需要在诊断模式中才能使用
- -XX:+PrintAssembly
默认关闭
打印即时编译后的二进制信息
JVM 8 调优参数 的 推荐数据
推荐数据
1、堆内存设置
- -Xms16g:设置初始堆内存为16GB,为系统内存的一半,确保系统有足够的内存用于非堆内存和操作系统本身。
- -Xmx16g:设置最大堆内存也为16GB,有助于减少堆内存的动态调整。
2、垃圾收集器选择
- -XX:+UseG1GC:使用G1垃圾收集器,适合于大堆内存和多核处理器的场景,可以提供平衡的吞吐量和较低的延迟。
3、G1垃圾收集器的进一步优化
- -XX:MaxGCPauseMillis=200:设置期望的最大GC暂停时间(毫秒),以便于优化延迟。
- -XX:ParallelGCThreads=8:设置并行垃圾收集线程数。一般设置为可用CPU核心数。
- -XX:ConcGCThreads=4:设置G1的并发标记线程数,一般为ParallelGCThreads的一半。
4、元空间(Metaspace)
-XX:MetaspaceSize=256m:设置初始元空间大小,元空间用于存放类元数据。
-XX:MaxMetaspaceSize=512m:设置最大元空间大小,以限制其无限增长可能导致的问题。
5、日志和监控
- -XX:+PrintGCDetails:打印详细的GC日志。
- -XX:+PrintGCDateStamps:为GC日志添加时间戳。
- -Xloggc:/var/log/yourapp-gc.log:将GC日志写入指定文件。
- -XX:+UseGCLogFileRotation:开启GC日志文件的轮替。
- -XX:NumberOfGCLogFiles=5:指定GC日志文件的数量。
- -XX:GCLogFileSize=20M:指定GC日志文件的大小。
6、JVM性能调优
- -XX:+UseStringDeduplication:开启JVM字符串去重功能,有助于减少堆内存的占用。
- -XX:+DisableExplicitGC:禁用System.gc()的显式调用,避免可能的性能问题。
代码示例
1:设置堆内存大小
JVM启动参数:
java -Xms256m -Xmx512m -jar YourApp.jar
-
-Xms256m 设置初始堆大小为256MB。
-
-Xmx512m 设置最大堆大小为512MB。
Java代码:
public class HeapSizeExample {
public static void main(String[] args) {
// 获取运行时环境
Runtime runtime = Runtime.getRuntime();
// 打印JVM的初始内存和最大内存配置
System.out.println("JVM初始内存大小: " + runtime.totalMemory() / (1024 * 1024) + " MB");
System.out.println("JVM最大内存大小: " + runtime.maxMemory() / (1024 * 1024) + " MB");
}
}
此代码显示了如何在Java程序中获取当前JVM的内存使用情况。
2:使用并调优G1垃圾收集器
JVM启动参数:
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar YourApp.jar
-
-XX:+UseG1GC 启用G1垃圾收集器。
-
-XX:MaxGCPauseMillis=200 设置垃圾收集的最大暂停时间。
Java代码:
import java.util.ArrayList;
import java.util.List;
public class G1GCExample {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配1MB的空间
if (list.size() > 100) {
list.clear(); // 当列表大小超过100时,清空列表释放内存
}
}
}
}
这段代码演示了在使用G1垃圾收集器时的内存分配和清理。
3:JVM性能监控和调试
JVM启动参数:
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar YourApp.jar
-
-XX:+PrintGCDetails 打印GC的详细信息。
-
-XX:+PrintGCDateStamps 在GC日志中添加时间戳。
-
-Xloggc:gc.log 将GC日志输出到指定的文件。
Java代码:
public class GCLoggingExample {
public static void main(String[] args) {
// 创建一个大对象并立即使其可回收,触发GC
byte[] allocation = new byte[50 * 1024 * 1024]; // 分配约50MB的空间
allocation = null; // 使分配的空间可回收
System.gc(); // 主动请求垃圾收集
// 等待一段时间,以便有时间打印GC日志
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这段代码演示了如何通过分配和释放大量内存来触发垃圾收集,并使用JVM参数来记录GC的详细日志。
4:监控JVM线程堆栈
JVM启动参数:
java -XX:+PrintCommandLineFlags -jar YourApp.jar
- -XX:+PrintCommandLineFlags:打印出JVM启动时使用的所有参数。
Java代码:
public class ThreadStackMonitor {
public static void main(String[] args) {
// 创建线程
Thread thread = new Thread(() -> {
try {
Thread.sleep(10000); // 让线程休眠一段时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start(); // 启动线程
System.out.println("线程堆栈监控已启动...");
}
}
这段代码启动了一个线程,并通过JVM参数打印出了JVM启动时使用的所有参数,有助于了解当前JVM配置。
5:配置Java堆和元空间大小
JVM启动参数:
java -Xms256m -Xmx512m -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m -jar YourApp.jar
-
-Xms256m:设置初始堆内存大小为256MB。
-
-Xmx512m:设置最大堆内存大小为512MB。
-
-XX:MetaspaceSize=64m:设置初始元空间大小为64MB。
-
-XX:MaxMetaspaceSize=256m:设置最大元空间大小为256MB。
Java代码:
public class HeapMetaspaceConfig {
public static void main(String[] args) {
System.out.println("Java堆和元空间大小已配置...");
// 这里的代码主要是为了演示如何设置JVM参数,并没有特定的操作来显示它们的效果
}
}
此代码段用于演示如何配置Java堆和元空间大小的JVM参数。
6:启用GC日志和详细输出
JVM启动参数:
java -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar YourApp.jar
-
-verbose:gc:启用垃圾收集日志。
-
-XX:+PrintGCDetails:打印详细的垃圾收集信息。
-
-XX:+PrintGCDateStamps:在垃圾收集日志中添加时间戳。
-
-Xloggc:gc.log:将垃圾收集日志输出到指定的文件。
Java代码:
public class VerboseGC {
public static void main(String[] args) {
System.out.println("GC日志和详细输出已启用...");
// 这段代码主要用于演示如何通过JVM参数开启GC日志,实际上并不执行特定的操作来触发GC
}
}
这段代码演示了如何通过JVM参数启用GC的详细日志,有助于分析和优化垃圾收集行为。
7:开启JVM的本地方法接口(JNI)检查
JVM启动参数:
java -Xcheck:jni -jar YourApp.jar
- -Xcheck:jni:开启对JNI函数的检查,这有助于发现JNI相关的问题。
Java代码:
public class JNICheckExample {
public static void main(String[] args) {
System.out.println("JNI检查已启动...");
// 这里的代码主要用于演示启动参数的效果,实际上并不涉及JNI调用
}
}
此代码 展示了如何使用JVM参数开启对JNI调用的检查,对于使用本地库的Java应用程序非常有用。
8:打印JVM启动时的系统属性
JVM启动参数:
java -Djava.util.logging.config.file=logging.properties -jar YourApp.jar
- -Djava.util.logging.config.file=logging.properties:设置日志系统属性。
Java代码:
public class SystemPropertiesExample {
public static void main(String[] args) {
System.out.println("JVM启动时的系统属性已设置...");
System.getProperties().forEach((key, value) -> {
System.out.println(key + ": " + value);
});
}
}
这段代码演示了如何打印JVM启动时设置的所有系统属性,有助于了解当前的配置环境。
9:开启并调整Java飞行记录器(JFR)
JVM启动参数:
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -jar YourApp.jar
-
-XX:+UnlockCommercialFeatures:解锁商业特性(在JVM 8中需要)。
-
-XX:+FlightRecorder:开启Java飞行记录器。
Java代码:
public class JavaFlightRecorderExample {
public static void main(String[] args) {
System.out.println("Java飞行记录器已启动...");
// 这里的代码主要用于演示如何开启Java飞行记录器
// 实际使用时,JFR会在后台收集数据
}
}
此代码展示了如何开启Java飞行记录器,它是一个强大的工具,用于收集关于JVM行为的详细数据。
10:启用并设置详细的类加载信息
JVM启动参数:
java -XX:+TraceClassLoading -XX:+TraceClassUnloading -jar YourApp.jar
-
-XX:+TraceClassLoading:启用类加载跟踪。
-
-XX:+TraceClassUnloading:启用类卸载跟踪。
Java代码:
public class ClassLoadingTracingExample {
public static void main(String[] args) {
System.out.println("类加载和卸载跟踪已启动...");
// 这里不需要特定的Java代码,因为类加载和卸载信息将通过JVM参数直接打印到控制台
}
}
这段代码用于演示如何启用JVM的类加载和卸载信息的跟踪,这对于分析和优化应用程序的性能非常有用。
11:监控垃圾收集器工作
JVM启动参数:
java -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -jar YourApp.jar
-
-XX:+PrintGC:开启基本的GC信息打印。
-
-XX:+PrintGCDetails:打印详细的GC信息。
-
-XX:+PrintGCTimeStamps:在GC信息中加入时间戳。
Java代码:
public class GCMonitorExample {
public static void main(String[] args) {
System.out.println("垃圾收集器监控已启动...");
// 此案例 不包含具体的垃圾收集触发操作,因为这些信息会通过JVM参数打印出来
}
}
此代码 展示了如何开启和查看垃圾收集器的工作信息,这对于优化内存管理和调试内存问题非常有价值。
12:设置并查看线程堆栈大小
JVM启动参数:
java -Xss1M -jar YourApp.jar
- -Xss1M:设置每个线程的堆栈大小为1MB。
Java代码:
public class ThreadStackSizeExample {
public static void main(String[] args) {
System.out.println("线程堆栈大小设置为1MB...");
// 这里不需要特定的代码来演示线程堆栈大小的影响,因为这是JVM层面的设置
}
}
这段代码演示了如何设置并查看线程堆栈的大小,这对于处理大量线程或深层递归的应用程序非常重要。
调优建议
1、年轻代和老年代的比例需要结合实际场景调整
我们知道Java的内存模型中,最大的内存区域块叫做堆,而在Hotspot JDK8中采用分代回收类型垃圾收集器的时候,堆内部被划分为了年轻代和老年代。
年轻代:新创建的对象生存于此,内部划分为eden区和from survior,to survior区。
老年代:主要用于存放经过年轻代多次回收(回收次数超过阈值即可晋升)依然存活的对象,或者某些因其他特殊原因晋升的对象。
老年代它有个特点,就是对象比较稳定,所以针对这部分的GC进行调优可能难度比较高,我们在对老年代进行关注的时候,更多是关注空间是否足够。
在Jvm的年轻代里,有两个模块组成,分别是eden区和survior区域。年轻代和老年代的整体内存布局如下所示:
在Hotspot版本的Jdk8中,年轻代和老年代的默认比例是1:2,这个比例可以通过下列参数来进行控制。
–XX:NewRatio=2 (这里意思是年轻代占据了1/3的堆内存空间,老年代占比是年轻代的两倍)
看到这里 可能你会想,那不如将年轻代的内存设置大一些,这样是不是可以减少minior gc的次数呢?
不过这样也会导致老年代的内存不够用,所以这个得结合实际测试得出最佳的比例。如果你拿不定主意,我建议使用默认的比例就好了。
如果你在实际测试中,发现了比默认值更好的比例设置,可以参考使用以下几个参数:
-Xms128M :设置堆内存最小值
-Xmx128M :设置堆内存最大值
-XX:NewSize=64M :设置New区最小值
-XX:MaxNewSize=64 :设置New区最大值
-XX:NewRatio=2 :设置Old区与New区的比例
-Xmn64M :设置New区大小,等价于-XX:NewSize=64M 与-XX:MaxNewSize=64M两个参数,此值一旦设置则–XX:NewRatio无效。
这里不是太推荐使用NewSize和MaxNewSize两个参数,建议使用Xmn区替代它们。这样可以在一开始就将年轻代的大小分配到最大,减少这个内存扩容的过程消耗。但是这里也要看实际的机器内存是否紧张。
另外在GC里面有一条很重要的实战调优经验总结是这样说的:
由于 老年代的GC成本 通常都会比 年轻代的成本 要高许多,所以建议适当地通过Xmn命令区设置年轻代的大小,最大限度的降低对象晋升到老年代的情况。
2、合理设置Eden区和Survivor区比例
这两个区域都存在于年轻代里面,可以通过下列参数来进行它们大小比例的设置:
-XX:SurvivorRatio=2 (表示eden区大小是survivor区大小的2倍)
通常JDK8里面,默认的这个比例是1:8(单个survivor:eden),这个比例其实是JDK开发者经过了众多实战之后,才设置的值,所以如果没有经过实际压测的话,不建议随便调整这个比例。为什么这么说呢,这里我总结了两个原因:
不要设置过高的eden区空间
虽然年轻代对象的存放空间很多,但是survivor的空间会很少,很可能导致从eden区晋升到survivor区的对象,没有足够的空间存放,然后直接进入了老年代。
不要设置过低的eden区大小
首先eden区的空间不足,会导致minior gc的频繁发生,同时survivor区也会导致空间过剩,内存浪费。
3、如何结合业务场景进行堆内存的分配
前边我们提到了合理的分配eden区和survivor区的比例很重要,为了让大家更加深入的去理解这里面的重要性,我通过一个案例和大家进行GC调优的分析。
假设我们有一个高并发的消息中台服务,专门提供了用户基础信息的crud操作。预估上线后的qps大概会在6000+左右,预计上线后部署的服务节点是2core/4gb,16台,那么此时要如何进行jvm的参数评估。
这里我们可以分析下,假设是6000+请求分配到了16个节点上,那么大概就是每个节点承载400左右的qps。这里由于消息中台底层会有比较多的数据库查询,所以存储部分做了分库分表,而且大部分情况会走缓存处理。
假设我们的消息对象Message为:
public class Tom{
private Long id;
private String sid;
private Long userId;
private String content;
//getter setter省略
}
这里我们可以模拟下这个对象的存储内容,然后进行大小预估:
public static void main(String[] args) throws IllegalAccessException {
Tom tom = new Tom();
tom.setId(191912342L);
tom.setSid("981hhdkahnhiodqw012");
tom.setUserId(10012L);
tom.setContent("这是一条测试语句");
System.out.println(tom);
ClassIntrospector ci = new ClassIntrospector();
ObjectsInfo res = null;
res = ci.introspect(tom);
System.out.println(res.getDeepSize());
}
假设 使用工具预估单个tom 对象的大小在912byte左右,这里我们预估它有1kb左右。
那么面对单个节点400qps的访问,一秒钟单是tom 对象可能就是400kb起步,再加上可能会有其他一些杂七杂八的其他对象产生,这里我们暂且可以预估个10倍的量。(这里的10倍要结合业务场景去计算)。
最后我们其实还需要考虑到代码里面是否会有使用List这种数据结构的情况,如果有,可能还得翻个10倍,也就是40mb/s的对象产生速率。
而这些新产生的对象,大多数都是用完就废的状态,所以基本上熬不过一轮Minior GC。但是在进行Minior GC的时候,对象可能还存在引用的可能(例如有些方法执行到了一半),Minior GC每次回收后会有部分的对象可能会存活下来,然后进入到survivor区中。
而之前我们说了,服务的节点总内存是4gb,那么jvm的堆内存可以分配大约60%的空间(预留一部分是元空间和线程内存等),也就是2.5gb左右。所以此时可以尝试分配参数是:
- -Xms2560m -Xmx2560m -Xmn1024m
这个参数可以分配给了年轻代1gb左右的大小,按照默认的比例来算就是eden区780mb,两个survivor区合并起来260mb左右,即单个survivor区为130mb。这也就意味着,按照我们上边预期的情况来想,40mb/s的对象产生速率,大概20秒可以占满eden区,按照统计,大概会有95%的对象被回收,大约剩下35mb左右的对象放入到survivor区中。
目前从理论层面来看似乎一切都还挺正常的,但是不要忘了,实际还是需要通过压测去验证的。假设哪天我们的业务场景变化了,程序员在代码中用了很多的List去存放对象,那么GC的情况可能就不像你想的那么简单了。
例如某天,当你发现上了一个需求之后,线上的老年代GC开始变得频繁了,但是代码里面也没有什么问题,那么这个时候,会有一种可能就是因为你的survivor区过小,导致对象在进行minior gc之后存活的对象体积大于survivor区的一半,从而导致了对象的直接晋升。
而这种时候,你可以结合业务场景进行调优分析,例如降低老年代的大小比例,增加survivor区的大小。
当然上边我说的这些都是需要你结合业务场景去分析的,这里我只是给了一个思路,整体思路我总结下来,大概就是:合理分配eden区和survivor区,尽量不要让对象进入老年代。
4、使用CMS垃圾收集器的时候注意老年代的内存压缩频率
在老年代中,CMS默认会先采用标记清除算法进行内存的回收,每次老年代进行full gc的时候,会有一个计数器在做累加。
当老年代的full gc 超过了一定次数之后,就会进行一次内存压缩。这个内存压缩可以减少内存碎片的存在,具体通过下列参数进行控制
-XX:CMSFullGCsBeforeCompaction=10
这个数值默认是0,也就是说 每次老年代的full gc执行之后,都会触发一次内存碎片的压缩,在进行内存压缩的过程中,会延长GC的时间。所以这个参数我觉得是可以进行调优的,不过要结合实战进行调整。
5、合理设置CMS垃圾收集器在老年代的回收频率
-XX:CMSInitiatingOccupancyFraction 表示触发 CMS GC 的老年代使用阈值,一般设置为 70~80(百分比),设置太小会增加 CMS GC 发现的频率,设置太大可能会导致并发模式失败或晋升失败。默认为 -1,表示 CMS GC 会由 JVM 自动触发。
-XX:+UseCMSInitiatingOccupancyOnly 表示 CMS GC 只基于 CMSInitiatingOccupancyFraction 触发,如果未设置该参数则 JVM 只会根据 CMSInitiatingOccupancyFraction 触发第一次 CMS GC ,后续还是会自动触发。建议同时设置这两个参数。
CMSInitiatingOccupancyFraction默认是92%,所以使用cms垃圾收集器,默认老年代的回收是非常少的,而如果当内存到达了92%比例的占用,那么此时就会触发CMS垃圾收集器的回收流程。
如果此时发现内存空间不足了,就会进入使用Serial Old收集器来进行回收的环节,这一阶段的性能就很差了,所以这一点也是CMS垃圾收集器存在的一个风险隐患,极端场景下可能会有长时间的stw。
6、容器化部署中的JVM参数需要注意哪些点
在HotSpot类型的Java程序中,JVM的内存大小通常会是(堆大小+栈空间 * 线程数+元空间+其他内存),所以如果只是配置了Xmx(最大堆内存)参数其实还是不够的。
其实Java程序默认启动的堆大小是操作系统内存大小的1/4;可以通过参数 -XshowSettings:vm -version 来查看。如果我们将程序部署到了容器节点里面的话,但是不想配置xmx类型的参数,这个时候可以用UseCGroupMemoryLimitForHeap来设置,使用了该参数后可以使Java应用在启动的时候,能够读取到容器节点内的内存大小。这样就不用担心JVM内存大小超过容器的cgoup内存占用大小了,而被误杀。但是使用该参数的利用率会很低。
当然如果你不太信任自动挡机制的话,安全起见可以使用手动挡方式设置Xmx内存参数。
基本流程
要进行 JVM 调优无非就是以下两种情况:
-
目标驱动型的 JVM 调优,如,我们是为了最短的停顿时间所以要进行 JVM 调优,或者是我们为了最大吞吐量所以要进行 JVM 调优等。
-
问题驱动型的 JVM 调优,因为生产环境出现了频繁的 FullGC 了,导致程序执行变慢,所以我们要进行 JVM 调优。
所以,针对不同的 JVM 调优的手段和侧重点也是不同的。
总的来说,JVM 进行调优的流程如下:
-
确定 JVM 调优原因
-
分析 JVM(目前)运行情况
-
设置 JVM 调优参数
-
压测观测调优后的效果
-
应用调优后的配置
具体来说它们的执行如下。
1.确定JVM调优原因
先确定是目标驱动型的 JVM 调优,还是问题驱动型的 JVM 调优。
如果是目标性的 JVM 调优,那么 JVM 调优实现思路就比较简单了,如:
-
以最短停顿时间为目标的调优,只需要将垃圾收集器设置成以最短停顿时间的为目标的垃圾收集器即可,如 CMS 收集器或 G1 收集器。
-
以吞吐量为目标的调优,只需要将垃圾收集器设置为 Parallel Scavenge 和 Parallel Old 这种以吞吐量为主要目标的垃圾回收器即可。
如果是以问题驱动的 JVM 调优,那就要先分析问题是什么,然后再进行下一步的调优了。
2.分析JVM运行情况
我们可以借助于目前主流的监控工具 Prometheus + Grafana 和 JDK 自带的命令行工具,如 jps、jstat、jinfo、jstack 等进行 JVM 运行情况的分析。
主要分析的点是 Young GC 和 Full GC 的频率,以及垃圾回收的执行时间。
3.设置JVM调优参数
常见的 JVM 调优参数有以下几个:
-
调整堆内存大小:通过设置 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数来调整堆内存大小,避免频繁的垃圾回收。
-
选择合适的垃圾回收器:根据应用程序的性能需求和特点,选择合适的垃圾回收器,如 Serial GC、Parallel GC、CMS GC、G1 GC 等。
-
调整新生代和老年代比:通过设置 -XX:NewRatio 参数来调整新生代和老年代的比例,优化内存分配。
-
设置合适的堆中的各个区域比例:通过设置 -XX:SurvivorRatio 参数和 -XX:MaxTenuringThreshold 参数来调整 Eden 区、Survivor 区和老年代的比例,避免过早晋升和过多频繁的垃圾回收。
-
设置对象从年轻代进入老年代的年龄值:-XX:InitialTenuringThreshold=7 表示 7 次年轻代存活的对象就会进入老年代。
-
设置元空间大小:在 JDK 1.8 版本中,元空间的默认大小会根据操作系统有所不同。具体来说,在 Windows 上,元空间的默认大小为 21MB;而在 Linux 上,其默认大小为 24MB。然而如果元空间不足也有可能触发 Full GC 从而导致程序执行变慢,因此我们可以通过 -XX:MaxMetaspaceSize=设置元空间的最大容量。
4.压测观测调优后的效果
JVM 参数调整之后,我们要通过压力测试来观察 JVM 参数调整前和调整后的差别,以确认调整后的效果。
5.应用调优后的配置
场景分析
1、cpu占用过高
cpu占用过高要分情况讨论,是不是业务上在搞活动,突然有大批的流量进来,而且活动结束后cpu占用率就下降了,如果是这种情况其实可以不用太关心,因为请求越多,需要处理的线程数越多,这是正常的现象。话说回来,如果你的服务器配置本身就差,cpu也只有一个核心,这种情况,稍微多一点流量就真的能够把你的cpu资源耗尽,这时应该考虑先把配置提升吧。
第二种情况,cpu占用率长期过高 ,这种情况下可能是你的程序有那种循环次数超级多的代码,甚至是出现死循环了。排查步骤如下:
(1)用top命令查看cpu占用情况
这样就可以定位出cpu过高的进程。在linux下,top命令获得的进程号和jps工具获得的vmid是相同的:
(2)用top -Hp命令查看线程的情况
可以看到是线程id为7287这个线程一直在占用cpu
(3)把线程号转换为16进制
[root@localhost ~]# printf "%x" 7287
1c77
记下这个16进制的数字,下面我们要用
(4)用jstack工具查看线程栈情况
[root@localhost ~]# jstack 7268 | grep 1c77 -A 10
"http-nio-8080-exec-2" #16 daemon prio=5 os_prio=0 tid=0x00007fb66ce81000 nid=0x1c77 runnable [0x00007fb639ab9000]
java.lang.Thread.State: RUNNABLE
at com.spareyaya.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19)
at com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
通过jstack工具输出现在的线程栈,再通过grep命令结合上一步拿到的线程16进制的id定位到这个线程的运行情况,其中jstack后面的7268是第(1)步定位到的进程号,grep后面的是(2)、(3)步定位到的线程号。
从输出结果可以看到这个线程处于运行状态,在执行com.spareyaya.jvm.service.EndlessLoopService.service
这个方法,代码行号是19行,这样就可以去到代码的19行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。
2、死锁
死锁并没有第一种场景那么明显,web应用肯定是多线程的程序,它服务于多个请求,程序发生死锁后,死锁的线程处于等待状态(WAITING或TIMED_WAITING),等待状态的线程不占用cpu,消耗的内存也很有限,而表现上可能是请求没法进行,最后超时了。在死锁情况不多的时候,这种情况不容易被发现。
可以使用jstack工具来查看
(1)jps查看java进程
[root@localhost ~]# jps -l
8737 sun.tools.jps.Jps
8682 jvm-0.0.1-SNAPSHOT.jar
(2)jstack查看死锁问题
由于web应用往往会有很多工作线程,特别是在高并发的情况下线程数更多,于是这个命令的输出内容会十分多。jstack最大的好处就是会把产生死锁的信息(包含是什么线程产生的)输出到最后,所以我们只需要看最后的内容就行了
Java stack information for the threads listed above:
===================================================
"Thread-4":
at com.spareyaya.jvm.service.DeadLockService.service2(DeadLockService.java:35)
- waiting to lock <0x00000000f5035ae0> (a java.lang.Object)
- locked <0x00000000f5035af0> (a java.lang.Object)
at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$1(JVMController.java:41)
at com.spareyaya.jvm.controller.JVMController$$Lambda$457/1776922136.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Thread-3":
at com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)
- waiting to lock <0x00000000f5035af0> (a java.lang.Object)
- locked <0x00000000f5035ae0> (a java.lang.Object)
at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37)
at com.spareyaya.jvm.controller.JVMController$$Lambda$456/474286897.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
发现了一个死锁,原因也一目了然。
3、内存泄漏
我们都知道,java和c++的最大区别是前者会自动收回不再使用的内存,后者需要程序员手动释放。在c++中,如果我们忘记释放内存就会发生内存泄漏。但是,不要以为jvm帮我们回收了内存就不会出现内存泄漏。
程序发生内存泄漏后,进程的可用内存会慢慢变少,最后的结果就是抛出OOM错误。发生OOM错误后可能会想到是内存不够大,于是把-Xmx参数调大,然后重启应用。这么做的结果就是,过了一段时间后,OOM依然会出现。最后无法再调大最大堆内存了,结果就是只能每隔一段时间重启一下应用。
内存泄漏的另一个可能的表现是请求的响应时间变长了。这是因为频繁发生的GC会暂停其它所有线程(Stop The World)造成的。
为了模拟这个场景,使用了以下的程序
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
Main main = new Main();
while (true) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
main.run();
}
}
private void run() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
// do something...
});
}
}
}
运行参数是-Xms20m -Xmx20m -XX:+PrintGC
,把可用内存调小一点,并且在发生gc时输出信息,运行结果如下
[GC (Allocation Failure) 12776K->10840K(18432K), 0.0309510 secs]
[GC (Allocation Failure) 13400K->11520K(18432K), 0.0333385 secs]
[GC (Allocation Failure) 14080K->12168K(18432K), 0.0332409 secs]
[GC (Allocation Failure) 14728K->12832K(18432K), 0.0370435 secs]
[Full GC (Ergonomics) 12832K->12363K(18432K), 0.1942141 secs]
[Full GC (Ergonomics) 14923K->12951K(18432K), 0.1607221 secs]
[Full GC (Ergonomics) 15511K->13542K(18432K), 0.1956311 secs]
...
[Full GC (Ergonomics) 16382K->16381K(18432K), 0.1734902 secs]
[Full GC (Ergonomics) 16383K->16383K(18432K), 0.1922607 secs]
[Full GC (Ergonomics) 16383K->16383K(18432K), 0.1824278 secs]
[Full GC (Allocation Failure) 16383K->16383K(18432K), 0.1710382 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1829138 secs]
[Full GC (Ergonomics) Exception in thread "main" 16383K->16382K(18432K), 0.1406222 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1392928 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1546243 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1755271 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1699080 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1697982 secs]
[Full GC (Ergonomics) 16383K->16382K(18432K), 0.1851136 secs]
[Full GC (Allocation Failure) 16382K->16382K(18432K), 0.1655088 secs]
java.lang.OutOfMemoryError: Java heap space
可以看到虽然一直在gc,占用的内存却越来越多,说明程序有的对象无法被回收。但是上面的程序对象都是定义在方法内的,属于局部变量,局部变量在方法运行结果后,所引用的对象在gc时应该被回收啊,但是这里明显没有。
为了找出到底是哪些对象没能被回收,我们加上运行参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.bin
,意思是发生OOM时把堆内存信息dump出来。运行程序直至异常,于是得到heap.dump文件,然后我们借助eclipse的MAT插件来分析,如果没有安装需要先安装。
然后File->Open Heap Dump… ,然后选择刚才dump出来的文件,选择Leak Suspects
MAT会列出所有可能发生内存泄漏的对象
可以看到居然有21260个Thread对象,3386个ThreadPoolExecutor对象,如果你去看一下java.util.concurrent.ThreadPoolExecutor
的源码,可以发现线程池为了复用线程,会不断地等待新的任务,线程也不会回收,需要调用其shutdown()
方法才能让线程池执行完任务后停止。
其实线程池定义成局部变量,好的做法是设置成单例。
上面只是其中一种处理方法
在线上的应用,内存往往会设置得很大,这样发生OOM再把内存快照dump出来的文件就会很大,可能大到在本地的电脑中已经无法分析了(因为内存不足够打开这个dump文件)。这里介绍另一种处理办法:
(1)用jps定位到进程号
C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jps -l
24836 org.example.net.Main
62520 org.jetbrains.jps.cmdline.Launcher
129980 sun.tools.jps.Jps
136028 org.jetbrains.jps.cmdline.Launcher
因为已经知道了是哪个应用发生了OOM,这样可以直接用jps找到进程号135988
(2)用jstat分析gc活动情况
jstat是一个统计java进程内存使用情况和gc活动的工具,参数可以有很多,可以通过jstat -help
查看所有参数以及含义
C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jstat -gcutil -t -h8 24836 1000
Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
29.1 32.81 0.00 23.48 85.92 92.84 84.13 14 0.339 0 0.000 0.339
30.1 32.81 0.00 78.12 85.92 92.84 84.13 14 0.339 0 0.000 0.339
31.1 0.00 0.00 22.70 91.74 92.72 83.71 15 0.389 1 0.233 0.622
上面是命令意思是输出gc的情况,输出时间,每8行输出一个行头信息,统计的进程号是24836,每1000毫秒输出一次信息。
输出信息是Timestamp是距离jvm启动的时间,S0、S1、E是新生代的两个Survivor和Eden,O是老年代区,M是Metaspace,CCS使用压缩比例,YGC和YGCT分别是新生代gc的次数和时间,FGC和FGCT分别是老年代gc的次数和时间,GCT是gc的总时间。虽然发生了gc,但是老年代内存占用率根本没下降,说明有的对象没法被回收(当然也不排除这些对象真的是有用)。
(3)用jmap工具dump出内存快照
jmap可以把指定java进程的内存快照dump出来,效果和第一种处理办法一样,不同的是它不用等OOM就可以做到,而且dump出来的快照也会小很多。
jmap -dump:live,format=b,file=heap.bin 24836
这时会得到heap.bin的内存快照文件,然后就可以用eclipse来分析了。
好文分享,一起加油!