first commit
This commit is contained in:
355
专栏/JVM核心技术32讲(完)/28JVM问题排查分析下篇(案例实战).md
Normal file
355
专栏/JVM核心技术32讲(完)/28JVM问题排查分析下篇(案例实战).md
Normal file
@ -0,0 +1,355 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
28 JVM 问题排查分析下篇(案例实战)
|
||||
GC 问题排查实战案例
|
||||
|
||||
这一部分,我们来看一个实际的案例。
|
||||
|
||||
假设我们有一个提供高并发请求的服务,系统使用 Spring Boot 框架,指标采集使用 MicroMeter,监控数据上报给 Datadog 服务。
|
||||
|
||||
当然,Micrometer支 持将数据上报给各种监控系统,例如:AppOptics、Atlas、Datadog、Dynatrace、Elastic、Ganglia、Graphite、Humio、Influx、Instana、JMX、KairosDB、New Relic、Prometh eus、SignalFx、Stackdriver、StatsD、Wavefront 等等。
|
||||
|
||||
有关MicroMeter的信息可参考:
|
||||
|
||||
|
||||
https://micrometer.io/docs
|
||||
|
||||
|
||||
问题现象描述
|
||||
|
||||
最近一段时间,通过监控指标发现,有一个服务节点的最大 GC 暂停时间经常会达到 400ms 以上。
|
||||
|
||||
如下图所示:
|
||||
|
||||
|
||||
|
||||
从图中可以看到,GC 暂停时间的峰值达到了 546ms,这里展示的时间点是 2020 年 02 月 04 日 09:20:00 左右。
|
||||
|
||||
客户表示这种情况必须解决,因为服务调用的超时时间为 1s,要求最大 GC 暂停时间不超过 200ms,平均暂停时间达到 100ms 以内,对客户的交易策略产生了极大的影响。
|
||||
|
||||
CPU 负载
|
||||
|
||||
CPU 的使用情况如下图所示:
|
||||
|
||||
|
||||
|
||||
从图中可以看到:系统负载为 4.92,CPU使用率 7% 左右,其实这个图中隐含了一些重要的线索,但我们此时并没有发现什么问题。
|
||||
|
||||
GC 内存使用情况
|
||||
|
||||
然后我们排查了这段时间的内存使用情况:
|
||||
|
||||
|
||||
|
||||
从图中可以看到,大约 09:25 左右 old_gen 使用量大幅下跌,确实是发生了 FullGC。
|
||||
|
||||
但 09:20 前后,老年代空间的使用量在缓慢上升,并没有下降,也就是说引发最大暂停时间的这个点并没有发生 FullGC。
|
||||
|
||||
当然,这些是事后复盘分析得出的结论。当时对监控所反馈的信息并不是特别信任,怀疑就是触发了 FullGC 导致的长时间 GC 暂停。
|
||||
|
||||
|
||||
为什么有怀疑呢,因为 Datadog 这个监控系统,默认 10s 上报一次数据。有可能在这 10s 内发生些什么事情但是被漏报了(当然,这是不可能的,如果上报失败会在日志系统中打印相关的错误)。
|
||||
|
||||
|
||||
再分析上面这个图,可以看到老年代对应的内存池是 “ps_old_gen”,通过前面的学习,我们知道,ps 代表的是 ParallelGC 垃圾收集器。
|
||||
|
||||
JVM 启动参数
|
||||
|
||||
查看 JVM 的启动参数,发现是这样的:
|
||||
|
||||
-Xmx4g -Xms4g
|
||||
|
||||
|
||||
|
||||
我们使用的是 JDK 8,启动参数中没有指定 GC,确定这个服务使用了默认的并行垃圾收集器。
|
||||
|
||||
于是怀疑问题出在这款垃圾收集器上面,因为很多情况下 ParallelGC 为了最大的系统处理能力,即吞吐量,而牺牲掉了单次的暂停时间,导致暂停时间会比较长。
|
||||
|
||||
使用 G1 垃圾收集器
|
||||
|
||||
怎么办呢?准备换成 G1,毕竟现在新版本的 JDK 8 中 G1 很稳定,而且性能不错。
|
||||
|
||||
然后换成了下面的启动参数:
|
||||
|
||||
# 这个参数有问题,启动失败
|
||||
-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMills=50ms
|
||||
|
||||
|
||||
|
||||
结果启动失败,忙中出错,参数名和参数值都写错了。
|
||||
|
||||
修正如下:
|
||||
|
||||
-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50
|
||||
|
||||
|
||||
|
||||
接着服务启动成功,等待健康检测自动切换为新的服务节点,继续查看指标。
|
||||
|
||||
|
||||
|
||||
看看暂停时间,每个节点的 GC 暂停时间都降下来了,基本上在 50ms 以内,比较符合我们的预期。
|
||||
|
||||
嗯!事情到此结束了?远远没有。
|
||||
|
||||
“彩蛋”惊喜
|
||||
|
||||
过了一段时间,我们发现了个下面这个惊喜(也许是惊吓),如下图所示:
|
||||
|
||||
|
||||
|
||||
中奖了,运行一段时间后,最大 GC 暂停时间达到了 1300ms。
|
||||
|
||||
情况似乎更恶劣了。
|
||||
|
||||
继续观察,发现不是个别现象:
|
||||
|
||||
|
||||
|
||||
内心是懵的,觉得可能是指标算错了,比如把 10s 内的暂停时间全部加到了一起。
|
||||
|
||||
注册 GC 事件监听
|
||||
|
||||
于是想了个办法,通过 JMX 注册 GC 事件监听,把相关的信息直接打印出来。
|
||||
|
||||
关键代码如下所示:
|
||||
|
||||
// 每个内存池都注册监听
|
||||
for (GarbageCollectorMXBean mbean
|
||||
: ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
if (!(mbean instanceof NotificationEmitter)) {
|
||||
continue; // 假如不支持监听...
|
||||
}
|
||||
final NotificationEmitter emitter = (NotificationEmitter) mbean;
|
||||
// 添加监听
|
||||
final NotificationListener listener = getNewListener(mbean);
|
||||
emitter.addNotificationListener(listener, null, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
通过这种方式,我们可以在程序中监听 GC 事件,并将相关信息汇总或者输出到日志。 具体的实现代码在后面的章节《应对容器时代面临的挑战》中给出。
|
||||
|
||||
再启动一次,运行一段时间后,看到下面这样的日志信息:
|
||||
|
||||
{
|
||||
"duration":1869,
|
||||
"maxPauseMillis":1869,
|
||||
"promotedBytes":"139MB",
|
||||
"gcCause":"G1 Evacuation Pause",
|
||||
"collectionTime":27281,
|
||||
"gcAction":"end of minor GC",
|
||||
"afterUsage":
|
||||
{
|
||||
"G1 Old Gen":"1745MB",
|
||||
"Code Cache":"53MB",
|
||||
"G1 Survivor Space":"254MB",
|
||||
"Compressed Class Space":"9MB",
|
||||
"Metaspace":"81MB",
|
||||
"G1 Eden Space":"0"
|
||||
},
|
||||
"gcId":326,
|
||||
"collectionCount":326,
|
||||
"gcName":"G1 Young Generation",
|
||||
"type":"jvm.gc.pause"
|
||||
}
|
||||
|
||||
|
||||
|
||||
情况确实有点不妙。
|
||||
|
||||
这次实锤了,不是 FullGC,而是年轻代 GC,而且暂停时间达到了 1869ms。 一点道理都不讲,我认为这种情况不合理,而且观察 CPU 使用量也不高。
|
||||
|
||||
找了一大堆资料,试图证明这个 1869ms 不是暂停时间,而只是 GC 事件的结束时间减去开始时间。
|
||||
|
||||
打印 GC 日志
|
||||
|
||||
既然这些手段不靠谱,那就只有祭出我们的终极手段:打印 GC 日志。
|
||||
|
||||
修改启动参数如下:
|
||||
|
||||
-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50
|
||||
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
|
||||
|
||||
|
||||
|
||||
重新启动,希望这次能排查出问题的原因。
|
||||
|
||||
|
||||
|
||||
运行一段时间,又发现了超长的暂停时间。
|
||||
|
||||
分析 GC 日志
|
||||
|
||||
因为不涉及敏感数据,那么我们把 GC 日志下载到本地进行分析。
|
||||
|
||||
定位到这次暂停时间超长的 GC 事件,关键的信息如下所示:
|
||||
|
||||
Java HotSpot(TM) 64-Bit Server VM (25.162-b12) for linux-amd64 JRE (1.8.0_162-b12),
|
||||
built on Dec 19 2017 21:15:48 by "java_re" with gcc 4.3.0 20080428 (Red Hat 4.3.0-8)
|
||||
Memory: 4k page, physical 144145548k(58207948k free), swap 0k(0k free)
|
||||
CommandLine flags:
|
||||
-XX:InitialHeapSize=4294967296 -XX:MaxGCPauseMillis=50 -XX:MaxHeapSize=4294967296
|
||||
-XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
|
||||
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
|
||||
|
||||
2020-02-24T18:02:31.853+0800: 2411.124: [GC pause (G1 Evacuation Pause) (young), 1.8683418 secs]
|
||||
[Parallel Time: 1861.0 ms, GC Workers: 48]
|
||||
[GC Worker Start (ms): Min: 2411124.3, Avg: 2411125.4, Max: 2411126.2, Diff: 1.9]
|
||||
[Ext Root Scanning (ms): Min: 0.0, Avg: 0.3, Max: 2.7, Diff: 2.7, Sum: 16.8]
|
||||
[Update RS (ms): Min: 0.0, Avg: 3.6, Max: 6.8, Diff: 6.8, Sum: 172.9]
|
||||
[Processed Buffers: Min: 0, Avg: 2.3, Max: 8, Diff: 8, Sum: 111]
|
||||
[Scan RS (ms): Min: 0.0, Avg: 0.2, Max: 0.5, Diff: 0.5, Sum: 7.7]
|
||||
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
|
||||
[Object Copy (ms): Min: 1851.6, Avg: 1854.6, Max: 1857.4, Diff: 5.8, Sum: 89020.4]
|
||||
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.6]
|
||||
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 48]
|
||||
[GC Worker Other (ms): Min: 0.0, Avg: 0.3, Max: 0.7, Diff: 0.6, Sum: 14.7]
|
||||
[GC Worker Total (ms): Min: 1858.0, Avg: 1859.0, Max: 1860.3, Diff: 2.3, Sum: 89233.3]
|
||||
[GC Worker End (ms): Min: 2412984.1, Avg: 2412984.4, Max: 2412984.6, Diff: 0.5]
|
||||
[Code Root Fixup: 0.0 ms]
|
||||
[Code Root Purge: 0.0 ms]
|
||||
[Clear CT: 1.5 ms]
|
||||
[Other: 5.8 ms]
|
||||
[Choose CSet: 0.0 ms]
|
||||
[Ref Proc: 1.7 ms]
|
||||
[Ref Enq: 0.0 ms]
|
||||
[Redirty Cards: 1.1 ms]
|
||||
[Humongous Register: 0.1 ms]
|
||||
[Humongous Reclaim: 0.0 ms]
|
||||
[Free CSet: 2.3 ms]
|
||||
[Eden: 2024.0M(2024.0M)->0.0B(2048.0K)
|
||||
Survivors: 2048.0K->254.0M
|
||||
Heap: 3633.6M(4096.0M)->1999.3M(4096.0M)]
|
||||
[Times: user=1.67 sys=14.00, real=1.87 secs]
|
||||
|
||||
|
||||
|
||||
前后的 GC 事件都很正常,也没发现 FullGC 或者并发标记周期,但找到了几个可疑的点。
|
||||
|
||||
|
||||
physical 144145548k(58207948k free):JVM 启动时,物理内存 137GB,空闲内存 55GB。
|
||||
[Parallel Time: 1861.0 ms, GC Workers: 48]:垃圾收集器工作线程 48 个。
|
||||
|
||||
|
||||
我们前面的课程中学习了怎样分析 GC 日志,一起来回顾一下。
|
||||
|
||||
|
||||
user=1.67:用户线程耗时 1.67s;
|
||||
sys=14.00:系统调用和系统等待时间 14s;
|
||||
real=1.87 secs:实际暂停时间 1.87s;
|
||||
GC 之前,年轻代使用量 2GB,堆内存使用量 3.6GB,存活区 2MB,可推断出老年代使用量 1.6GB;
|
||||
GC 之后,年轻代使用量为 0,堆内存使用量 2GB,存活区 254MB,那么老年代大约 1.8GB,那么“内存提升量为 200MB 左右”。
|
||||
|
||||
|
||||
这样分析之后,可以得出结论:
|
||||
|
||||
|
||||
年轻代转移暂停,复制了 400MB 左右的对象,却消耗了 1.8s,系统调用和系统等待的时间达到了 14s。
|
||||
JVM 看到的物理内存 137GB。
|
||||
推算出 JVM 看到的 CPU 内核数量 72个,因为 GC 工作线程 72* 5/8 ~= 48 个。
|
||||
|
||||
|
||||
看到这么多的 GC 工作线程我就开始警惕了,毕竟堆内存才指定了 4GB。
|
||||
|
||||
按照一般的 CPU 和内存资源配比,常见的比例差不多是 4 核 4GB、4 核 8GB 这样的。
|
||||
|
||||
看看对应的 CPU 负载监控信息:
|
||||
|
||||
|
||||
|
||||
通过和运维同学的沟通,得到这个节点的配置被限制为 4 核 8GB。
|
||||
|
||||
这样一来,GC 暂停时间过长的原因就定位到了:
|
||||
|
||||
|
||||
K8S 的资源隔离和 JVM 未协调好,导致 JVM 看见了 72 个 CPU 内核,默认的并行 GC 线程设置为 72* 5/8 ~= 48 个,但是 K8S 限制了这个 Pod 只能使用 4 个 CPU 内核的计算量,致使 GC 发生时,48 个线程在 4 个 CPU 核心上发生资源竞争,导致大量的上下文切换。
|
||||
|
||||
|
||||
处置措施为:
|
||||
|
||||
|
||||
限制 GC 的并行线程数量
|
||||
|
||||
|
||||
事实证明,打印 GC 日志确实是一个很有用的排查分析方法。
|
||||
|
||||
限制 GC 的并行线程数量
|
||||
|
||||
下面是新的启动参数配置:
|
||||
|
||||
-Xmx4g -Xms4g
|
||||
-XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:ParallelGCThreads=4
|
||||
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
|
||||
|
||||
|
||||
|
||||
这里指定了 -XX:ParallelGCThreads=4,为什么这么配呢?我们看看这个参数的说明。
|
||||
|
||||
-XX:ParallelGCThreads=n
|
||||
|
||||
|
||||
|
||||
设置 STW 阶段的并行 worker 线程数量。 如果逻辑处理器小于等于 8 个,则默认值 n 等于逻辑处理器的数量。
|
||||
|
||||
如果逻辑处理器大于 8 个,则默认值 n 大约等于处理器数量的 5/8。在大多数情况下都是个比较合理的值。如果是高配置的 SPARC 系统,则默认值 n 大约等于逻辑处理器数量的 5/16。
|
||||
|
||||
-XX:ConcGCThreads=n
|
||||
|
||||
|
||||
|
||||
设置并发标记的 GC 线程数量。默认值大约是 ParallelGCThreads 的四分之一。
|
||||
|
||||
一般来说不用指定并发标记的 GC 线程数量,只用指定并行的即可。
|
||||
|
||||
重新启动之后,看看 GC 暂停时间指标:
|
||||
|
||||
|
||||
|
||||
红色箭头所指示的点就是重启的时间点,可以发现,暂停时间基本上都处于 50ms 范围内。
|
||||
|
||||
后续的监控发现,这个参数确实解决了问题。
|
||||
|
||||
那么还有没有其他的办法呢?请关注后续的章节《应对容器时代面临的挑战》。
|
||||
|
||||
小结
|
||||
|
||||
通过这个案例,我们可以看到,JVM 问题排查和性能调优主要基于监控数据来进行。
|
||||
|
||||
还是那句话:没有量化,就没有改进。
|
||||
|
||||
简单汇总一下这里使用到的手段:
|
||||
|
||||
|
||||
指标监控
|
||||
指定 JVM 启动内存
|
||||
指定垃圾收集器
|
||||
打印和分析 GC 日志
|
||||
|
||||
|
||||
GC 和内存是最常见的 JVM 调优场景,还记得课程开始时我们介绍的 GC 的性能维度吗?
|
||||
|
||||
|
||||
延迟,GC 中影响延迟的主要因素就是暂停时间。
|
||||
吞吐量,主要看业务线程消耗的 CPU 资源百分比,GC 占用的部分包括:GC 暂停时间,以及高负载情况下并发 GC 消耗的 CPU 资源。
|
||||
系统容量,主要说的是硬件配置,以及服务能力。
|
||||
|
||||
|
||||
只要这些方面的指标都能够满足,各种资源占用也保持在合理范围内,就达成了我们的预期。
|
||||
|
||||
参考
|
||||
|
||||
|
||||
Native Memory Tracking(NMT,Native 内存跟踪)排查文档
|
||||
生产环境 GC 参数调优
|
||||
https://plumbr.io/blog/monitoring/why-is-troubleshooting-so-hard
|
||||
Linux 的性能调优的思路
|
||||
Linux 工具快速教程
|
||||
|
||||
|
||||
|
||||
|
||||
|
388
专栏/JVM核心技术32讲(完)/29GC疑难情况问题排查与分析(上篇).md
Normal file
388
专栏/JVM核心技术32讲(完)/29GC疑难情况问题排查与分析(上篇).md
Normal file
@ -0,0 +1,388 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
29 GC 疑难情况问题排查与分析(上篇)
|
||||
本章介绍导致 GC 性能问题的典型情况。相关示例都来源于生产环境,为演示需要做了一定程度的精简。
|
||||
|
||||
|
||||
名词说明:Allocation Rate,翻译为“分配速率”,而不是分配率。因为不是百分比,而是单位时间内分配的量。同理,Promotion Rate 翻译为“提升速率”。
|
||||
|
||||
|
||||
高分配速率(High Allocation Rate)
|
||||
|
||||
分配速率(Allocation Rate)表示单位时间内分配的内存量。通常使用 MB/sec 作为单位,也可以使用 PB/year 等。分配速率过高就会严重影响程序的性能,在 JVM 中可能会导致巨大的 GC 开销。
|
||||
|
||||
如何测量分配速率?
|
||||
|
||||
通过指定 JVM 参数:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,通过 GC 日志来计算分配速率。GC 日志如下所示:
|
||||
|
||||
0.291: [GC (Allocation Failure)
|
||||
[PSYoungGen: 33280K->5088K(38400K)]
|
||||
33280K->24360K(125952K), 0.0365286 secs]
|
||||
[Times: user=0.11 sys=0.02, real=0.04 secs]
|
||||
0.446: [GC (Allocation Failure)
|
||||
[PSYoungGen: 38368K->5120K(71680K)]
|
||||
57640K->46240K(159232K), 0.0456796 secs]
|
||||
[Times: user=0.15 sys=0.02, real=0.04 secs]
|
||||
0.829: [GC (Allocation Failure)
|
||||
[PSYoungGen: 71680K->5120K(71680K)]
|
||||
112800K->81912K(159232K), 0.0861795 secs]
|
||||
[Times: user=0.23 sys=0.03, real=0.09 secs]
|
||||
|
||||
|
||||
|
||||
具体就是计算上一次垃圾收集之后,与下一次 GC 开始之前的年轻代使用量,两者的差值除以时间,就是分配速率。通过上面的日志,可以计算出以下信息:
|
||||
|
||||
|
||||
JVM 启动之后 291ms,共创建了 33280KB 的对象。第一次 Minor GC(小型 GC)完成后,年轻代中还有 5088KB 的对象存活。
|
||||
在启动之后 446ms,年轻代的使用量增加到 38368KB,触发第二次 GC,完成后年轻代的使用量减少到 5120KB。
|
||||
在启动之后 829ms,年轻代的使用量为 71680KB,GC 后变为 5120KB。
|
||||
|
||||
|
||||
可以通过年轻代的使用量来计算分配速率,如下表所示:
|
||||
|
||||
|
||||
|
||||
|
||||
Event
|
||||
Time
|
||||
Young before
|
||||
Young after
|
||||
Allocated during
|
||||
Allocation rate
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1st GC
|
||||
291ms
|
||||
33,280KB
|
||||
5,088KB
|
||||
33,280KB
|
||||
114MB/sec
|
||||
|
||||
|
||||
|
||||
2nd GC
|
||||
446ms
|
||||
38,368KB
|
||||
5,120KB
|
||||
33,280KB
|
||||
215MB/sec
|
||||
|
||||
|
||||
|
||||
3rd GC
|
||||
829ms
|
||||
71,680KB
|
||||
5,120KB
|
||||
66,560KB
|
||||
174MB/sec
|
||||
|
||||
|
||||
|
||||
Total
|
||||
829ms
|
||||
N/A
|
||||
N/A
|
||||
133,120KB
|
||||
161MB/sec
|
||||
|
||||
|
||||
|
||||
通过这些信息可以知道,在此期间,该程序的内存分配速率为 16MB/sec。
|
||||
|
||||
分配速率的意义
|
||||
|
||||
分配速率的变化,会增加或降低 GC 暂停的频率,从而影响吞吐量。但只有年轻代的 Minor GC 受分配速率的影响,老年代 GC 的频率和持续时间一般不受 分配速率(Allocation Rate)的直接影响(想想为什么?),而是受到 提升速率(Promotion Rate)的影响,请参见下文。
|
||||
|
||||
现在我们只关心 Minor GC 暂停,查看年轻代的 3 个内存池。因为对象在 Eden 区分配,所以我们一起来看 Eden 区的大小和分配速率的关系。看看增加 Eden 区的容量,能不能减少 Minor GC 暂停次数,从而使程序能够维持更高的分配速率。
|
||||
|
||||
经过我们的实验,通过参数 -XX:NewSize、-XX:MaxNewSize 以及 -XX:SurvivorRatio 设置不同的 Eden 空间,运行同一程序时,可以发现:
|
||||
|
||||
|
||||
Eden 空间为 100MB 时,分配速率低于 100MB/秒。
|
||||
将 Eden 区增大为 1GB,分配速率也随之增长,大约等于 200MB/秒。
|
||||
|
||||
|
||||
为什么会这样?
|
||||
|
||||
因为减少 GC 暂停,就等价于减少了任务线程的停顿,就可以做更多工作,也就创建了更多对象,所以对同一应用来说,分配速率越高越好。
|
||||
|
||||
在得出“Eden 区越大越好”这个结论前,我们注意到:分配速率可能会、也可能不会影响程序的实际吞吐量。
|
||||
|
||||
总而言之,吞吐量和分配速率有一定关系,因为分配速率会影响 Minor GC 暂停,但对于总体吞吐量的影响,还要考虑 Major GC 暂停等。
|
||||
|
||||
示例
|
||||
|
||||
参考 Demo 程序。假设系统连接了一个外部的数字传感器。应用通过专有线程,不断地获取传感器的值(此处使用随机数模拟),其他线程会调用 processSensorValue() 方法,传入传感器的值来执行某些操作。
|
||||
|
||||
public class BoxingFailure {
|
||||
private static volatile Double sensorValue;
|
||||
|
||||
private static void readSensor() {
|
||||
while(true) sensorValue = Math.random();
|
||||
}
|
||||
|
||||
private static void processSensorValue(Double value) {
|
||||
if(value != null) {
|
||||
//...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
如同类名所示,这个 Demo 是模拟 boxing 的。为了 null 值判断,使用的是包装类型 Double。程序基于传感器的最新值进行计算,但从传感器取值是一个耗时的操作,所以采用了异步方式:一个线程不断获取新值,计算线程则直接使用暂存的最新值,从而避免同步等待。
|
||||
|
||||
Demo 程序在运行的过程中,由于分配速率太大而受到 GC 的影响。下面将确认问题,并给出解决办法。
|
||||
|
||||
高分配速率对 JVM 的影响
|
||||
|
||||
首先,我们应该检查程序的吞吐量是否降低。如果创建了过多的临时对象,Minor GC 的次数就会增加。如果并发较大,则 GC 可能会严重影响吞吐量。
|
||||
|
||||
遇到这种情况时,GC 日志将会像下面这样,当然这是上面的示例程序 产生的 GC 日志。
|
||||
|
||||
JVM 启动参数为:-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xmx32m。
|
||||
|
||||
2.808: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003076 secs]
|
||||
2.819: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003079 secs]
|
||||
2.830: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0002968 secs]
|
||||
2.842: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003374 secs]
|
||||
2.853: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0004672 secs]
|
||||
2.864: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003371 secs]
|
||||
2.875: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003214 secs]
|
||||
2.886: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003374 secs]
|
||||
2.896: [GC (Allocation Failure)
|
||||
[PSYoungGen: 9760K->32K(10240K)], 0.0003588 secs]
|
||||
|
||||
|
||||
|
||||
很显然 Minor GC 的频率太高了。这说明创建了大量的对象。另外,年轻代在 GC 之后的使用量又很低,也没有 Full GC 发生。种种迹象表明,GC 对吞吐量造成了严重的影响。
|
||||
|
||||
解决方案
|
||||
|
||||
在某些情况下,只要增加年轻代的大小,即可降低分配速率过高所造成的影响。增加年轻代空间并不会降低分配速率,但是会减少 GC 的频率。如果每次 GC 后只有少量对象存活,Minor GC 的暂停时间就不会明显增加。
|
||||
|
||||
运行 示例程序 时,增加堆内存大小(同时也就增大了年轻代的大小),使用的 JVM 参数为:-Xmx64m。
|
||||
|
||||
2.808: [GC (Allocation Failure)
|
||||
[PSYoungGen: 20512K->32K(20992K)], 0.0003748 secs]
|
||||
2.831: [GC (Allocation Failure)
|
||||
[PSYoungGen: 20512K->32K(20992K)], 0.0004538 secs]
|
||||
2.855: [GC (Allocation Failure)
|
||||
[PSYoungGen: 20512K->32K(20992K)], 0.0003355 secs]
|
||||
2.879: [GC (Allocation Failure)
|
||||
[PSYoungGen: 20512K->32K(20992K)], 0.0005592 secs]
|
||||
|
||||
|
||||
|
||||
但有时候增加堆内存的大小,并不能解决问题。
|
||||
|
||||
通过前面学到的知识,我们可以通过分配分析器找出大部分垃圾产生的位置。实际上,在此示例中 99% 的对象属于 Double 包装类,在readSensor 方法中创建。
|
||||
|
||||
最简单的优化,将创建的 Double 对象替换为原生类型 double,而针对 null 值的检测,可以使用 Double.NaN 来进行。
|
||||
|
||||
由于原生类型不算是对象,也就不会产生垃圾,导致 GC 事件。
|
||||
|
||||
优化之后,不在堆中分配新对象,而是直接覆盖一个属性域即可。对示例程序进行简单的改造(查看 diff)后,GC 暂停基本上完全消除。
|
||||
|
||||
有时候 JVM 也很智能,会使用逃逸分析技术(Escape Analysis Technique)来避免过度分配。
|
||||
|
||||
简单来说,JIT 编译器可以通过分析得知,方法创建的某些对象永远都不会“逃出”此方法的作用域。这时候就不需要在堆上分配这些对象,也就不会产生垃圾,所以 JIT 编译器的一种优化手段就是:消除堆上内存分配(请参考基准测试)。
|
||||
|
||||
过早提升(Premature Promotion)
|
||||
|
||||
提升速率(Promotion Rate)用于衡量单位时间内从年轻代提升到老年代的数据量。一般使用 MB/sec 作为单位,和“分配速率”类似。
|
||||
|
||||
JVM 会将长时间存活的对象从年轻代提升到老年代。根据分代假设,可能存在一种情况,老年代中不仅有存活时间长的对象,也可能有存活时间短的对象。这就是过早提升:对象存活时间还不够长的时候就被提升到了老年代。
|
||||
|
||||
Major GC 不是为频繁回收而设计的,但 Major GC 现在也要清理这些生命短暂的对象,就会导致 GC 暂停时间过长。这会严重影响系统的吞吐量。
|
||||
|
||||
如何测量提升速率
|
||||
|
||||
可以指定 JVM 参数 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps,通过 GC 日志来测量提升速率。JVM 记录的 GC 暂停信息如下所示:
|
||||
|
||||
0.291: [GC (Allocation Failure)
|
||||
[PSYoungGen: 33280K->5088K(38400K)]
|
||||
33280K->24360K(125952K), 0.0365286 secs]
|
||||
[Times: user=0.11 sys=0.02, real=0.04 secs]
|
||||
0.446: [GC (Allocation Failure)
|
||||
[PSYoungGen: 38368K->5120K(71680K)]
|
||||
57640K->46240K(159232K), 0.0456796 secs]
|
||||
[Times: user=0.15 sys=0.02, real=0.04 secs]
|
||||
0.829: [GC (Allocation Failure)
|
||||
[PSYoungGen: 71680K->5120K(71680K)]
|
||||
112800K->81912K(159232K), 0.0861795 secs]
|
||||
[Times: user=0.23 sys=0.03, real=0.09 secs]
|
||||
|
||||
|
||||
|
||||
从上面的日志可以得知:GC 之前和之后的年轻代使用量以及堆内存使用量。这样就可以通过差值算出老年代的使用量。GC 日志中的信息可以表述为:
|
||||
|
||||
|
||||
|
||||
|
||||
Event
|
||||
Time
|
||||
Young decreased
|
||||
Total decreased
|
||||
Promoted
|
||||
Promotion rate
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(事件)
|
||||
(耗时)
|
||||
(年轻代减少)
|
||||
(整个堆内存减少)
|
||||
(提升量)
|
||||
(提升速率)
|
||||
|
||||
|
||||
|
||||
1st GC
|
||||
291ms
|
||||
28192K
|
||||
8920K
|
||||
19272K
|
||||
66.2 MB/sec
|
||||
|
||||
|
||||
|
||||
2nd GC
|
||||
446ms
|
||||
33248K
|
||||
11400K
|
||||
21848K
|
||||
140.95 MB/sec
|
||||
|
||||
|
||||
|
||||
3rd GC
|
||||
829ms
|
||||
66560K
|
||||
30888K
|
||||
35672K
|
||||
93.14 MB/sec
|
||||
|
||||
|
||||
|
||||
Total
|
||||
829ms
|
||||
|
||||
|
||||
76792K
|
||||
92.63 MB/sec
|
||||
|
||||
|
||||
|
||||
根据这些信息,就可以计算出观测周期内的提升速率:平均提升速率为 92MB/秒,峰值为 140.95MB/秒。
|
||||
|
||||
请注意,只能根据 Minor GC 计算提升速率。Full GC 的日志不能用于计算提升速率,因为 Major GC 会清理掉老年代中的一部分对象。
|
||||
|
||||
提升速率的意义
|
||||
|
||||
和分配速率一样,提升速率也会影响 GC 暂停的频率。但分配速率主要影响 minor GC,而提升速率则影响 major GC 的频率。有大量的对象提升,自然很快将老年代填满。老年代填充的越快,则 Major GC 事件的频率就会越高。
|
||||
|
||||
|
||||
|
||||
前面章节提到过,Full GC 通常需要更多的时间,因为需要处理更多的对象,还要执行碎片整理等额外的复杂过程。
|
||||
|
||||
示例
|
||||
|
||||
让我们看一个过早提升的示例。这个程序创建/获取大量的对象/数据,并暂存到集合之中,达到一定数量后进行批处理:
|
||||
|
||||
public class PrematurePromotion {
|
||||
|
||||
private static final Collection<byte[]> accumulatedChunks
|
||||
= new ArrayList<>();
|
||||
|
||||
private static void onNewChunk(byte[] bytes) {
|
||||
accumulatedChunks.add(bytes);
|
||||
|
||||
if(accumulatedChunks.size() > MAX_CHUNKS) {
|
||||
processBatch(accumulatedChunks);
|
||||
accumulatedChunks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
此 Demo 程序 受到过早提升的影响。下面将进行验证并给出解决办法。
|
||||
|
||||
过早提升的影响
|
||||
|
||||
一般来说过早提升的症状表现为以下形式:
|
||||
|
||||
|
||||
短时间内频繁地执行 Full GC
|
||||
每次 Full GC 后老年代的使用率都很低,在 10~20% 或以下
|
||||
提升速率接近于分配速率
|
||||
|
||||
|
||||
要演示这种情况稍微有点麻烦,所以我们使用特殊手段,让对象提升到老年代的年龄比默认情况小很多。指定 GC 参数 -Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1,运行程序之后,可以看到下面的 GC 日志:
|
||||
|
||||
2.176: [Full GC (Ergonomics)
|
||||
[PSYoungGen: 9216K->0K(10752K)]
|
||||
[ParOldGen: 10020K->9042K(12288K)]
|
||||
19236K->9042K(23040K), 0.0036840 secs]
|
||||
2.394: [Full GC (Ergonomics)
|
||||
[PSYoungGen: 9216K->0K(10752K)]
|
||||
[ParOldGen: 9042K->8064K(12288K)]
|
||||
18258K->8064K(23040K), 0.0032855 secs]
|
||||
2.611: [Full GC (Ergonomics)
|
||||
[PSYoungGen: 9216K->0K(10752K)]
|
||||
[ParOldGen: 8064K->7085K(12288K)]
|
||||
17280K->7085K(23040K), 0.0031675 secs]
|
||||
2.817: [Full GC (Ergonomics)
|
||||
[PSYoungGen: 9216K->0K(10752K)]
|
||||
[ParOldGen: 7085K->6107K(12288K)]
|
||||
16301K->6107K(23040K), 0.0030652 secs]
|
||||
|
||||
|
||||
|
||||
乍一看似乎不是过早提升的问题,每次 GC 之后老年代的使用率似乎在减少。但反过来想,要是没有对象提升或者提升率很小,也就不会看到这么多的 Full GC 了。
|
||||
|
||||
简单解释一下这里的 GC 行为:有很多对象提升到老年代,同时老年代中也有很多对象被回收了,这就造成了老年代使用量减少的假象。但事实是大量的对象不断地被提升到老年代,并触发 Full GC。
|
||||
|
||||
解决方案
|
||||
|
||||
简单来说,要解决这类问题,需要让年轻代存放得下暂存的数据。有两种简单的方法:
|
||||
|
||||
一是增加年轻代的大小,设置 JVM 启动参数,类似这样:-Xmx64m -XX:NewSize=32m,程序在执行时,Full GC 的次数自然会减少很多,只会对 Minor GC 的持续时间产生影响:
|
||||
|
||||
2.251: [GC (Allocation Failure)
|
||||
[PSYoungGen: 28672K->3872K(28672K)]
|
||||
37126K->12358K(61440K), 0.0008543 secs]
|
||||
2.776: [GC (Allocation Failure)
|
||||
[PSYoungGen: 28448K->4096K(28672K)]
|
||||
36934K->16974K(61440K), 0.0033022 secs]
|
||||
|
||||
|
||||
|
||||
二是减少每次批处理的数量,也能得到类似的结果。
|
||||
|
||||
至于选用哪个方案,要根据业务需求决定。
|
||||
|
||||
在某些情况下,业务逻辑不允许减少批处理的数量,那就只能增加堆内存,或者重新指定年轻代的大小。
|
||||
|
||||
如果都不可行,就只能优化数据结构,减少内存消耗。但总体目标依然是一致的——让临时数据能够在年轻代存放得下。
|
||||
|
||||
|
||||
|
||||
|
299
专栏/JVM核心技术32讲(完)/30GC疑难情况问题排查与分析(下篇).md
Normal file
299
专栏/JVM核心技术32讲(完)/30GC疑难情况问题排查与分析(下篇).md
Normal file
@ -0,0 +1,299 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
30 GC 疑难情况问题排查与分析(下篇)
|
||||
Weak、Soft 及 Phantom 引用
|
||||
|
||||
另一类影响 GC 的问题是程序中的 non-strong 引用。虽然这类引用在很多情况下可以避免出现 OutOfMemoryError,但过量使用也会对 GC 造成严重的影响,反而降低系统性能。
|
||||
|
||||
弱引用的缺点
|
||||
|
||||
首先,弱引用(weak reference)是可以被 GC 强制回收的。当垃圾收集器发现一个弱可达对象(weakly reachable,即指向该对象的引用只剩下弱引用)时,就会将其置入相应的 ReferenceQueue 中,变成可终结的对象。之后可能会遍历这个 reference queue,并执行相应的清理。典型的示例是清除缓存中不再引用的 KEY。
|
||||
|
||||
当然,在这个时候我们还可以将该对象赋值给新的强引用,在最后终结和回收前,GC 会再次确认该对象是否可以安全回收。因此,弱引用对象的回收过程是横跨多个 GC 周期的。
|
||||
|
||||
实际上弱引用使用的很多。大部分缓存框架都是基于弱引用实现的,所以虽然业务代码中没有直接使用弱引用,但程序中依然会大量存在。
|
||||
|
||||
其次,软引用(soft reference)比弱引用更难被垃圾收集器回收。回收软引用没有确切的时间点,由 JVM 自己决定。一般只会在即将耗尽可用内存时,才会回收软引用,以作最后手段。这意味着可能会有更频繁的 Full GC,暂停时间也比预期更长,因为老年代中的存活对象会很多。
|
||||
|
||||
最后,使用虚引用(phantom reference)时,必须手动进行内存管理,以标识这些对象是否可以安全地回收。表面上看起来很正常,但实际上并不是这样。javadoc 中写道:
|
||||
|
||||
|
||||
In order to ensure that a reclaimable object remains so, the referent of a phantom reference may not be retrieved: The get method of a phantom reference always returns null.
|
||||
|
||||
为了防止可回收对象的残留,虚引用对象不应该被获取:phantom reference 的 get 方法返回值永远是 null。
|
||||
|
||||
|
||||
令人惊讶的是,很多开发者忽略了下一段内容(这才是重点):
|
||||
|
||||
|
||||
Unlike soft and weak references,phantom references are not automatically cleared by the garbage collector as they are enqueued. An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.
|
||||
|
||||
与软引用和弱引用不同,虚引用不会被 GC 自动清除,因为他们被存放到队列中。通过虚引用可达的对象会继续留在内存中,直到调用此引用的 clear 方法,或者引用自身变为不可达。
|
||||
|
||||
|
||||
也就是说,我们必须手动调用 clear() 来清除虚引用,否则可能会造成 OutOfMemoryError 而导致 JVM 挂掉。使用虚引用的理由是,对于用编程手段来跟踪某个对象何时变为不可达对象,这是唯一的常规手段。和软引用/弱引用不同的是,我们不能“复活”虚可达(phantom-reachable)对象。
|
||||
|
||||
示例
|
||||
|
||||
让我们看一个弱引用示例,其中创建了大量的对象,并在 Minor GC 中完成回收。和前面一样,修改提升阀值。可以使用下列 JVM 参数:
|
||||
|
||||
-Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1
|
||||
|
||||
|
||||
|
||||
此时 GC 日志如下所示:
|
||||
|
||||
2.330: [GC (Allocation Failure) 20933K->8229K(22528K), 0.0033848 secs]
|
||||
2.335: [GC (Allocation Failure) 20517K->7813K(22528K), 0.0022426 secs]
|
||||
2.339: [GC (Allocation Failure) 20101K->7429K(22528K), 0.0010920 secs]
|
||||
2.341: [GC (Allocation Failure) 19717K->9157K(22528K), 0.0056285 secs]
|
||||
2.348: [GC (Allocation Failure) 21445K->8997K(22528K), 0.0041313 secs]
|
||||
2.354: [GC (Allocation Failure) 21285K->8581K(22528K), 0.0033737 secs]
|
||||
2.359: [GC (Allocation Failure) 20869K->8197K(22528K), 0.0023407 secs]
|
||||
2.362: [GC (Allocation Failure) 20485K->7845K(22528K), 0.0011553 secs]
|
||||
2.365: [GC (Allocation Failure) 20133K->9501K(22528K), 0.0060705 secs]
|
||||
2.371: [Full GC (Ergonomics) 9501K->2987K(22528K), 0.0171452 secs]
|
||||
|
||||
|
||||
|
||||
可以看到,Full GC 的次数很少。但如果使用弱引用来指向创建的对象,使用 JVM 参数 -Dweak.refs=true,则情况会发生明显变化。使用弱引用的原因很多,比如在 weak hash map 中将对象作为 Key 的情况。在任何情况下,使用弱引用都可能会导致以下情形:
|
||||
|
||||
2.059: [Full GC (Ergonomics) 20365K->19611K(22528K), 0.0654090 secs]
|
||||
2.125: [Full GC (Ergonomics) 20365K->19711K(22528K), 0.0707499 secs]
|
||||
2.196: [Full GC (Ergonomics) 20365K->19798K(22528K), 0.0717052 secs]
|
||||
2.268: [Full GC (Ergonomics) 20365K->19873K(22528K), 0.0686290 secs]
|
||||
2.337: [Full GC (Ergonomics) 20365K->19939K(22528K), 0.0702009 secs]
|
||||
2.407: [Full GC (Ergonomics) 20365K->19995K(22528K), 0.0694095 secs]
|
||||
|
||||
|
||||
|
||||
可以看到,发生了多次 Full GC,比起前一节的示例,GC 时间增加了一个数量级!
|
||||
|
||||
这是过早提升的另一个例子,但这次情况更加棘手:问题的根源在于弱引用。这些临死的对象,在添加弱引用之后,被提升到了老年代。但是,他们现在陷入另一次 GC 循环之中,所以需要对其做一些适当的清理。
|
||||
|
||||
像之前一样,最简单的办法是增加年轻代的大小,例如指定 JVM 参数 -Xmx64m -XX:NewSize=32m:
|
||||
|
||||
2.328: [GC (Allocation Failure) 38940K->13596K(61440K),0.0012818 secs]
|
||||
2.332: [GC (Allocation Failure) 38172K->14812K(61440K),0.0060333 secs]
|
||||
2.341: [GC (Allocation Failure) 39388K->13948K(61440K),0.0029427 secs]
|
||||
2.347: [GC (Allocation Failure) 38524K->15228K(61440K),0.0101199 secs]
|
||||
2.361: [GC (Allocation Failure) 39804K->14428K(61440K),0.0040940 secs]
|
||||
2.368: [GC (Allocation Failure) 39004K->13532K(61440K),0.0012451 secs]
|
||||
|
||||
|
||||
|
||||
这时候,对象在 Minor GC 中就被回收了。
|
||||
|
||||
更坏的情况是使用软引用,例如这个软引用示例程序。如果程序不是即将发生 OutOfMemoryError,软引用对象就不会被回收。在示例程序中,用软引用替代弱引用,立即出现了更多的 Full GC 事件:
|
||||
|
||||
2.162: [Full GC (Ergonomics) 31561K->12865K(61440K),0.0181392 secs]
|
||||
2.184: [GC (Allocation Failure) 37441K->17585K(61440K),0.0024479 secs]
|
||||
2.189: [GC (Allocation Failure) 42161K->27033K(61440K),0.0061485 secs]
|
||||
2.195: [Full GC (Ergonomics) 27033K->14385K(61440K),0.0228773 secs]
|
||||
2.221: [GC (Allocation Failure) 38961K->20633K(61440K),0.0030729 secs]
|
||||
2.227: [GC (Allocation Failure) 45209K->31609K(61440K),0.0069772 secs]
|
||||
2.234: [Full GC (Ergonomics) 31609K->15905K(61440K),0.0257689 secs]
|
||||
|
||||
|
||||
|
||||
最有趣的是虚引用示例中的虚引用,使用同样的 JVM 参数启动,其结果和弱引用示例非常相似。实际上,Full GC 暂停的次数会小得多,原因前面说过,他们有不同的终结方式。
|
||||
|
||||
如果禁用虚引用清理,增加 JVM 启动参数(-Dno.ref.clearing=true),则可以看到:
|
||||
|
||||
4.180: [Full GC (Ergonomics) 57343K->57087K(61440K),0.0879851 secs]
|
||||
4.269: [Full GC (Ergonomics) 57089K->57088K(61440K),0.0973912 secs]
|
||||
4.366: [Full GC (Ergonomics) 57091K->57089K(61440K),0.0948099 secs]
|
||||
|
||||
|
||||
|
||||
主线程中很快抛出异常:
|
||||
|
||||
java.lang.OutOfMemoryError: Java heap space
|
||||
|
||||
|
||||
|
||||
使用虚引用时要小心谨慎,并及时清理虚可达对象。如果不清理,很可能会发生 OutOfMemoryError。
|
||||
|
||||
请相信我们的经验教训:处理 reference queue 的线程中如果没 catch 住异常,系统很快就会被整挂了。
|
||||
|
||||
使用非强引用的影响
|
||||
|
||||
建议使用 JVM 参数 -XX:+PrintReferenceGC 来看看各种引用对 GC 的影响。如果将此参数用于启动弱引用示例,将会看到:
|
||||
|
||||
2.173: [Full GC (Ergonomics)
|
||||
2.234: [SoftReference,0 refs,0.0000151 secs]
|
||||
2.234: [WeakReference,2648 refs,0.0001714 secs]
|
||||
2.234: [FinalReference,1 refs,0.0000037 secs]
|
||||
2.234: [PhantomReference,0 refs,0 refs,0.0000039 secs]
|
||||
2.234: [JNI Weak Reference,0.0000027 secs]
|
||||
[PSYoungGen: 9216K->8676K(10752K)]
|
||||
[ParOldGen: 12115K->12115K(12288K)]
|
||||
21331K->20792K(23040K),
|
||||
[Metaspace: 3725K->3725K(1056768K)],
|
||||
0.0766685 secs]
|
||||
[Times: user=0.49 sys=0.01,real=0.08 secs]
|
||||
2.250: [Full GC (Ergonomics)
|
||||
2.307: [SoftReference,0 refs,0.0000173 secs]
|
||||
2.307: [WeakReference,2298 refs,0.0001535 secs]
|
||||
2.307: [FinalReference,3 refs,0.0000043 secs]
|
||||
2.307: [PhantomReference,0 refs,0 refs,0.0000042 secs]
|
||||
2.307: [JNI Weak Reference,0.0000029 secs]
|
||||
[PSYoungGen: 9215K->8747K(10752K)]
|
||||
[ParOldGen: 12115K->12115K(12288K)]
|
||||
21331K->20863K(23040K),
|
||||
[Metaspace: 3725K->3725K(1056768K)],
|
||||
0.0734832 secs]
|
||||
[Times: user=0.52 sys=0.01,real=0.07 secs]
|
||||
2.323: [Full GC (Ergonomics)
|
||||
2.383: [SoftReference,0 refs,0.0000161 secs]
|
||||
2.383: [WeakReference,1981 refs,0.0001292 secs]
|
||||
2.383: [FinalReference,16 refs,0.0000049 secs]
|
||||
2.383: [PhantomReference,0 refs,0 refs,0.0000040 secs]
|
||||
2.383: [JNI Weak Reference,0.0000027 secs]
|
||||
[PSYoungGen: 9216K->8809K(10752K)]
|
||||
[ParOldGen: 12115K->12115K(12288K)]
|
||||
21331K->20925K(23040K),
|
||||
[Metaspace: 3725K->3725K(1056768K)],
|
||||
0.0738414 secs]
|
||||
[Times: user=0.52 sys=0.01,real=0.08 secs]
|
||||
|
||||
|
||||
|
||||
只有确定 GC 对应用的吞吐量和延迟造成影响之后,才应该花心思来分析这些信息,审查这部分日志。通常情况下,每次 GC 清理的引用数量都是很少的,大部分情况下为 0。
|
||||
|
||||
如果 GC 花了较多时间来清理这类引用,或者清除了很多的此类引用,就需要进一步观察和分析了。
|
||||
|
||||
解决方案
|
||||
|
||||
如果程序确实碰到了 mis-、ab- 等问题或者滥用 weak/soft/phantom 引用,一般都要修改程序的实现逻辑。每个系统不一样,因此很难提供通用的指导建议,但有一些常用的经验办法:
|
||||
|
||||
|
||||
弱引用(Weak references):如果某个内存池的使用量增大,造成了性能问题,那么增加这个内存池的大小(可能也要增加堆内存的最大容量)。如同示例中所看到的,增加堆内存的大小,以及年轻代的大小,可以减轻症状。
|
||||
软引用(Soft references):如果确定问题的根源是软引用,唯一的解决办法是修改程序源码,改变内部实现逻辑。
|
||||
虚引用(Phantom references):请确保在程序中调用了虚引用的 clear 方法。编程中很容易忽略某些虚引用,或者清理的速度跟不上生产的速度,又或者清除引用队列的线程挂了,就会对 GC 造成很大压力,最终可能引起 OutOfMemoryError。
|
||||
|
||||
|
||||
其他性能问题的案例
|
||||
|
||||
前面介绍了最常见的 GC 性能问题,本节介绍一些不常见、但也可能会导致系统故障的问题。
|
||||
|
||||
RMI 与 GC
|
||||
|
||||
如果系统提供或者消费 RMI 服务,则 JVM 会定期执行 Full GC 来确保本地未使用的对象在另一端也不占用空间。即使你的代码中没有发布 RMI 服务,但第三方或者工具库也可能会打开 RMI 终端。最常见的元凶是 JMX,如果通过 JMX 连接到远端,底层则会使用 RMI 发布数据。
|
||||
|
||||
问题是有很多不必要的周期性 Full GC。查看老年代的使用情况,一般是没有内存压力,其中还存在大量的空闲区域,但 Full GC 就是被触发了,也就会暂停所有的应用线程。
|
||||
|
||||
这种周期性调用 System.gc() 删除远程引用的行为,是在 sun.rmi.transport.ObjectTable 类中,通过 sun.misc.GC.requestLatency(long gcInterval) 调用的。
|
||||
|
||||
对许多应用来说,根本没必要,甚至对性能有害。禁止这种周期性的 GC 行为,可以使用以下 JVM 参数:
|
||||
|
||||
java -Dsun.rmi.dgc.server.gcInterval=9223372036854775807L
|
||||
-Dsun.rmi.dgc.client.gcInterval=9223372036854775807L
|
||||
com.yourcompany.YourApplication
|
||||
|
||||
|
||||
|
||||
这让 Long.MAX_VALUE 毫秒之后,才调用 System.gc(),实际运行的系统可能永远都不会触发。
|
||||
|
||||
// ObjectTable.class
|
||||
private static final long gcInterval =
|
||||
((Long)AccessController.doPrivileged(
|
||||
new GetLongAction("sun.rmi.dgc.server.gcInterval",3600000L)
|
||||
)).longValue();
|
||||
|
||||
|
||||
|
||||
可以看到,默认值为 3600000L,也就是 1 小时触发一次 Full GC。
|
||||
|
||||
另一种方式是指定 JVM 参数 -XX:+DisableExplicitGC,禁止显式地调用 System.gc()。但我们强烈反对这种方式,因为我们不清楚这么做是否埋有地雷,例如第三方库里需要显式调研。
|
||||
|
||||
JVMTI tagging 与 GC
|
||||
|
||||
如果在程序启动时指定了 Java Agent(-javaagent),Agent 就可以使用 JVMTI tagging 标记堆中的对象。如果 tagging 标记了大量的对象,很可能会引起 GC 性能问题,导致延迟增加,以及吞吐量降低。
|
||||
|
||||
问题发生在 native 代码中,JvmtiTagMap::do_weak_oops 在每次 GC 时,都会遍历所有标标记(tag),并执行一些比较耗时的操作。更坑的是,这种操作是串行执行的。
|
||||
|
||||
如果存在大量的标记,就意味着 GC 时有很大一部分工作是单线程执行的,GC 暂停时间可能会增加一个数量级。
|
||||
|
||||
检查是否因为 Java Agent 增加了 GC 暂停时间,可以使用诊断参数 –XX:+TraceJVMTIObjectTagging。
|
||||
|
||||
启用跟踪之后,可以估算出内存中 的标记映射了多少 native 内存,以及遍历所消耗的时间。
|
||||
|
||||
如果你不是 需要使用的这个 agent 的作者,那一般是搞不定这类问题的。除了提 Bug 之外你什么都做不了。如果发生了这种情况,请建议厂商清理不必要的标记。(以前我们就在生产环境里发现 APM 厂商的 Agent 偶尔会导致 JVM OOM 崩溃。)
|
||||
|
||||
巨无霸对象的分配(Humongous Allocations)
|
||||
|
||||
如果使用 G1 垃圾收集算法,会产生一种巨无霸对象引起的 GC 性能问题。
|
||||
|
||||
|
||||
说明:在 G1 中,巨无霸对象是指所占空间超过一个小堆区(region)50% 的对象。
|
||||
|
||||
|
||||
频繁地创建巨无霸对象,无疑会造成 GC 的性能问题,看看 G1 的处理方式:
|
||||
|
||||
|
||||
如果某个 region 中含有巨无霸对象,则巨无霸对象后面的空间将不会被分配。如果所有巨无霸对象都超过某个比例,则未使用的空间就会引发内存碎片问题。
|
||||
G1 没有对巨无霸对象进行优化。这在 JDK 8 以前是个特别棘手的问题——在 Java 1.8u40 之前的版本中,巨无霸对象所在 region 的回收只能在 Full GC 中进行。最新版本的 Hotspot JVM,在 marking 阶段之后的 cleanup 阶段中释放巨无霸区间,所以这个问题在新版本 JVM 中的影响已大大降低。
|
||||
|
||||
|
||||
要监控是否存在巨无霸对象,可以打开 GC 日志,使用的命令如下:
|
||||
|
||||
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
|
||||
-XX:+PrintReferenceGC -XX:+UseG1GC
|
||||
-XX:+PrintAdaptiveSizePolicy -Xmx128m
|
||||
MyClass
|
||||
|
||||
|
||||
|
||||
GC 日志中可能会发现这样的部分:
|
||||
|
||||
0.106: [G1Ergonomics (Concurrent Cycles)
|
||||
request concurrent cycle initiation,
|
||||
reason: occupancy higher than threshold,
|
||||
occupancy: 60817408 bytes,
|
||||
allocation request: 1048592 bytes,
|
||||
threshold: 60397965 bytes (45.00 %),
|
||||
source: concurrent humongous allocation]
|
||||
0.106: [G1Ergonomics (Concurrent Cycles)
|
||||
request concurrent cycle initiation,
|
||||
reason: requested by GC cause,
|
||||
GC cause: G1 Humongous Allocation]
|
||||
0.106: [G1Ergonomics (Concurrent Cycles)
|
||||
initiate concurrent cycle,
|
||||
reason: concurrent cycle initiation requested]
|
||||
0.106: [GC pause (G1 Humongous Allocation)
|
||||
(young) (initial-mark)
|
||||
0.106: [G1Ergonomics (CSet Construction)
|
||||
start choosing CSet,
|
||||
_pending_cards: 0,
|
||||
predicted base
|
||||
time: 10.00 ms,
|
||||
remaining time: 190.00 ms,
|
||||
target pause time: 200.00 ms]
|
||||
|
||||
|
||||
|
||||
这样的日志就是证据,表明程序中确实创建了巨无霸对象。可以看到 G1 Humongous Allocation 是 GC 暂停的原因。再看前面一点的 allocation request: 1048592 bytes,可以发现程序试图分配一个 1048592 字节的对象,这要比巨无霸区域(2MB)的 50% 多出 16 个字节。
|
||||
|
||||
第一种解决方式,是修改 region size,以使得大多数的对象不超过 50%,也就不进行巨无霸对象区域的分配。G1 的 region 大小默认值在启动时根据堆内存的大小算出。但也可以指定参数来覆盖默认设置,-XX:G1HeapRegionSize=XX。指定的 region size 必须在 1~32MB 之间,还必须是 2 的幂(2^10=1024=1KB,2^20=1MB,所以 region size 只能是下列值之一:1m、2m、4m、8m、16m、32m)。
|
||||
|
||||
这种方式也有副作用,增加 region 的大小也就变相地减少了 region 的数量,所以需要谨慎使用,最好进行一些测试,看看是否改善了吞吐量和延迟。
|
||||
|
||||
更好的使用方式是,在程序中限制对象的大小,我们可以在运行时使用内存分析工具,展示出巨无霸对象的信息,以及分配时所在的堆栈跟踪信息。
|
||||
|
||||
总结
|
||||
|
||||
Java 作为一个通用平台,运行在 JVM 上的应用程序多种多样,其启动参数也有上百个,其中有很多会影响到 GC 和性能,所以调优 GC 性能的方法也有很多种。
|
||||
|
||||
但是我们也要时刻提醒自己:没有真正的银弹,能满足所有的性能调优指标。
|
||||
|
||||
我们需要做的,就是了解这些可能会出现问题的各个要点,掌握常见的排查分析方法和工具。
|
||||
|
||||
在碰到类似问题时知道是知其然知其所以然,深入理解 JVM/GC 的工作原理,熟练应用各种手段,观察各种现象,收集各种有用的指标数据,进行定性和定量的分析,找到瓶颈,制定解决方案,进行调优和改进,提高应用系统的性能和稳定性。
|
||||
|
||||
|
||||
|
||||
|
461
专栏/JVM核心技术32讲(完)/31JVM相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外.md
Normal file
461
专栏/JVM核心技术32讲(完)/31JVM相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外.md
Normal file
@ -0,0 +1,461 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
31 JVM 相关的常见面试问题汇总:运筹策帷帐之中,决胜于千里之外
|
||||
面试和笔试的要点其实差不多,基础知识和实战经验都是最重要的关注点(当然,面试时的态度和眼缘也很重要)。
|
||||
|
||||
实际面试时,因为时间有限,不可能所有问题都问一遍,一般是根据简历上涉及的内容,抽一部分话题来聊一聊。看看面试者的经验、态度,以及面对一层层深入问题时的处理思路。借此了解面试者的技术水平,对深度、广度,以及思考和解决问题的能力。
|
||||
|
||||
常见的面试套路是什么呢?
|
||||
|
||||
|
||||
XXX 是什么?
|
||||
实现原理是什么?
|
||||
为什么这样实现?
|
||||
如果让你实现你会怎么做?
|
||||
分析下你的实现有什么优缺点?
|
||||
有哪些需要改进的地方?
|
||||
|
||||
|
||||
下面总结一些比较常见的面试题,供大家参考。针对这些问题,大家可以给自己打一个分。
|
||||
|
||||
|
||||
0 分:不清楚相关知识。
|
||||
30 分:有一点印象,知道一些名词。
|
||||
60 分:知道一些概念以及含义,了解功能和常见用途。
|
||||
80 分:能在参考答案的基础上进行补充。
|
||||
100 分:发现参考答案的问题。
|
||||
|
||||
|
||||
下面我们来看看 JVM 相关面试问题。
|
||||
|
||||
1. 什么是 JVM?
|
||||
|
||||
JVM 全称是 Java Virtual Machine,中文称为 Java 虚拟机。
|
||||
|
||||
JVM 是 Java 程序运行的底层平台,与 Java 支持库一起构成了 Java 程序的执行环境。
|
||||
|
||||
分为 JVM 规范和 JVM 实现两个部分。简单来说,Java 虚拟机就是指能执行标准 Java 字节码的虚拟计算机。
|
||||
|
||||
1.1 请问 JDK 与 JVM 有什么区别?
|
||||
|
||||
现在的 JDK、JRE 和 JVM 一般是整套出现的。
|
||||
|
||||
|
||||
JDK = JRE + 开发调试诊断工具
|
||||
JRE = JVM + Java 标准库
|
||||
|
||||
|
||||
1.2 你认识哪些 JVM 厂商?
|
||||
|
||||
常见的 JDK 厂商包括:
|
||||
|
||||
|
||||
Oracle 公司,包括 Hotspot 虚拟机、GraalVM,分为 OpenJDK 和 OracleJDK 两种版本
|
||||
IBM 公司,J9 虚拟机,用在 IBM 的产品套件中
|
||||
Azul Systems 公司,高性能的 Zing 和开源的 Zulu
|
||||
阿里巴巴,Dragonwell 是阿里开发的 OpenJDK 定制版
|
||||
亚马逊,Corretto OpenJDK
|
||||
Red Hat 公司的 OpenJDK
|
||||
Adopt OpenJDK
|
||||
此外,还有一些开源和试验性质的 JVM 实现,比如 Go.JVM
|
||||
|
||||
|
||||
1.3 OracleJDK 与 OpenJDK 有什么区别?
|
||||
|
||||
各种版本的 JDK 一般来说都会符合 Java 虚拟机规范。 两者的区别一般来说包括:
|
||||
|
||||
|
||||
两种 JDK 提供的工具套件略有差别,比如 jmc 等有版权的工具。
|
||||
某些协议或配置不一样,比如美国限制出口的加密算法。
|
||||
其他细微差别,比如 JRE 中某些私有的 API 不一样。
|
||||
|
||||
|
||||
1.4 开发中使用哪个版本的 JDK?生产环境呢?为什么这么选?
|
||||
|
||||
有一说一,选择哪个版本需要考虑研发团队的具体情况:比如机器的操作系统、团队成员的掌握情况、兼顾遗留项目等等。
|
||||
|
||||
当前 Java 最受欢迎的长期维护版本是 Java 8 和 Java 11。
|
||||
|
||||
|
||||
Java 8 是经典 LTS 版本,性能优秀,系统稳定,良好支持各种 CPU 架构和操作系统平台。
|
||||
Java 11 是新的长期支持版,性能更强,支持更多新特性,而且经过几年的维护已经很稳定。
|
||||
|
||||
|
||||
有的企业在开发环境使用 OracleJDK,在生产环境使用 OpenJDK。也有的企业恰好相反,在开发环境使用 OpenJDK,在生产环境使用 OracleJDK。也有的公司使用同样的打包版本。开发和部署时只要进行过测试就没问题。一般来说,测试环境、预上线环境的 JDK 配置需要和生产环境一致。
|
||||
|
||||
2. 什么是 Java 字节码?
|
||||
|
||||
Java 中的字节码,是值 Java 源代码编译后的中间代码格式,一般称为字节码文件。
|
||||
|
||||
2.1 字节码文件中包含哪些内容?
|
||||
|
||||
字节码文件中,一般包含以下部分:
|
||||
|
||||
|
||||
版本号信息
|
||||
静态常量池(符号常量)
|
||||
类相关的信息
|
||||
字段相关的信息
|
||||
方法相关的信息
|
||||
调试相关的信息
|
||||
|
||||
|
||||
可以说,大部分信息都是通过常量池中的符号常量来表述的。
|
||||
|
||||
2.2 什么是常量?
|
||||
|
||||
常量是指不变的量,字母 ‘K’ 或者数字 1024 在 UTF-8 编码中对应到对应的二进制格式都是不变的。同样地,字符串在 Java 中的二进制表示也是不变的, 比如 “KK”。
|
||||
|
||||
在 Java 中需要注意的是,final 关键字修饰的字段和变量,表示最终变量,只能赋值 1 次,不允许再次修改,由编译器和执行引擎共同保证。
|
||||
|
||||
2.3 你怎么理解常量池?
|
||||
|
||||
在 Java 中,常量池包括两层含义:
|
||||
|
||||
|
||||
静态常量池,class 文件中的一个部分,里面保存的是类相关的各种符号常量。
|
||||
运行时常量池,其内容主要由静态常量池解析得到,但也可以由程序添加。
|
||||
|
||||
|
||||
3. JVM 的运行时数据区有哪些?
|
||||
|
||||
根据 JVM 规范,标准的 JVM 运行时数据区包括以下部分:
|
||||
|
||||
|
||||
程序计数器
|
||||
Java 虚拟机栈
|
||||
堆内存
|
||||
方法区
|
||||
运行时常量池
|
||||
本地方法栈
|
||||
|
||||
|
||||
具体的 JVM 实现可根据实际情况进行优化或者合并,满足规范的要求即可。
|
||||
|
||||
3.1 什么是堆内存?
|
||||
|
||||
堆内存是指由程序代码自由分配的内存,与栈内存作区分。
|
||||
|
||||
在 Java 中,堆内存主要用于分配对象的存储空间,只要拿到对象引用,所有线程都可以访问堆内存。
|
||||
|
||||
3.2 堆内存包括哪些部分?
|
||||
|
||||
以 Hotspot 为例,堆内存(HEAP)主要由 GC 模块进行分配和管理,可分为以下部分:
|
||||
|
||||
|
||||
新生代
|
||||
存活区
|
||||
老年代
|
||||
|
||||
|
||||
其中,新生代和存活区一般称为年轻代。
|
||||
|
||||
3.3 什么是非堆内存?
|
||||
|
||||
除堆内存之外,JVM 的内存池还包括非堆(NON_HEAP),对应于 JVM 规范中的方法区,常量池等部分:
|
||||
|
||||
|
||||
MetaSpace
|
||||
CodeCache
|
||||
Compressed Class Space
|
||||
|
||||
|
||||
4. 什么是内存溢出?
|
||||
|
||||
内存溢出(OOM)是指可用内存不足。
|
||||
|
||||
程序运行需要使用的内存超出最大可用值,如果不进行处理就会影响到其他进程,所以现在操作系统的处理办法是:只要超出立即报错,比如抛出“内存溢出错误”。
|
||||
|
||||
就像杯子装不下,满了要溢出来一样,比如一个杯子只有 500ml 的容量,却倒进去 600ml,于是水就溢出造成破坏。
|
||||
|
||||
4.1 什么是内存泄漏?
|
||||
|
||||
内存泄漏(Memory Leak)是指本来无用的对象却继续占用内存,没有再恰当的时机释放占用的内存。
|
||||
|
||||
不使用的内存,却没有被释放,称为“内存泄漏”。也就是该释放的没释放,该回收的没回收。
|
||||
|
||||
比较典型的场景是:每一个请求进来,或者每一次操作处理,都分配了内存,却有一部分不能回收(或未释放),那么随着处理的请求越来越多,内存泄漏也就越来越严重。
|
||||
|
||||
在 Java 中一般是指无用的对象却因为错误的引用关系,不能被 GC 回收清理。
|
||||
|
||||
4.2 两者有什么关系?
|
||||
|
||||
如果存在严重的内存泄漏问题,随着时间的推移,则必然会引起内存溢出。
|
||||
|
||||
内存泄漏一般是资源管理问题和程序 Bug,内存溢出则是内存空间不足和内存泄漏的最终结果。
|
||||
|
||||
5. 给定一个具体的类,请分析对象的内存占用
|
||||
|
||||
public class MyOrder{
|
||||
private long orderId;
|
||||
private long userId;
|
||||
private byte state;
|
||||
private long createMillis;
|
||||
}
|
||||
|
||||
|
||||
|
||||
一般来说,MyOrder 类的每个对象会占用 40 个字节。
|
||||
|
||||
5.1 怎么计算出来的?
|
||||
|
||||
计算方式为:
|
||||
|
||||
|
||||
对象头占用 12 字节。
|
||||
每个 long 类型的字段占用 8 字节,3 个 long 字段占用 24 字节。
|
||||
byte 字段占用 1 个字节。
|
||||
以上合计 37 字节,加上以 8 字节对齐,则实际占用 40 个字节。
|
||||
|
||||
|
||||
5.2 对象头中包含哪些部分?
|
||||
|
||||
对象头中一般包含两个部分:
|
||||
|
||||
|
||||
标记字,占用一个机器字,也就是 8 字节。
|
||||
类型指针,占用一个机器字,也就是 8 个字节。
|
||||
如果堆内存小于 32GB,JVM 默认会开启指针压缩,则只占用 4 个字节。
|
||||
|
||||
|
||||
所以前面的计算中,对象头占用 12 字节。如果是数组,对象头中还会多出一个部分:
|
||||
|
||||
|
||||
数组长度,int 值,占用 4 字节。
|
||||
|
||||
|
||||
6. 常用的 JVM 启动参数有哪些?
|
||||
|
||||
截止目前(2020 年 3 月),JVM 可配置参数已经达到 1000 多个,其中 GC 和内存配置相关的 JVM 参数就有 600 多个。但在绝大部分业务场景下,常用的 JVM 配置参数也就 10 来个。
|
||||
|
||||
例如:
|
||||
|
||||
# JVM 启动参数不换行
|
||||
# 设置堆内存
|
||||
-Xmx4g -Xms4g
|
||||
# 指定 GC 算法
|
||||
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
|
||||
# 指定 GC 并行线程数
|
||||
-XX:ParallelGCThreads=4
|
||||
# 打印 GC 日志
|
||||
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
|
||||
# 指定 GC 日志文件
|
||||
-Xloggc:gc.log
|
||||
# 指定 Meta 区的最大值
|
||||
-XX:MaxMetaspaceSize=2g
|
||||
# 设置单个线程栈的大小
|
||||
-Xss1m
|
||||
# 指定堆内存溢出时自动进行 Dump
|
||||
-XX:+HeapDumpOnOutOfMemoryError
|
||||
-XX:HeapDumpPath=/usr/local/
|
||||
|
||||
|
||||
|
||||
此外,还有一些常用的属性配置:
|
||||
|
||||
# 指定默认的连接超时时间
|
||||
-Dsun.net.client.defaultConnectTimeout=2000
|
||||
-Dsun.net.client.defaultReadTimeout=2000
|
||||
# 指定时区
|
||||
-Duser.timezone=GMT+08
|
||||
# 设置默认的文件编码为 UTF-8
|
||||
-Dfile.encoding=UTF-8
|
||||
# 指定随机数熵源(Entropy Source)
|
||||
-Djava.security.egd=file:/dev/./urandom
|
||||
|
||||
|
||||
|
||||
6.1 设置堆内存 XMX 应该考虑哪些因素?
|
||||
|
||||
需要根据系统的配置来确定,要给操作系统和 JVM 本身留下一定的剩余空间。推荐配置系统或容器里可用内存的 70~80% 最好。
|
||||
|
||||
6.2 假设物理内存是 8G,设置多大堆内存比较合适?
|
||||
|
||||
比如说系统有 8G 物理内存,系统自己可能会用掉一点,大概还有 7.5G 可以用,那么建议配置 -Xmx6g。
|
||||
|
||||
说明:7.5G*0.8=6G,如果知道系统里有明确使用堆外内存的地方,还需要进一步降低这个值。
|
||||
|
||||
6.3 -Xmx 设置的值与 JVM 进程所占用的内存有什么关系?
|
||||
|
||||
JVM 总内存 = 栈 + 堆 + 非堆 + 堆外 + Native
|
||||
|
||||
6.4 怎样开启 GC 日志?
|
||||
|
||||
一般来说,JDK 8 及以下版本通过以下参数来开启 GC 日志:
|
||||
|
||||
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
|
||||
|
||||
|
||||
|
||||
如果是在 JDK 9 及以上的版本,则格式略有不同:
|
||||
|
||||
-Xlog:gc*=info:file=gc.log:time:filecount=0
|
||||
|
||||
|
||||
|
||||
6.5 请指定使用 G1 垃圾收集器来启动 Hello 程序
|
||||
|
||||
java -XX:+UseG1GC
|
||||
-Xms4g
|
||||
-Xmx4g
|
||||
-Xloggc:gc.log
|
||||
-XX:+PrintGCDetails
|
||||
-XX:+PrintGCDateStamps
|
||||
Hello
|
||||
|
||||
|
||||
|
||||
7. Java 8 默认使用的垃圾收集器是什么?
|
||||
|
||||
Java 8 版本的 Hotspot JVM,默认情况下使用的是并行垃圾收集器(Parallel GC)。其他厂商提供的 JDK 8 基本上也默认使用并行垃圾收集器。
|
||||
|
||||
7.1 Java11 的默认垃圾收集器是什么?
|
||||
|
||||
Java 9 之后,官方 JDK 默认使用的垃圾收集器是 G1。
|
||||
|
||||
7.2 常见的垃圾收集器有哪些?
|
||||
|
||||
常见的垃圾收集器包括:
|
||||
|
||||
|
||||
串行垃圾收集器:-XX:+UseSerialGC
|
||||
并行垃圾收集器:-XX:+UseParallelGC
|
||||
CMS 垃圾收集器:-XX:+UseConcMarkSweepGC
|
||||
G1 垃圾收集器:-XX:+UseG1GC
|
||||
|
||||
|
||||
7.3 什么是串行垃圾收集?
|
||||
|
||||
就是只有单个 worker 线程来执行 GC 工作。
|
||||
|
||||
7.4 什么是并行垃圾收集?
|
||||
|
||||
并行垃圾收集,是指使用多个 GC worker 线程并行地执行垃圾收集,能充分利用多核 CPU 的能力,缩短垃圾收集的暂停时间。
|
||||
|
||||
除了单线程的 GC,其他的垃圾收集器,比如 PS、CMS、G1 等新的垃圾收集器都使用了多个线程来并行执行 GC 工作。
|
||||
|
||||
7.5 什么是并发垃圾收集器?
|
||||
|
||||
并发垃圾收集器,是指在应用程序在正常执行时,有一部分 GC 任务,由 GC 线程在应用线程一起并发执行。 例如 CMS/G1 的各种并发阶段。
|
||||
|
||||
7.6 什么是增量式垃圾收集?
|
||||
|
||||
首先,G1 的堆内存不再单纯划分为年轻代和老年代,而是划分为多个(通常是 2048 个)可以存放对象的小块堆区域(smaller heap regions)。
|
||||
|
||||
每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor 区或者 Old 区。
|
||||
|
||||
这样划分之后,使得 G1 不必每次都去回收整个堆空间,而是以增量的方式来进行处理:每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。
|
||||
|
||||
下一次 GC 时在本次的基础上,再选定一定的区域来进行回收。增量式垃圾收集的好处是大大降低了单次 GC 暂停的时间。
|
||||
|
||||
7.7 什么是年轻代?
|
||||
|
||||
年轻代是分来垃圾收集算法中的一个概念,相对于老年代而言,年轻代一般包括:
|
||||
|
||||
|
||||
新生代,Eden 区。
|
||||
存活区,执行年轻代 GC 时,用存活区来保存活下来的对象。存活区也是年轻代的一部分,但一般有 2 个存活区,所以可以来回倒腾。
|
||||
|
||||
|
||||
7.8 什么是 GC 停顿(GC pause)?
|
||||
|
||||
因为 GC 过程中,有一部分操作需要等所有应用线程都到达安全点,暂停之后才能执行,这时候就叫做 GC 停顿,或者叫做 GC 暂停。
|
||||
|
||||
7.9 GC 停顿与 STW 停顿有什么区别?
|
||||
|
||||
这两者一般可以认为就是同一个意思。
|
||||
|
||||
8. 如果 CPU 使用率突然飙升,你会怎么排查?
|
||||
|
||||
缺乏经验的话,针对当前问题,往往需要使用不同的工具来收集信息,例如:
|
||||
|
||||
|
||||
收集不同的指标(CPU、内存、磁盘 IO、网络等等)
|
||||
分析应用日志
|
||||
分析 GC 日志
|
||||
获取线程转储并分析
|
||||
获取堆转储来进行分析
|
||||
|
||||
|
||||
8.1 如果系统响应变慢,你会怎么排查?
|
||||
|
||||
一般根据 APM 监控来排查应用系统本身的问题,有时候也可以使用 Chrome 浏览器等工具来排查外部原因,比如网络问题。
|
||||
|
||||
8.2 系统性能一般怎么衡量?
|
||||
|
||||
可量化的 3 个性能指标:
|
||||
|
||||
|
||||
系统容量:比如硬件配置,设计容量;
|
||||
吞吐量:最直观的指标是 TPS;
|
||||
响应时间:也就是系统延迟,包括服务端延时和网络延迟。
|
||||
|
||||
|
||||
这些指标。可以具体拓展到单机并发、总体并发、数据量、用户数、预算成本等等。
|
||||
|
||||
9. 使用过哪些 JVM 相关的工具?
|
||||
|
||||
这个问题请根据实际情况回答,比如 Linux 命令,或者 JDK 提供的工具等。
|
||||
|
||||
9.1 查看 JVM 进程号的命令是什么?
|
||||
|
||||
可以使用 ps -ef 和 jps -v 等等。
|
||||
|
||||
9.2 怎么查看剩余内存?
|
||||
|
||||
比如:free -m、free -h、top 命令等等。
|
||||
|
||||
9.3 查看线程栈的工具是什么?
|
||||
|
||||
一般先使用 jps 命令,再使用 jstack -l。
|
||||
|
||||
9.4 用什么工具来获取堆内存转储?
|
||||
|
||||
一般使用 jmap 工具来获取堆内存快照。
|
||||
|
||||
9.5 内存 Dump 时有哪些注意事项?
|
||||
|
||||
根据实际情况来看,获取内存快照可能会让系统暂停或阻塞一段时间,根据内存量决定。
|
||||
|
||||
使用 jmap 时,如果指定 live 参数,则会触发一次 Full GC,需要注意。
|
||||
|
||||
9.6 使用 JMAP 转储堆内存大致的参数怎么处理?
|
||||
|
||||
示例:
|
||||
|
||||
jmap -dump:format=b,file=3826.hprof 3826
|
||||
|
||||
|
||||
|
||||
9.7 为什么转储文件以 .hprof 结尾?
|
||||
|
||||
JVM 有一个内置的分析器叫做 HPROF,堆内存转储文件的格式,最早就是这款工具定义的。
|
||||
|
||||
9.8 内存 Dump 完成之后,用什么工具来分析?
|
||||
|
||||
一般使用 Eclipse MAT 工具,或者 jhat 工具来处理。
|
||||
|
||||
9.9 如果忘记了使用什么参数你一般怎么处理?
|
||||
|
||||
上网搜索是比较笨的办法,但也是一种办法。
|
||||
|
||||
另外就是,各种 JDK 工具都支持 -h 选项来查看帮助信息,只要用得比较熟练,即使忘记了也很容易根据提示进行操作。
|
||||
|
||||
10. 开发性问题:你碰到过哪些 JVM 问题?
|
||||
|
||||
比如 GC 问题、内存泄漏问题、或者其他疑难杂症等等。然后可能还有一些后续的问题。例如:
|
||||
|
||||
|
||||
你遇到过的印象最深的 JVM 问题是什么?
|
||||
这个问题是怎么分析和解决的?
|
||||
这个过程中有哪些值得分享的经验?
|
||||
|
||||
|
||||
此问题为开放性问题,请根据自身情况进行回答,可以把自己思考的答案发到本专栏的微信群里,我们会逐个进行分析点评。
|
||||
|
||||
|
||||
|
||||
|
464
专栏/JVM核心技术32讲(完)/32应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海.md
Normal file
464
专栏/JVM核心技术32讲(完)/32应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海.md
Normal file
@ -0,0 +1,464 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
32 应对容器时代面临的挑战:长风破浪会有时、直挂云帆济沧海
|
||||
当今的时代,容器的使用越来越普及,Cgroups、Docker、Kubernetes 等项目和技术越来越成熟,成为很多大规模集群的基石。
|
||||
|
||||
容器是一种沙盒技术,可以对资源进行调度分配和限制配额、对不同应用进行环境隔离。
|
||||
|
||||
容器时代不仅给我们带来的机遇,也带来了很多挑战。跨得过去就是机会,跳不过去就是坑。
|
||||
|
||||
在容器环境下,要直接进行调试并不容易,我们更多地是进行应用性能指标的采集和监控,并构建预警机制。而这需要架构师、开发、测试、运维人员的协作。
|
||||
|
||||
但监控领域的工具又多又杂,而且在持续发展和不断迭代。最早期的监控,只在系统发布时检查服务器相关的参数,并将这些参数用作系统运行状况的指标。监控服务器的健康状况,与用户体验之间紧密相关,悲剧在于监控的不完善,导致发生的问题比实际检测到的要多很多。
|
||||
|
||||
随着时间推移,日志管理、预警、遥测以及系统报告领域持续发力。其中有很多有效的措施,诸如安全事件、有效警报、记录资源使用量等等。但前提是我们需要有一个清晰的策略和对应工具,进行用户访问链路跟踪,比如 Zabbix、Nagios 以及 Prometheus 等工具在生产环境中被广泛使用。
|
||||
|
||||
性能问题的关键是人,也就是我们的用户。但已有的这些工具并没有实现真正的用户体验监控。仅仅使用这些软件也不能缓解性能问题,我们还需要采取各种措施,在勇敢和专注下不懈地努力。
|
||||
|
||||
一方面,Web 系统的问题诊断和性能调优,是一件意义重大的事情。需要严格把控,也需要付出很多精力。
|
||||
|
||||
当然,成功实施这些工作对企业的回报也是巨大的!
|
||||
|
||||
另一方面,拿 Java 领域事实上的标准 Spring 来说,SpringBoot 提供了一款应用指标收集器——Micrometer,官方文档连接:https://micrometer.io/docs。
|
||||
|
||||
|
||||
支持直接将数据上报给 Elasticsearch、Datadog、InfluxData 等各种流行的监控系统。
|
||||
自动采集最大延迟、平均延迟、95% 线、吞吐量、内存使用量等指标。
|
||||
|
||||
|
||||
此外,在小规模集群中,我们还可以使用 Pinpoint、Skywalking 等开源 APM 工具。
|
||||
|
||||
容器环境的资源隔离性
|
||||
|
||||
容器毕竟是一种轻量级的实现方式,所以其封闭性不如虚拟机技术。
|
||||
|
||||
举个例子:
|
||||
|
||||
|
||||
物理机/宿主机有 96 个 CPU 内核、256GB 物理内存,容器限制的资源是 4 核 8G,那么容器内部的 JVM 进程看到的内核数和内存数是多少呢?
|
||||
|
||||
目前来说,JVM 看到的内核数是 96,内存值是 256G。
|
||||
|
||||
|
||||
这会造成一些问题,基于 CPU 内核数 availableProcessors 的各种算法都会受到影响,比如默认 GC 线程数:假如啥都不配置,JVM 看见 96 个内核,设置 GC 并行线程数为 96*5/8~=60,但容器限制了只能使用 4 个内核资源,于是 60 个并行 GC 线程来争抢 4 个机器内核,造成严重的 GC 性能问题。
|
||||
|
||||
同样的道理,很多线程池的实现,根据内核数量来设置并发线程数,也会造成剧烈的资源争抢。如果容器不限制资源的使用也会造成一些困扰,比如下面介绍的坏邻居效应。基于物理内存 totalPhysicalMemorySize 和空闲内存 freePhysicalMemorySize 等配置信息的算法也会产生一些奇怪的 Bug。
|
||||
|
||||
最新版的 JDK 加入了一些修正手段。
|
||||
|
||||
JDK 对容器的支持和限制
|
||||
|
||||
新版 JDK 支持 Docker 容器的 CPU 和内存限制:
|
||||
|
||||
|
||||
https://blogs.oracle.com/java-platform-group/java-se-support-for-docker-cpu-and-memory-limits
|
||||
|
||||
|
||||
可以增加 JVM 启动参数来读取 Cgroups 对 CPU 的限制:
|
||||
|
||||
|
||||
https://www.oracle.com/technetwork/java/javase/8u191-relnotes-5032181.html#JDK-8146115
|
||||
|
||||
|
||||
Hotspot 是一个规范的开源项目,关于 JDK 的新特性,可以阅读官方的邮件订阅,例如:
|
||||
|
||||
|
||||
https://mail.openjdk.java.net/pipermail/jdk8u-dev/
|
||||
|
||||
|
||||
其他版本的 JDK 特性,也可以按照类似的命名规范,从官网的 Mailing Lists 中找到:
|
||||
|
||||
|
||||
https://mail.openjdk.java.net/mailman/listinfo
|
||||
|
||||
|
||||
关于这个问题的排查和分析,请参考前面的章节[《JVM 问题排查分析调优经验》]。
|
||||
|
||||
坏邻居效应
|
||||
|
||||
有共享资源的地方,就会有资源争用。在计算机领域,共享的资源主要包括:
|
||||
|
||||
|
||||
网络
|
||||
磁盘
|
||||
CPU
|
||||
内存
|
||||
|
||||
|
||||
在多租户的公有云环境中,会存在一种严重的问题,称为“坏邻居效应”(noisy neighbor phenomenon)。当一个或多个客户过度使用了某种公共资源时,就会明显损害到其他客户的系统性能。(就像是小区宽带一样)
|
||||
|
||||
吵闹的坏邻居(noisy neighbor),用于描述云计算领域中,用来描述抢占共有带宽,磁盘 I/O、CPU 以及其他资源的行为。
|
||||
|
||||
坏邻居效应,对同一环境下的其他虚拟机/应用的性能会造成影响或抖动。一般来说,会对其他用户的性能和体验造成恶劣的影响。
|
||||
|
||||
云,是一种多租户环境,同一台物理机,会共享给多个客户来运行程序/存储数据。
|
||||
|
||||
坏邻居效应产生的原因,是某个虚拟机/应用霸占了大部分资源,进而影响到其他客户的性能。
|
||||
|
||||
带宽不足是造成网络性能问题的主要原因。在网络中传输数据严重依赖带宽的大小,如果某个应用或实例占用太多的网络资源,很可能对其他用户造成延迟/缓慢。坏邻居会影响虚拟机、数据库、网络、存储以及其他云服务。
|
||||
|
||||
有一种避免坏邻居效应的方法,是使用裸机云(bare-metal cloud)。裸机云在硬件上直接运行一个应用,相当于创建了一个单租户环境,所以能消除坏邻居。虽然单租户环境避免了坏邻居效应,但并没有解决根本问题。超卖(over-commitment)或者共享给太多的租户,都会限制整个云环境的性能。
|
||||
|
||||
另一种避免坏邻居效应的方法,是通过在物理机之间进行动态迁移,以保障每个客户获得必要的资源。此外,还可以通过 存储服务质量保障(QoS,quality of service)控制每个虚拟机的 IOPS,来限制坏邻居效应。通过 IOPS 来限制每个虚拟机使用的资源量,就不会造成某个客户的虚机/应用/实例去挤占其他客户的资源/性能。
|
||||
|
||||
有兴趣的同学可以查看:
|
||||
|
||||
|
||||
谈谈公有云的坏邻居效应
|
||||
|
||||
|
||||
GC 日志监听
|
||||
|
||||
从 JDK 7 开始,每一款垃圾收集器都提供了通知机制,在程序中监听 GarbageCollectorMXBean,即可在垃圾收集完成后收到 GC 事件的详细信息。目前的监听机制只能得到 GC 完成之后的 Pause 数据,其它环节的 GC 情况无法观察到。
|
||||
|
||||
一个简单的监听程序实现如下:
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.sun.management.GarbageCollectionNotificationInfo;
|
||||
import com.sun.management.GcInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.management.ListenerNotFoundException;
|
||||
import javax.management.Notification;
|
||||
import javax.management.NotificationEmitter;
|
||||
import javax.management.NotificationListener;
|
||||
import javax.management.openmbean.CompositeData;
|
||||
import java.lang.management.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* GC 日志监听并输出到 Log
|
||||
* JVM 启动参数示例:
|
||||
* -Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=50
|
||||
* -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
|
||||
*/
|
||||
@Configuration
|
||||
public class BindGCNotifyConfig {
|
||||
|
||||
public BindGCNotifyConfig() {
|
||||
}
|
||||
|
||||
//
|
||||
private Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
private final AtomicBoolean inited = new AtomicBoolean(Boolean.FALSE);
|
||||
private final List<Runnable> notifyCleanTasks = new CopyOnWriteArrayList<Runnable>();
|
||||
private final AtomicLong maxPauseMillis = new AtomicLong(0L);
|
||||
private final AtomicLong maxOldSize = new AtomicLong(getOldGen().getUsage().getMax());
|
||||
private final AtomicLong youngGenSizeAfter = new AtomicLong(0L);
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
try {
|
||||
doInit();
|
||||
} catch (Throwable e) {
|
||||
logger.warn("[GC 日志监听-初始化]失败! ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
for (Runnable task : notifyCleanTasks) {
|
||||
task.run();
|
||||
}
|
||||
notifyCleanTasks.clear();
|
||||
}
|
||||
|
||||
private void doInit() {
|
||||
//
|
||||
if (!inited.compareAndSet(Boolean.FALSE, Boolean.TRUE)) {
|
||||
return;
|
||||
}
|
||||
logger.info("[GC 日志监听-初始化]maxOldSize=" + mb(maxOldSize.longValue()));
|
||||
|
||||
// 每个 mbean 都注册监听
|
||||
for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
if (!(mbean instanceof NotificationEmitter)) {
|
||||
continue;
|
||||
}
|
||||
final NotificationEmitter notificationEmitter = (NotificationEmitter) mbean;
|
||||
// 添加监听
|
||||
final NotificationListener notificationListener = getNewListener(mbean);
|
||||
notificationEmitter.addNotificationListener(notificationListener, null, null);
|
||||
|
||||
logger.info("[GC 日志监听-初始化]MemoryPoolNames=" + JSON.toJSONString(mbean.getMemoryPoolNames()));
|
||||
// 加入清理队列
|
||||
notifyCleanTasks.add(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// 清理掉绑定的 listener
|
||||
notificationEmitter.removeNotificationListener(notificationListener);
|
||||
} catch (ListenerNotFoundException e) {
|
||||
logger.error("[GC 日志监听-清理]清理绑定的 listener 失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationListener getNewListener(final GarbageCollectorMXBean mbean) {
|
||||
//
|
||||
final NotificationListener listener = new NotificationListener() {
|
||||
@Override
|
||||
public void handleNotification(Notification notification, Object ref) {
|
||||
// 只处理 GC 事件
|
||||
if (!notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
|
||||
return;
|
||||
}
|
||||
CompositeData cd = (CompositeData) notification.getUserData();
|
||||
GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd);
|
||||
//
|
||||
JSONObject gcDetail = new JSONObject();
|
||||
|
||||
String gcName = notificationInfo.getGcName();
|
||||
String gcAction = notificationInfo.getGcAction();
|
||||
String gcCause = notificationInfo.getGcCause();
|
||||
GcInfo gcInfo = notificationInfo.getGcInfo();
|
||||
// duration 是指 Pause 阶段的总停顿时间,并发阶段没有 pause 不会通知。
|
||||
long duration = gcInfo.getDuration();
|
||||
if (maxPauseMillis.longValue() < duration) {
|
||||
maxPauseMillis.set(duration);
|
||||
}
|
||||
long gcId = gcInfo.getId();
|
||||
//
|
||||
String type = "jvm.gc.pause";
|
||||
//
|
||||
if (isConcurrentPhase(gcCause)) {
|
||||
type = "jvm.gc.concurrent.phase.time";
|
||||
}
|
||||
//
|
||||
gcDetail.put("gcName", gcName);
|
||||
gcDetail.put("gcAction", gcAction);
|
||||
gcDetail.put("gcCause", gcCause);
|
||||
gcDetail.put("gcId", gcId);
|
||||
gcDetail.put("duration", duration);
|
||||
gcDetail.put("maxPauseMillis", maxPauseMillis);
|
||||
gcDetail.put("type", type);
|
||||
gcDetail.put("collectionCount", mbean.getCollectionCount());
|
||||
gcDetail.put("collectionTime", mbean.getCollectionTime());
|
||||
|
||||
// 存活数据量
|
||||
AtomicLong liveDataSize = new AtomicLong(0L);
|
||||
// 提升数据量
|
||||
AtomicLong promotedBytes = new AtomicLong(0L);
|
||||
|
||||
// Update promotion and allocation counters
|
||||
final Map<String, MemoryUsage> before = gcInfo.getMemoryUsageBeforeGc();
|
||||
final Map<String, MemoryUsage> after = gcInfo.getMemoryUsageAfterGc();
|
||||
//
|
||||
Set<String> keySet = new HashSet<String>();
|
||||
keySet.addAll(before.keySet());
|
||||
keySet.addAll(after.keySet());
|
||||
//
|
||||
final Map<String, String> afterUsage = new HashMap<String, String>();
|
||||
//
|
||||
for (String key : keySet) {
|
||||
final long usedBefore = before.get(key).getUsed();
|
||||
final long usedAfter = after.get(key).getUsed();
|
||||
long delta = usedAfter - usedBefore;
|
||||
// 判断是 yong 还是 old,算法不同
|
||||
if (isYoungGenPool(key)) {
|
||||
delta = usedBefore - youngGenSizeAfter.get();
|
||||
youngGenSizeAfter.set(usedAfter);
|
||||
} else if (isOldGenPool(key)) {
|
||||
if (delta > 0L) {
|
||||
// 提升到老年代的量
|
||||
promotedBytes.addAndGet(delta);
|
||||
gcDetail.put("promotedBytes", mb(promotedBytes));
|
||||
}
|
||||
if (delta < 0L || GcGenerationAge.OLD.contains(gcName)) {
|
||||
liveDataSize.set(usedAfter);
|
||||
gcDetail.put("liveDataSize", mb(liveDataSize));
|
||||
final long oldMaxAfter = after.get(key).getMax();
|
||||
if (maxOldSize.longValue() != oldMaxAfter) {
|
||||
maxOldSize.set(oldMaxAfter);
|
||||
// 扩容;老年代的 max 有变更
|
||||
gcDetail.put("maxOldSize", mb(maxOldSize));
|
||||
}
|
||||
}
|
||||
} else if (delta > 0L) {
|
||||
//
|
||||
} else if (delta < 0L) {
|
||||
// 判断 G1
|
||||
}
|
||||
afterUsage.put(key, mb(usedAfter));
|
||||
}
|
||||
//
|
||||
gcDetail.put("afterUsage", afterUsage);
|
||||
//
|
||||
|
||||
logger.info("[GC 日志监听-GC 事件]gcId={}; duration:{}; gcDetail: {}", gcId, duration, gcDetail.toJSONString());
|
||||
}
|
||||
};
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
private static String mb(Number num) {
|
||||
long mbValue = num.longValue() / (1024 * 1024);
|
||||
if (mbValue < 1) {
|
||||
return "" + mbValue;
|
||||
}
|
||||
return mbValue + "MB";
|
||||
}
|
||||
|
||||
private static MemoryPoolMXBean getOldGen() {
|
||||
List<MemoryPoolMXBean> list = ManagementFactory
|
||||
.getPlatformMXBeans(MemoryPoolMXBean.class);
|
||||
//
|
||||
for (MemoryPoolMXBean memoryPoolMXBean : list) {
|
||||
// 非堆的部分-不是老年代
|
||||
if (!isHeap(memoryPoolMXBean)) {
|
||||
continue;
|
||||
}
|
||||
if (!isOldGenPool(memoryPoolMXBean.getName())) {
|
||||
continue;
|
||||
}
|
||||
return (memoryPoolMXBean);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isConcurrentPhase(String cause) {
|
||||
return "No GC".equals(cause);
|
||||
}
|
||||
|
||||
private static boolean isYoungGenPool(String name) {
|
||||
return name.endsWith("Eden Space");
|
||||
}
|
||||
|
||||
private static boolean isOldGenPool(String name) {
|
||||
return name.endsWith("Old Gen") || name.endsWith("Tenured Gen");
|
||||
}
|
||||
|
||||
private static boolean isHeap(MemoryPoolMXBean memoryPoolBean) {
|
||||
return MemoryType.HEAP.equals(memoryPoolBean.getType());
|
||||
}
|
||||
|
||||
private enum GcGenerationAge {
|
||||
OLD,
|
||||
YOUNG,
|
||||
UNKNOWN;
|
||||
|
||||
private static Map<String, GcGenerationAge> knownCollectors = new HashMap<String, BindGCNotifyConfig.GcGenerationAge>() {{
|
||||
put("ConcurrentMarkSweep", OLD);
|
||||
put("Copy", YOUNG);
|
||||
put("G1 Old Generation", OLD);
|
||||
put("G1 Young Generation", YOUNG);
|
||||
put("MarkSweepCompact", OLD);
|
||||
put("PS MarkSweep", OLD);
|
||||
put("PS Scavenge", YOUNG);
|
||||
put("ParNew", YOUNG);
|
||||
}};
|
||||
|
||||
static GcGenerationAge fromName(String name) {
|
||||
return knownCollectors.getOrDefault(name, UNKNOWN);
|
||||
}
|
||||
|
||||
public boolean contains(String name) {
|
||||
return this == fromName(name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
不只是 GC 事件,内存相关的信息都可以通过 JMX 来实现监听。很多 APM 也是通过类似的手段来实现数据上报。
|
||||
|
||||
APM 工具与监控系统
|
||||
|
||||
在线可视化监控是如今生产环境必备的一个功能。业务出错和性能问题随时都可能会发生,而且现在很多系统不再有固定的业务窗口期,所以必须做到 7x24 小时的实时监控。
|
||||
|
||||
目前业界有很多监控工具,各有优缺点,需要根据需要进行抉择。
|
||||
|
||||
一般来说,系统监控可以分为三个部分:
|
||||
|
||||
|
||||
系统性能监控,包括 CPU、内存、磁盘 IO、网络等硬件资源和系统负载的监控信息。
|
||||
业务日志监控,场景的是 ELK 技术栈、并使用 Logback+Kafka 等技术来采集日志。
|
||||
APM 性能指标监控,比如 QPS、TPS、响应时间等等,例如 MicroMeter、Pinpoint 等。
|
||||
|
||||
|
||||
系统监控的模块也是两大块:
|
||||
|
||||
|
||||
指标采集部分
|
||||
数据可视化系统
|
||||
|
||||
|
||||
如今监控工具是生产环境的重要组成部分。测量结果的可视化、错误追踪、性能监控和应用分析是对应用的运行状况进行深入观测的基本手段。
|
||||
|
||||
认识到这一需求非常容易,但要选择哪一款监控工具或者哪一组监控工具却异常困难。
|
||||
|
||||
下面介绍几款监测工具,这些工具包括混合开源和 SaaS 模式,每个都有其优缺点,可以说没有完美的工具,只有合适的工具。
|
||||
|
||||
指标采集客户端
|
||||
|
||||
|
||||
Micrometer:作为指标采集的基础类库,基于客户端机器来进行,用户无需关注具体的 JVM 版本和厂商。以相同的方式来配置,可以对接到不同的可视化监控系统服务。主要用于监控、告警,以及对当前的系统环境变化做出响应。Micrometer 还会注册 JMX 相关的 MBeans,非常简单和方便地在本地通过 JMX 来查看相关指标。如果是生产环境中使用,则一般是将监控指标导出到其他监控系统中保存起来。
|
||||
云服务监控系统:云服务监控系统厂商一般都会提供配套的指标采集客户端,并对外开放各种 API 接口和数据标准,允许客户使用自己的指标采集系统。
|
||||
开源监控系统:各种开源监控系统也会提供对应的指标采集客户端。
|
||||
|
||||
|
||||
云服务监控系统
|
||||
|
||||
SaaS 服务的监控系统一般提供存储、查询、可视化等功能的一体化云服务。大多包含免费试用和收费服务两种模式。如果企业和机构的条件允许,付费使用云服务一般是最好的选择,毕竟“免费的才是最贵的”。
|
||||
|
||||
下面我们一起来看看有哪些云服务:
|
||||
|
||||
|
||||
AppOptics,支持 APM 和系统监控的 SaaS 服务,支持各种仪表板和时间轴等监控界面,提供 API 和客户端。
|
||||
Datadog,支持 APM 和系统监控的 SaaS 服务,内置各种仪表板,支持告警。支持 API 和客户端,以及客户端代理。
|
||||
Dynatrace,支持 APM 和系统监控的 SaaS 服务,内置各种仪表板,集成了监控和分析平台。
|
||||
Humio,支持 APM、日志和系统监控的 SaaS 服务。
|
||||
Instana,支持自动 APM、系统监控的 SaaS 服务。
|
||||
New Relic,这是一款具有完整 UI 的可视化 SaaS 产品,支持 NRQL 查询语言,New Relic Insights 基于推模型来运行。
|
||||
SignalFx,在推送模型上运行的 SaaS 服务,具有完整 UI。支持实时的系统性能、微服务,以及 APM 监控系统,支持多样化的预警“检测器”。
|
||||
Stackdriver,是 Google Cloud 的嵌入式监测套件,用于监控云基础架构、软件和应用的性能,排查其中的问题并加以改善。这个监测套件属于 SaaS 服务,支持内置仪表板和告警功能。
|
||||
Wavefront,是基于 SaaS 的指标监视和分析平台,支持可视化查询,以及预警监控等功能,包括系统性能、网络、自定义指标、业务 KPI 等等。
|
||||
听云,是国内最大的应用性能管理(APM)解决方案提供商。可以实现应用性能全方位可视化,从 PC 端、浏览器端、移动客户端到服务端,监控定位崩溃、卡顿、交互过慢、第三方 API 调用失败、数据库性能下降、CDN 质量差等多维复杂的性能问题。
|
||||
OneAPM,OneAPM(蓝海讯通)提供端到端 APM 应用性能管理软件及应用性能监控软件解决方案。
|
||||
Plumbr,监测可用性和性能问题,使用跟踪技术,能迅速定位错误相关的位置信息,发现、验证和修复各种故障和性能问题。
|
||||
Takipi,现在改名叫做 OverOps,系统故障实时监测系统。能快速定位问题发生的时间、位置和原因。
|
||||
|
||||
|
||||
其中做得比较好的有国外的 Datadog,国内的听云。
|
||||
|
||||
开源监控系统
|
||||
|
||||
|
||||
Pinpoint,受 Dapper 启发,使用 Java/PHP 来实现的大型分布式系统 APM 工具。Pinpoint 提供了一套解决方案,可通过跟踪分布式应用程序之间的事务来快速定位调用链路。
|
||||
Atlas,是 Netflix 旗下的一款开源的,基于内存的时序数据库,内置图形界面,支持高级数学运算和自定义查询语言。
|
||||
ELK 技术栈,一般用于日志监控,Elasticsearch 是搜索引擎,支持各种数据和指标存储,日志监控一般通过 Logstash 执行分析,Kibana 负责人机交互和可视化。
|
||||
Influx,InfluxDB 是由 InfluxData 开发的一款开源时序型数据库。它由 Go 写成,着力于高性能地查询与存储时序数据。InfluxDB 被广泛应用于存储系统的监控数据、IoT 行业的实时数据等场景,通过类似 SQL 的查询语言来完成数据分析。InfluxData 工具套件可用于实时流处理,支持抽样采集指标、自动过期、删除不需要的数据,以及备份和还原等功能。
|
||||
Ganglia,用于高性能计算系统、群集和网络的可伸缩的分布式监控工具。起源于加州大学伯克利分校,是一款历史悠久的多层级指标监控系统,在 Linux 系统中广受欢迎。
|
||||
Graphite,当前非常流行的多层级次指标监控系统,使用固定数量的底层数据库,其设计和目的与 RRD 相似。由 Orbitz 在 2006 年创建,并于 2008 年开源。
|
||||
KairosDB,是建立在 Apache Cassandra 基础上的时序数据库。可以通过 Grafana 来绘制精美漂亮的监控图表。
|
||||
Prometheus,具有简单的内置 UI,支持自定义查询语言和数学运算的、开源的内存时序数据库。Prometheus 设计为基于拉模型来运行,根据服务发现,定期从应用程序实例中收集指标。
|
||||
StatsD,开源的、简单但很强大的统计信息聚合服务器。
|
||||
|
||||
|
||||
其中 Pinpoint 和 Prometheus 比较受欢迎。
|
||||
|
||||
参考链接
|
||||
|
||||
|
||||
利用 JMX 的 Notifications 监听 GC
|
||||
推荐 7 个超棒的监控工具
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user