java性能实战_【从零单排】Java性能排查实战模拟

当线上发生了性能问题时,需要我们快速定位问题。本文模拟了一次内存泄漏,从零教学一步步手动排查。

模拟事故现场

使用如下代码模拟内存泄漏。起了几个问题线程(在不停地创建很大的StringBuilder)。另外还有一些常规线程。

import java.util.List;

import java.util.Random;

import java.util.Scanner;

import java.util.concurrent.Executors;

import java.util.concurrent.ExecutorService;

import static java.lang.Thread.sleep;

public class app {

public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

ExecutorService executorService = Executors.newFixedThreadPool(100);

System.out.println("Please Enter when VM is ready.");

sc.nextLine();

System.out.println("START");

// 5 threads with issues

for (int i=0; i<5; i++) {

executorService.execute(() -> {

System.out.println(Thread.currentThread().getName());

double input = new Random().nextDouble();

List l;

while (true) {

// create many string builder

for (int j=0; j<100; j++) {

StringBuilder s = new StringBuilder(Double.toString(input++));

// make it big, 100 million

for (int k=0; k<100000000; k++) {

s.append(input+k);

}

}

}

});

}

// 95 threads - good

for (int i=0; i<95; i++) {

executorService.execute(() -> {

System.out.println(Thread.currentThread().getName());

double input = new Random().nextDouble();

List l;

while (true) {

StringBuilder s = new StringBuilder(Double.toString(input++));

try {

sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

});

}

}

}

把代码部署到服务器上。接下来,我们需要找一个open的端口给JMX用。

使用命令查看已经占用的端口:lsof -i -P -n | grep LISTEN。(也可以用:netstat -tulpn | grep LISTEN)

f8f6a1de33bb49215e84a311beae1f4a.png

挑一个不在上述列表里的,这里就用7788吧。于是就可以跑起来了。

// 编译

javac app.java

// 运行

java -Dcom.sun.management.jmxremote.port=7788 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -Xloggc:/home/username/LOGS/test_my_gc_log.log app

这里要加上jmxremote的三个参数。第一个指定了端口,后两个设置了无需密码。这是为了local的visualvm可以远程连上server上的JVM。

-Dcom.sun.management.jmxremote.port=7788

-Dcom.sun.management.jmxremote.ssl=false

-Dcom.sun.management.jmxremote.authenticate=false

另外,还使用了gc的参数。第一个表示要打印垃圾回收的log,第二个指定了log的位置。这时为了保留gc日志方便以后分析。

-verbose:gc

-Xloggc:/home/username/LOGS/test_my_gc_log.log

现在,我们把事故现场模拟好了。但是实际发生问题的时候,我们一开始是不知道具体细节的,需要一步步排查。让我们把上面的代码都忘掉,从头开始。

top命令

首先,使用top命令查看服务器上占用CPU,Memory资源最多的进程(Top Process's Resource Usage)。

630316e63d93ffb6873d2dfdcfc2ce09.png

这里,我们定位到PID为18264的这个java进程,发现它的CPU使用率为773.4%,MEM使用率为11.7%。估计问题出在这儿。

(当然,实际生产环境发生内存泄漏问题,一般MEM会撑爆,这里仅仅作为测试用例,占用了近30个G的内存,没有把内存撑满。)

其次,使用top -H p18264观察该进程下的线程资源使用情况。(18264是Java进程的PID)

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

18413 eufiudwq 20 0 42.4g 21.9g 13220 R 83.7 11.7 1:36.71 java

18414 eufiudwq 20 0 42.4g 21.9g 13220 R 83.7 11.7 1:37.18 java

18415 eufiudwq 20 0 42.4g 21.9g 13220 R 83.3 11.7 1:36.75 java

18416 eufiudwq 20 0 42.4g 21.9g 13220 R 83.3 11.7 1:36.74 java

19060 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.05 java

19061 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.06 java

19062 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.13 java

19063 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.14 java

19065 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.11 java

19066 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.10 java

19067 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.12 java

19068 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.10 java

19069 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.13 java

19070 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.10 java

19071 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.13 java

19072 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.00 java

19074 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:09.99 java

19075 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:09.92 java

19076 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:09.94 java

19077 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.09 java

19078 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.08 java

19079 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.13 java

19080 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.09 java

19081 eufiudwq 20 0 42.4g 21.9g 13220 S 12.0 11.7 0:10.09 java

可以看到,起了很多线程,需要特别关注的是头部的PID为18413,18414,18415,18416这几个,CPU和MEM占用都很大。其它的资源占用相对较小。

jvisualvm工具

下面,我们用jvisualvm远程连接到server端,查看Java进程运行时CPU,堆栈内存,线程使用情况(Overall JVM Status)。

(jvisualvm以及下述的jstack,jstat,jmap都是位于Javajdk/bin目录下)

f79569b1a28256045e10faf3a7bd5ded.png

查看Threads页面,发现主要是pool-1-thread-5,pool-1-thread-4,pool-1-thread-3,pool-1-thread-2这几个线程跑了很多时间,且在Running状态,其它的大多都在sleep。

这几个,应该就是对应到上面说的PID为18413,18414,18415,18416的线程。

a1b86f208210253da7d79ecca6af3f53.png

Thread Dump

这里,我们想要深入到Thread内部,看看具体某一时刻有问题的线程在干啥。

Method 1

在jvisualvm中,使用 -> Thread Dump。再定位到可能有问题的pool-1-thread-3中,得到log如下:

"pool-1-thread-3" - Thread t@33

java.lang.Thread.State: RUNNABLE

at sun.misc.FloatingDecimal$BinaryToASCIIBuffer.access$100(FloatingDecimal.java:259)

at sun.misc.FloatingDecimal.getBinaryToASCIIConverter(FloatingDecimal.java:1785)

at sun.misc.FloatingDecimal.getBinaryToASCIIConverter(FloatingDecimal.java:1738)

at sun.misc.FloatingDecimal.appendTo(FloatingDecimal.java:89)

at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:736)

at java.lang.StringBuilder.append(StringBuilder.java:226)

at app.lambda$main$0(app.java:28)

at app$$Lambda$9/1681433494.run(Unknown Source)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)

at java.lang.Thread.run(Thread.java:748)

Locked ownable synchronizers:

- locked <3d71d552> (a java.util.concurrent.ThreadPoolExecutor$Worker)

发现这一行:at java.lang.StringBuilder.append(StringBuilder.java:226),猜想可能创建了很多StringBuilder,造成了大量的内存占用。

Method 2

另外一种方法是用jstack,在server上可以直接把thread信息dump到一个文件,然后分析:jstack 18264 > jstack.txt (18264是Java进程的PID)

jstack出来的log,id是16进制,上面top看到的是十进制,需要进行转换。比如18413(10进制)这个,转换之后为47ed(16进制)。找到对应的log如下:

"pool-1-thread-2" #32 prio=5 os_prio=0 tid=0x00007fa38c27a000 nid=0x47ed runnable [0x00007fa3455a8000]

java.lang.Thread.State: RUNNABLE

at sun.misc.FloatingDecimal$BinaryToASCIIBuffer.access$100(FloatingDecimal.java:259)

at sun.misc.FloatingDecimal.getBinaryToASCIIConverter(FloatingDecimal.java:1785)

at sun.misc.FloatingDecimal.getBinaryToASCIIConverter(FloatingDecimal.java:1738)

at sun.misc.FloatingDecimal.appendTo(FloatingDecimal.java:89)

at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:736)

at java.lang.StringBuilder.append(StringBuilder.java:226)

at app.lambda$main$0(app.java:28)

at app$$Lambda$9/1681433494.run(Unknown Source)

at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)

at java.lang.Thread.run(Thread.java:748)

结论和方法1是一致的。

jstat - GC status

下面,我们来看看GC情况:jstat -gcutil 18264 2000 10

参数说明:

18264是Java进程的PID

2000:每间隔2000毫秒收集一次

10:收集10次

403f277cfd18d5a9b24a1338eefe4187.png

分析可知,Eden区和Old区占用超过了一半以上,有15次Full GC,一共花了大概9秒。

(服务器上内存并没有占满,我们只拿了30G左右来测试,所以不需要频繁FGC。而正常线上发生内存泄漏问题时,往往伴随着频繁的FGC。)

Usage in Class Level

从上面的分析,我们已经大概知道问题可能出现在StringBuilder。下面从Class level分析,希望能进一步证实我们的推断。

Method 1

在local,使用jvisualvm查看 -> Sampler -> Memory

6b26125c65bf9c819f07f66f4830a24b.png

可以看到,大部分的占用在char[],其实StringBuilder.append底层是调用char进行拼接的,jdk代码如下:

// AbstractStringBuilder.java

public AbstractStringBuilder append(String str) {

if (str == null)

return appendNull();

int len = str.length();

ensureCapacityInternal(count + len);

str.getChars(0, len, value, count);

count += len;

return this;

}

由此,我们可以确信,是程序中的某一个部分使用StringBuilder时出了问题。

Method 2

在server端,使用jmap -histo 18264查看。(18264是Java进程的PID)

num #instances #bytes class name

----------------------------------------------

1: 17874 21878767104 [C

2: 1366 23338808 [I

3: 1842 207544 java.lang.Class

4: 6524 156576 java.lang.String

5: 5503 132072 java.lang.StringBuilder

6: 1304 114752 java.lang.reflect.Method

7: 346 113480 [B

8: 2294 105424 [Ljava.lang.Object;

9: 125 47000 java.lang.Thread

10: 1195 38240 java.util.HashMap$Node

11: 1547 34736 [Ljava.lang.Class;

12: 521 25008 java.util.HashMap

13: 345 24840 java.lang.reflect.Field

14: 309 24720 java.lang.reflect.Constructor

15: 255 22456 [Ljava.util.HashMap$Node;

16: 557 22280 java.lang.ref.SoftReference

17: 411 13152 java.util.concurrent.ConcurrentHashMap$Node

18: 398 12088 [Ljava.lang.String;

19: 204 11424 java.lang.invoke.MemberName

20: 357 11424 java.util.Hashtable$Entry

21: 472 11328 java.lang.Long

22: 341 10912 sun.misc.FDBigInteger

23: 101 10504 java.io.ObjectStreamClass

24: 127 10320 [Ljava.util.Hashtable$Entry;

25: 237 9480 java.util.LinkedHashMap$Entry

26: 100 8000 [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;

27: 195 7800 java.io.ObjectStreamField

28: 219 7008 java.util.Vector

29: 96 6912 sun.reflect.DelegatingClassLoader

30: 203 6496 java.lang.invoke.LambdaForm$Name

31: 113 6328 java.lang.Class$ReflectionData

32: 191 6112 java.io.ObjectStreamClass$WeakClassKey

33: 119 5712 java.util.Hashtable

34: 142 5680 java.lang.invoke.MethodType

35: 94 5640 [Ljava.lang.ref.SoftReference;

36: 139 5560 java.security.AccessControlContext

37: 321 5136 java.lang.Object

38: 102 4896 java.util.concurrent.ThreadPoolExecutor$Worker

39: 27 4784 [Ljava.util.concurrent.ConcurrentHashMap$Node;

40: 144 4608 java.lang.invoke.MethodType$ConcurrentWeakInternSet$WeakEntry

41: 287 4592 java.lang.Integer

42: 191 4584 javax.management.ImmutableDescriptor

43: 113 4520 java.security.ProtectionDomain

44: 61 4448 [Ljava.lang.reflect.Method;

45: 110 4400 java.util.TreeMap$Entry

46: 109 4360 java.util.WeakHashMap$Entry

47: 130 4160 com.sun.jmx.mbeanserver.ConvertingMethod

结论和方法1是一致的。

总结

首先,我们使用了top查看服务器上高占用的进程,继而定位到某些问题线程。

其次,我们使用了jvisualvm查看某一个进程的运行情况。

接着,我们使用了jvisualvm的Thread Dump,或者jstack查看问题线程的状态。

然后,我们使用了jstat查看GC情况。

最后,我们使用了jvisualvm的Sampler,或者jmap,查看高占用的类。

性能排查,是一项有挑战的活。有可能在中间的某一步,就发现了问题;也可能所有的步骤都做完了,还是没能发现问题。这就需要我们抓住每次机会,积累经验,细致思考,发散思维。

Reference

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值