当前位置: 首页 > 科技观察

这些常见的JVM调优场景你知道吗?

时间:2023-03-22 01:20:46 科技观察

假设你已经了解了运行时数据区和常用的垃圾回收算法,也了解了Hotspot支持的垃圾回收器。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"72871c77记下这个16进制数,我们将使用(4)使用jstack工具查看线程栈情况[root@localhost~]#jstack7268|grep1c77-A10"http-nio-8080-exec-2"#16daemonprio=5os_prio=0tid=0x00007fb66ce81000nid=0x1c77runnable[0x00007fb639ab9000]java.lang.Thread.State:RUNNABreat.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19)在com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30)在sun.reflect.NativeMethodAccessorImpl.invoke0(本地方法)在sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)在sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)在java.lang.reflect.Method.invoke(Method.java:498)在org.springframework.web。method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)在org.springframework.web.method.suppoorg.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)处的rt.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)通过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-l8737sun.tools.jps.Jps8682jvm-0.0.1-SNAPSHOT.jar(2)jstack查看web应用导致的死锁问题工作线程往往很多,尤其是在高并发的情况下,线程数更多,所以这条命令的输出量会很大。jstack最大的好处就是会把死锁信息(包括是什么线程产生的)输出到最后,所以我们只需要看最后的内容即可。上面列出的线程的Java堆栈信息:==================================================="Thread-4":atcom.spareyaya.jvm.service.DeadLockService.service2(DeadLockService.java:35)-等待锁定<0x00000000f5035ae0>(java.lang.Object)-锁定<0x00000000f5035af0>(java.lang.Object)在com.spareyaya.jvm.controller.JVMController.lambda$deadLock$1(JVMController.java:41)在com.spareyaya.jvm.controller.JVMController$$Lambda$457/1776922136.run(UnknownSource)atjava.lang.Thread.run(Thread.java:748)"Thread-3":atcom.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)-等待lock<0x00000000f5035af0>(ajava.lang.Object)-locked<0x00000000f5035ae0>(ajava.lang.Object)atcom.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37)atcom.spareyaya.jvm.controller.JVMController$$Lambda$456/474286897.run(UnknownSource)atjava.lang.Thread.run(Thread.java:748)Found1de死锁。发现死锁了,原因一目了然。3、内存泄漏我们都知道java和c++最大的区别就是前者会自动回收未使用的内存,而后者则需要程序员手动释放。在C++中,如果我们忘记释放内存,就会发生内存泄漏。但是,不要以为jvm为我们回收内存后就不会发生内存泄漏了。程序发生内存泄漏后,进程的可用内存会逐渐减少,最终的结果是OOM错误。出现OOM错误后,可能会认为是内存不够大,所以增大-Xmx参数,然后重启应用。这样做的结果就是一段时间后,还是会出现OOM。到最后再也无法增加最大堆内存,结果就是应用只能每隔一段时间重启一次。内存泄漏的另一个可能症状是请求的响应时间增加。这是因为频繁的GC会暂停所有其他线程(StopTheWorld)。为了模拟这种情况,使用了以下过程。导入java.util.concurrent.ExecutorService;导入java.util.concurrent.Executors;公共类Main{publicstaticvoidmain(String[]args){Mainmain=newMain();while(true){try{线程。睡觉(1);}catch(InterruptedExceptione){e.printStackTrace();}main.run();}}privatevoidrun(){ExecutorServiceexecutorService=Executors.newCachedThreadPool();for(inti=0;i<10;i++){executorService.execute(()->{//做某事...});}}}运行参数为-Xms20m-Xmx20m-XX:+PrintGC,减少一点可用内存,并在发生gc时输出信息,运行结果如下:...[GC(AllocationFailure)12776K->10840K(18432K),0.0309510秒][GC(分配失败)13400K->11520K(18432K),0.0333385秒][GC(分配失败)14080K->12168K(18432K),0.0332409秒][GC(分配失败)14728K->12832K(18432K),0.0370435秒][FullGC(Ergonomics)12832K->12363K(18432K),0.1942141secs][FullGC(Ergonomics)14923K->12951K(18432K),0.1607221secs][FullGC(Ergonomics)15511K->13542K(18432K),0.1secs5...6[FullGC(Ergonomics)16382K->16381K(18432K),0.1734902secs][FullGC(Ergonomics)16383K->16383K(18432K),0.1922607secs][FullGC(Ergonomics)16383K->16383Ks2),184.38Ks2(184.38Ks2)][FullGC(分配失败)16383K->16383K(18432K),0.1710382秒][FullGC(人体工程学)16383K->16382K(18432K),0.1829138秒][FullGC(人体工程学)线程“main”中的异常16383K->16382K(18432K),0.1406222秒][FullGC(分配失败)16382K->16382K(18432K),0.1392928秒][FullGC(Ergonomics)16383K->16382K(18432K),0.1546243secs][FullGC)16383K->16382K(18432K),0.1755271secs][FullGC(Ergonomics)16383K->16382K(18432K),0.1699080secs][FullGC(AllocationFailure)16382K->16382K(18432K),0.169[FullGC]7982secs(Ergonomics)16383K->16382K(18432K),0.1851136secs][FullGC(AllocationFailure)16382K->16382K(18432K),0.1655088secs]java.lang.OutOfMemoryError:Javaheapspacecanbeseenthoughithasbeengc,但是内存越来越多,说明程序中有些对象是不能回收的,但是上面的程序对象都是在方法中定义的,属于局部变量。方法运行后的结果,局部变量引用的对象应该是垃圾回收的,而这里显然不是。为了找出哪些对象不能被回收,我们添加运行参数-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=heap.bin,意思是当OOM发生时dump堆内存信息。运行程序直到出现异常,这样我们就得到了heap.dump文件,接下来我们使用eclipse的MAT插件进行分析。如果没有安装,需要先安装。然后File->OpenHeapDump...,然后选择刚才dump出来的文件,选择LeakSuspectsMAT会列出所有可能有内存泄漏的对象。可以看到实际上有21260个Thread对象,3386个ThreadPoolExecutor对象。如果你去查看java.util.concurrent.ThreadPoolExecutor的源码,可以发现线程池为了复用线程会不断等待新的任务,线程不会被回收。执行任务后需要调用它的shutdown方法停止线程池。事实上,线程池被定义为局部变量,将其设置为单例是一个很好的做法。以上只是线上应用的一种处理方式,内存往往设置的很大,这样OOM后dump出来的文件会很大,可能大到无法分析本地计算机(因为内存不足,无法打开此转储文件)。这里还有一个解决办法:(1)用jps定位进程号C:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jps-l24836org.example.net.Main62520org.jetbrains.jps.cmdline.Launcher129980sun.tools.jps.Jps136028org.jetbrains.jps.cmdline.Launcher已经知道是哪个应用程序出现了OOM,所以可以直接使用jps查找进程号135988(2)使用jstat分析gc活动。jstat是一个统计java进程内存使用和gc活动的工具。可以有很多参数。可以通过jstat-helpC:\Users\spareyaya\IdeaProjects\maven-project\target\classes\org\example\net>jstat-gcutil-t-h8248361000TimestampS0S1EOMCCSYGCYGCT查看所有参数及其含义FGCFGCTGCT29.132.810.0023.4885.9292.8484.13140.33900.0000.33930.132.810.0078.1285.9292.8484.13140.33900.0000.33931.10.000.0022.7091.7492.7283.71150.38910.2330.622Theabovecommandmeanstooutputgc,theoutputtime就是每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可以dump指定java进程的内存快照,效果和第一种处理方式一样,不同的是不需要等待OOM就可以完成,而转储的快照也会小很多。jmap-dump:live,format=b,file=heap.bin24836这时候会得到heap.bin的内存快照文件,然后就可以用eclipse分析了。4.总结以上三种都不是严格意义上的jvm调优,只是利用jvm工具来找出代码中的问题。我们jvm的主要目的是尽量减少停顿时间,提高系统的吞吐量。但是,如果不对系统进行分析就盲目设置参数,可能会得到更糟糕的结果。jvm发展到今天,各种默认的参数可能是实验室里的人经过多次测试来平衡的。适用于大部分应用场景。如果你认为你的jvm确实需要调优,一定要选择