关于jvm:这几种常见的-JVM-调优场景你知道吗

34次阅读

共计 6913 个字符,预计需要花费 18 分钟才能阅读完成。

假设你曾经理解了运行时的数据区域和罕用的垃圾回收算法,也理解了 Hotspot 反对的垃圾回收器。

一、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 行,找到其所在的代码块,看看是不是处于循环中,这样就定位到了问题。

二、死锁

死锁并没有第一种场景那么显著,web 利用必定是多线程的程序,它服务于多个申请,程序产生死锁后,死锁的线程处于期待状态(WAITINGTIMED_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.

发现了一个死锁,起因也高深莫测。

三、内存透露

咱们都晓得,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 是新生代的两个 SurvivorEden,O 是老年代区,M 是 Metaspace,CCS 应用压缩比例,YGC 和 YGCT 别离是新生代 gc 的次数和工夫,FGCFGCT别离是老年代 gc 的次数和工夫,GCT 是 gc 的总工夫。尽管产生了 gc,然而老年代内存占用率基本没降落,阐明有的对象没法被回收(当然也不排除这些对象真的是有用)。

(3)用 jmap 工具 dump 出内存快照

jmap 能够把指定 java 过程的内存快照 dump 进去,成果和第一种解决方法一样,不同的是它不必等 OOM 就能够做到,而且 dump 进去的快照也会小很多。

jmap -dump:live,format=b,file=heap.bin 24836

这时会失去 heap.bin 的内存快照文件,而后就能够用 eclipse 来剖析了。

四、总结

以上三种严格地说还算不上 jvm 的调优,只是用了 jvm 工具把代码中存在的问题找了进去。咱们进行 jvm 的次要目标是尽量减少进展工夫,进步零碎的吞吐量。

然而如果咱们没有对系统进行剖析就自觉去设置其中的参数,可能会失去更坏的后果,jvm 倒退到明天,各种默认的参数可能是实验室的人通过屡次的测试来做均衡的,实用大多数的利用场景。

如果你认为你的 jvm 的确有调优的必要,也务必要取样剖析,最初还得缓缓屡次调节,才有可能失去更优的成果。

正文完
 0