JVM性能监控和调优
JVM性能监控和调优
JVM(Java虚拟机)调优是为了优化Java应用程序的性能和稳定性。JVM调优的目的是通过调整JVM的配置参数和优化应用程序代码,使其在给定的硬件和软件环境下达到更好的性能表现。防止出现OOM,进行JVM规划和预调优,解决程序中出现的各种OOM,减少FullGC出现的频率,解决运行慢、卡顿等问题。
性能优化的步骤
第1步:熟悉业务场景
第2步(发现问题):性能监控
-
GC 频繁
-
cpu load过高
-
OOM
-
内存泄漏
-
死锁
-
程序响应时间较长
第3步(排查问题):性能分析
-
打印GC日志,通过GCviewer或者 http://gceasy.io来分析日志信息
-
灵活运用 命令行工具,jstack,jmap,jinfo等
-
dump出堆文件,使用内存分析工具分析文件,比如jconsole/ jvisualvm / jprofiler / MAT
-
使用阿里Arthas,或jconsole,JVisualVM来实时查看JVM状态
-
jstack查看堆栈信息
第4步(解决问题):性能调优
-
适当增加内存,根据业务背景选择垃圾回收器
-
优化代码,控制内存使用
-
增加机器,分散节点压力
-
合理设置线程池线程数量
-
使用中间件提高程序效率,比如缓存,消息队列等
GC日志分析
GC日志参数
-verbose:gc 输出gc日志信息,默认输出到标准输出
-XX:+PrintGC 输出GC日志。类似:-verbose:gc
-XX:+PrintGCDetails 在发生垃圾回收时打印内存回收详细的日志,并在进程退出时输出当前内存各区域分配情况
-XX:+PrintGCTimeStamps 输出GC发生时的时间戳
-XX:+PrintGCDateStamps 输出GC发生时的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 每一次GC前和GC后,都打印堆信息
-Xloggc:<file> 表示把GC日志写入到一个文件中去,而不是打印到标准输出中
测试
添加运行时参数
-Xms60m -Xmx60m -XX:+PrintGCDetails
输出
也可以导出日志文件,上传到http://gceasy.io进行在线分析
-Xms60m -Xmx60m -XX:+PrintGCDetails -Xloggc:D:\testGC.log
GC分类
-
部分收集(Partial GC):不是完整收集整个Java堆的垃圾收集。其中又分为:
-
- 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
-
- 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
-
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
-
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
日志结构
MinorGC(或young GC或YGC)日志:
[GC (Allocation Failure) [PSYoungGen: 31744K->2192K(36864K)] 31744K->2200K(121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
Full GC日志介绍:
[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K(132096K)] [ParOldGen: 416K->5453K(50176K)] 5520K->5453K(182272K), [Metaspace: 20637K->20637K(1067008K)], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
OOM:堆溢出
模拟堆溢出
@RequestMapping("/add")
public void addObject(){
System.err.println("add"+peopleSevice);
ArrayList<People> people = new ArrayList<>();
while (true){
people.add(new People());
}
}
初始参数配置:
-Xms30M -Xmx30M
// 发生oom会导出一个dump文件
-XX:+PrintGCDetails -XX:MetaspaceSize=64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump.hprof
-XX:+PrintGCDateStamps -Xms200M -Xmx200M -Xloggc:log/gc-oomHeap.log
报错信息
java.lang.OutOfMemoryError: Java heap space
dump文件分析
- jvisualvm工具分析堆内存文件heapdump.hprof
- 使用MAT工具查看,能找到对应的线程及相应线程中对应实例的位置和代码:
gc日志分析
将日志文件导入http://gceasy.io 上面讲了
运行时参数如果没设置堆转储,Jmap命令导出一个即可
jmap -dump:format=b,file=<filename.hprof> <pid>
jmap -dump:live,format=b,file=<filename.hprof> <pid>
OOM:元空间溢出
元空间存储数据类型
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
模拟元空间溢出
@RequestMapping("/metaSpaceOom")
public void metaSpaceOom(){
ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean();
while (true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(People.class);
enhancer.setUseCache(false);
enhancer.setUseCache(true);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {
System.out.println("我是加强类,输出print之前的加强方法");
return methodProxy.invokeSuper(o,objects);
});
People people = (People)enhancer.create();
people.print();
System.out.println(people.getClass());
System.out.println("totalClass:" + classLoadingMXBean.getTotalLoadedClassCount());
System.out.println("activeClass:" + classLoadingMXBean.getLoadedClassCount());
System.out.println("unloadedClass:" + classLoadingMXBean.getUnloadedClassCount());
}
}
初始参数
-XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512K -XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX:SurvivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading
-XX:+PrintGCDateStamps -Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log
dump文件分析
JDK8后,元空间替换了永久代,元空间使用的是本地内存
原因及解决方案
原因:
-
运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
-
应用长时间运行,没有重启
-
元空间内存设置过小
解决方法:
因为该 OOM 原因比较简单,解决方法有如下几种:
-
检查是否永久代空间或者元空间设置的过小
-
检查代码中是否存在大量的反射操作
-
dump之后通过mat检查是否存在大量由于反射生成的代理类
OOM:GC overhead limit exceeded
模拟代码
public class OOMTest {
public static void main(String[] args) {
test1();
// test2();
}
public static void test1() {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(UUID.randomUUID().toString().intern());
i++;
}
} catch (Throwable e) {
System.out.println("************i: " + i);
e.printStackTrace();
throw e;
}
}
public static void test2() {
String str = "";
Integer i = 1;
try {
while (true) {
i++;
str += UUID.randomUUID();
}
} catch (Throwable e) {
System.out.println("************i: " + i);
e.printStackTrace();
throw e;
}
}
}
代码解析
第一段代码:运行期间将内容放入常量池的典型案例
intern()方法
-
如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;
-
如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用
第二段代码:不停的追加字符串str
为什么第二个没有报GC overhead limit exceeded呢?以上两个demo的区别在于:
-
Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出
-
GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。
报错信息:
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7110K->7095K(7168K)] 9158K->9143K(9728K), [Metaspace: 3177K->3177K(1056768K)], 0.0479640 secs] [Times: user=0.23 sys=0.01, real=0.05 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7114K->7096K(7168K)] 9162K->9144K(9728K), [Metaspace: 3198K->3198K(1056768K)], 0.0408506 secs] [Times: user=0.22 sys=0.01, real=0.04 secs]
通过查看GC日志可以发现,系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆DUMP文件来具体分析
dump文件分析
和上面两个相同,可自行分析
解决方案
原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
解决方法:
-
检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
-
添加参数
-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。 -
dump内存,检查是否存在内存泄漏,如果没有,加大内存。
OOM:线程溢出
模拟代码
public class TestNativeOutOfMemoryError {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("i = " + i);
new Thread(new HoldThread()).start();
}
}
}
class HoldThread extends Thread {
CountDownLatch cdl = new CountDownLatch(1);
@Override
public void run() {
try {
cdl.await();
} catch (InterruptedException e) {
}
}
}
报错信息
java.lang.OutOfMemoryError : unable to create new native Thread
原因和解决方案
出现这种异常,基本上都是创建了大量的线程导致的
解决一
通过 -Xss 设置每个线程栈大小的容量
-
JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
-
正常情况下,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-
能创建的线程数的具体计算公式如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
——————————————————————————————————————
MaxProcessMemory 指的是进程可寻址的最大空间
JVMMemory JVM内存
ReservedOsMemory 保留的操作系统内存
ThreadStackSize 线程栈的大小
——————————————————————————————————————
解决二
-
在Java语言里, 当你创建一个线程的时候,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。
-
由公式得出结论:你给JVM内存越多,那么你能创建的线程越少,越容易发生java.lang.OutOfMemoryError: unable to create new native thread
综上,在生产环境下如果需要更多的线程数量,建议使用64位操作系统,如果必须使用32位操作系统,可以通过调整Xss的大小来控制线程数量。
线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
-
/proc/sys/kernel/pid_max 系统最大pid值,在大型系统里可适当调大
-
/proc/sys/kernel/threads-max 系统允许的最大线程数
-
maxuserprocess(ulimit -u) 系统限制某用户下最多可以运行多少进程或线程
-
/proc/sys/vm/max_map_count
max_map_count文件包含限制一个进程可以拥有的VMA(虚拟内存区域)的数量。虚拟内存区域是一个连续的虚拟地址空间区域。在进程的生命周期中,每当程序尝试在内存中映射文件,链接到共享内存段,或者分配堆空间的时候,这些区域将被创建。调优这个值将限制进程可拥有VMA的数量。限制一个进程拥有VMA的总数可能导致应用程序出错,因为当进程达到了VMA上线但又只能释放少量的内存给其他的内核进程使用时,操作系统会抛出内存不足的错误。如果你的操作系统在NORMAL区域仅占用少量的内存,那么调低这个值可以帮助释放内存给内核用。
性能优化一:合理配置堆内存
增加内存可以提高系统的性能而且效果显著,那么随之带来的一个问题就是,我们增加多少内存比较合适?如果内存过大,那么如果产生FullGC的时候,GC时间会相对比较长,如果内存较小,那么就会频繁的触发GC,在这种情况下,我们该如何合理的适配堆内存大小呢?
分析
依据的原则是根据Java Performance里面的推荐公式来进行设置。
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。 方法区(永久代 PermSize和MaxPermSize 或 元空间 MetaspaceSize 和 MaxMetaspaceSize)设置为老年代存活对象的1.2-1.5倍。 年轻代Xmn的设置为老年代存活对象的1-1.5倍。 老年代的内存大小设置为老年代存活对象的2-3倍。
但是,上面的说法也不是绝对的,也就是说这给的是一个参考值,根据多种调优之后得出的一个结论,大家可以根据这个值来设置一下我们的初始化内存,在保证程序正常运行的情况下,我们还要去查看GC的回收率,GC停顿耗时,内存里的实际数据来判断,Full GC是基本上不能有的,如果有就要做内存Dump分析,然后再去做一个合理的内存分配。
我们还要注意到一点就是,上面说的老年代存活对象怎么去判定
判定:
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)。
总结
在内存相对紧张的情况下,可以按照上述的方式来进行内存的调优, 找到一个在GC频率和GC耗时上都可接受的一个内存设置,可以用较小的内存满足当前的服务需要。
但当内存相对宽裕的时候,可以相对给服务多增加一点内存,可以减少GC的频率,GC的耗时相应会增加一些。 一般要求低延时的可以考虑多设置一点内存, 对延时要求不高的,可以按照上述方式设置较小内存。
如果在垃圾回收日志中观察到OutOfMemoryError,尝试把Java堆的大小扩大到物理内存的80%~90%。尤其需要注意的是堆空间导致的OutOfMemoryError以及一定要增加空间。
-
比如说,增加-Xms和-Xmx的值来解决old代的OutOfMemoryError
-
增加-XX:PermSize和-XX:MaxPermSize来解决permanent代引起的OutOfMemoryError(jdk7之前);增加-XX:MetaspaceSize和-XX:MaxMetaspaceSize来解决Metaspace引起的OutOfMemoryError(jdk8之后)
记住一点Java堆能够使用的容量受限于硬件以及是否使用64位的JVM。在扩大了Java堆的大小之后,再检查垃圾回收日志,直到没有OutOfMemoryError为止。如果应用运行在稳定状态下没有OutOfMemoryError就可以进入下一步了,计算活动对象的大小。
性能优化二:JIT优化
可见这篇文章的逃逸分析内容
性能优化三:分析CPU占用超高
如果是生产环境的话,是怎么样才能发现目前程序有问题呢?我们可以推导一下,如果线程死锁,那么线程一直在占用CPU,这样就会导致CPU一直处于一个比较高的占用率。所示我们解决问题的思路应该是:
1.查看所有java进程 ID
jps -l
2.根据进程 ID 检查当前使用异常线程的pid
top -Hp 1456
3.当前占用cpu比较高的线程 ID 是1465
接下来把 线程 PID 转换为16进制为
# 10 进制线程PId 转换为 16 进制
1465 -------> 5b9
# 5b9 在计算机中显示为
0x5b9
4.最后我们把线程信息打印出来:
jstack 1456 > jstack.log
5.打开jstack.log文件 查找一下刚刚我们转换完的16进制ID是否存在
jstack命令生成的thread dump信息包含了JVM中所有存活的线程,里面确实是存在我们定位到的线程 ID ,在thread dump中每个线程都有一个nid,在nid=0x5b9的线程调用栈中,我们发现两个线程在互相等待对方释放资源
xxx大厂问题排查过程:
...省略;
4、ps aux | grep java 查看到当前java进程使用cpu、内存、磁盘的情况获取使用量异常的进程
5、top -Hp 进程pid 检查当前使用异常线程的pid
6、把线程pid变为16进制如 31695 - 》 7bcf 然后得到0x7bcf
7、jstack 进程的pid | grep -A20 0x7bcf 得到相关进程的代码