3、JVM调优基础:堆内存与栈内存配置、GC算法选择、JVM参数调优实战

说到ThingsBoard的性能调优,JVM这块是绕不开的硬骨头。我刚开始接触ThingsBoard时,也踩过不少坑——明明服务器配置不差,但系统就是卡顿,时不时还来个Full GC把服务搞挂。后来才发现,问题出在JVM参数上。

这一章,咱们就聊聊JVM调优的基础。说白了,就是搞清楚堆内存和栈内存怎么分,GC算法怎么选,以及那些关键的JVM参数到底该怎么配。

3.1 堆内存(Heap)与栈内存(Stack)配置

先说说堆内存。ThingsBoard作为一个物联网平台,要处理大量设备数据、规则链执行、告警计算等任务。这些对象都住在堆里。堆配小了,频繁GC;配大了,又浪费资源。

堆内存的核心配置

我个人习惯,先看ThingsBoard的负载类型。如果是纯消息转发场景,堆内存可以小一些;如果规则链复杂、有大量聚合计算,堆就得给足。

# 典型的ThingsBoard JVM堆配置
-Xms4g          # 初始堆大小
-Xmx8g          # 最大堆大小
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

这里有个经验值:堆内存不要超过物理内存的70%。为什么?因为操作系统、其他进程、还有堆外内存都需要空间。我曾经在一台16G的服务器上把堆设成了12G,结果系统频繁SWAP,性能反而更差。

堆内存分配建议(基于ThingsBoard场景)

设备数量 规则链复杂度 建议堆大小
< 1万 简单 2G - 4G
1万 - 10万 中等 4G - 8G
10万 - 50万 复杂 8G - 16G
> 50万 高复杂度 16G - 32G(建议集群)

栈内存(Stack)配置

栈内存容易被忽略。ThingsBoard的规则链执行是深度递归的,特别是嵌套的脚本节点。如果栈配小了,很容易抛出 StackOverflowError

-Xss512k   # 每个线程的栈大小

嗯,这里要注意:栈不是越大越好。栈越大,能创建的线程数就越少。ThingsBoard的Netty线程池、规则链执行线程、定时任务线程,加起来可能上百个。我建议栈大小设在256k到1m之间,根据规则链的嵌套深度来调整。

小技巧: 如果你发现规则链执行时频繁报StackOverflowError,先别急着加栈大小。检查一下规则链是不是有循环引用,或者脚本节点是不是写了死递归。我曾经遇到一个客户,一个规则链嵌套了20多层,栈加到2m才解决——后来发现是设计问题。

3.2 GC算法选择:G1还是ZGC?

GC算法的选择,直接影响ThingsBoard的响应时间。你想想看,设备上报数据时,如果正好赶上Full GC,那延迟就上去了。

G1 GC:成熟稳定的选择

G1是JDK 11及以后版本的默认GC。它把堆分成多个Region,能控制GC暂停时间。对于ThingsBoard这种需要平衡吞吐量和延迟的场景,G1是个好选择。

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200    # 目标暂停时间200ms
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发GC的堆占用阈值

我个人习惯把 MaxGCPauseMillis 设在100-300ms之间。设得太小,GC会过于频繁,反而降低吞吐量。

ZGC:低延迟的利器

如果你用的是JDK 17+,而且对延迟要求极高(比如毫秒级响应),ZGC值得一试。ZGC的暂停时间基本在10ms以内,几乎不影响业务。

-XX:+UseZGC
-XX:ConcGCThreads=2          # 并发GC线程数
-XX:ParallelGCThreads=4      # 并行GC线程数

不过,ZGC也有代价。它更吃CPU,而且堆内存越大,效果越好。我建议堆在8G以上再考虑ZGC。

避坑指南: 我曾经在一个4G堆的ThingsBoard实例上试过ZGC,结果CPU飙到90%以上,GC倒是快了,但业务处理能力反而下降了。所以,ZGC不是万能的,要根据场景选。

GC算法对比

特性 G1 GC ZGC
JDK版本 JDK 9+ JDK 15+(实验性)/ JDK 17+(正式)
暂停时间 100-300ms < 10ms
CPU开销 中等 较高
堆内存建议 4G - 16G 8G以上
适用场景 通用场景,平衡吞吐和延迟 低延迟敏感场景

3.3 JVM参数调优实战

理论知识说完了,咱们来点实际的。下面是我在多个ThingsBoard生产环境中验证过的一套JVM参数模板。

# ThingsBoard JVM调优参数模板(8G堆,G1 GC)
-server
-Xms8g
-Xmx8g
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-Xss512k

# GC配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1HeapRegionSize=4m
-XX:G1NewSizePercent=10
-XX:G1MaxNewSizePercent=30

# GC日志(用于排查问题)
-Xlog:gc*:file=/var/log/thingsboard/gc.log:time,uptime,level,tags:filecount=10,filesize=10m

# 内存溢出时自动dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/thingsboard/heapdump.hprof

# 其他优化
-XX:+DisableExplicitGC
-XX:+UseStringDeduplication
-Djava.awt.headless=true

参数解读

  • -XX:G1HeapRegionSize=4m:Region大小设为4m,适合ThingsBoard这种对象大小不一的场景。太小了Region数量多,管理开销大;太大了大对象分配容易浪费。
  • -XX:G1NewSizePercent=10-XX:G1MaxNewSizePercent=30:控制年轻代大小。ThingsBoard的规则链执行会产生大量临时对象,年轻代太小会导致频繁Minor GC。
  • -XX:+DisableExplicitGC:禁用显式GC调用。有些第三方库会调用 System.gc(),这个参数可以防止它们触发Full GC。
  • -XX:+UseStringDeduplication:去重重复的字符串。ThingsBoard的设备名称、数据类型等字符串重复率很高,这个参数能有效减少堆内存占用。

调优步骤建议:

  1. 先用默认参数跑一周,收集GC日志和性能指标。
  2. 分析GC频率和暂停时间,确定瓶颈。
  3. 逐步调整参数,每次只改1-2个,观察效果。
  4. 不要一次性把所有参数都改了,否则出了问题你都不知道是哪个参数导致的。

实战案例

我记得有一次,一个客户的ThingsBoard集群在设备量达到5万时,频繁出现Full GC,每次暂停时间超过5秒。我上去一看,堆内存设了16G,但 InitiatingHeapOccupancyPercent 用的是默认值45。问题出在哪里?

原来,他们的规则链里有个节点会缓存大量设备状态数据,导致老年代增长很快。45%的阈值太低了,G1过早开始并发标记,但标记过程中又有大量新对象进入老年代,最终触发Full GC。

我把 InitiatingHeapOccupancyPercent 调到了60,同时把 G1HeapRegionSize 从默认的1m调到了4m。调整后,Full GC消失了,暂停时间稳定在150ms左右。

注意: JVM参数调优没有银弹。同样的参数,在不同的硬件、不同的负载下,效果可能完全不同。一定要基于实际监控数据做调整,不要照搬别人的配置。

好了,这一章的内容就到这里。下一章我们会聊聊ThingsBoard的数据库调优,特别是Cassandra和PostgreSQL的配置优化。到时候见。


公众号:蓝海资料掘金营,微信deep3321