系统最近老年代的内存上升的比较快, 三到四天会发生一波 fullGC. 于是开始对 GC 的情况做一波分析.
线上老年代 2.7G, 年轻带 1.3G 老年代上升较快, 3 天一波 fullGC, 并且 fullGC 会把内存回收, 有时回收一般, 有时回收全部. 所以判断是不会有内存泄漏现象的, 内存发生泄漏是回收不了的. 第二个判断, 不存在大对象, 一个是基于对程序的理解, 一个是对于老年代上升的速率, 基本是稳固上升. 不存在峰值.
我是先用 jstack 命令打印出线程状态的 jstack -l pid >> 文件名 发现这么操作是无用的. jstack 主要是分析线程状态的, 尤其是针对占用 CPU, 死循环, 死锁操作等.
一开始确实定位到一个线程, 是我们服务每个一分钟拉去配置中心数据的线程, 数据量很小, 而且基本都是重复的数据. 所以那会没有头绪方向不对.
后来老大说打下内存快照啊. 于是就开始看下内存快照了.
1, 先使用 jmap 命令打印出内存快照文件.
jmap -dump:format=b,file= 文件路径 pid(进程号)
2, 使用 MemoryAnalyzer(Windows 下的分析工具) 工具分析大对象
分析出来是 jdbc 连接对象实例太多, 因为 MySQL 的 jdbc 包在每次创建 jdbc 连接的时候, 会对连接进行虚引用包装, 最后放到一个 ConcurrentHashMap 里. 而系统 1 分钟会进行年轻代的 GC, 而程序配置的链接生命周期为 30 分钟. 所以 30 分钟 MinGC 将到达 30 多次, 而 JVM 默认的配置是 15. 所以我们推测这个配置的不对, 要么减少生命周期要么增加 JVM 年龄的配置.
经过测试在 jdk8 的环境下, 年龄的阈值是不能超过 15 的, 这就尴尬了. 于是只好降低生命周期, 同时设置数据源的最大连接和最小连接相等, 防止数据源一直创建空闲连接, 这样能方式连接数量, 同时通过反射, 检测 ConcurrentHashMap 的数量. 经过上线后, 发现并没有明显效果. 于是乎干脆把 map 的数据直接清空, 发现清空了上完线也并没有解决老年代上升的问题. 不过老年代每次 fullGC 都会回收到底. 于是判断清空有助于进行回收, 并且老年代回收的耗时也变短了, 减少到一半时间.
另一方面观测, 检查了 JVM 年轻代 E 区和 S 区的比例, 你会认为默认是 1:8, 但是不是那么简单地. 发现程序刚启动时确实是 1 比 8, 但是随着程序启动, MinGC,jvm 默认的垃圾回收器会自动调节这个比例. 只有设置 CMS 才不会自动调节. 于是决策是由于 S 区分配内存太小导致, 当时我们的 S 区稳定在 30M 左右, E 区差不多有 1.3G. 于是改成了 CMS 垃圾回收器, 发现也并没有卵用, 而且老年代内存到达 50% 就发生了 fullGC, 看来 CMS 是可以设置回收阈值的. 实在没办法了, 只好换数据源了, 把 HikairCP 换成了 druid. 发现换了之后就解决了, fullGC 频率从 4 天能到达 15 天, ConcurrentMap 的数量很少, 最多也就 2 百的量. 而之前能达到上万的量. 总之不知道是因为 HikairCP 的原因, 还是我们配置的原因. 据说这个数据源号称比 druid 速度更优的数据源.
.
.
.
历史是何其的相似, 程序性能问题又来了, 我们程序为了提高 tps, 经过压测, 发现瓶颈在于 Redis, 当时 Redis 是哨兵模式, 读写访问一个 master, 于是想切换为集群. 发现切换为集群后, fullGC 的频率又回到了原先的地步, 而且把操作切换为读集群时, 压力更大, 竟然再短短 5 小时内, 打满老年代内存, 并且 CPU 占用也较大. 没办法观测了一会, 发现每 5 分钟上升 2 个百分点. 代码回滚了. 于是乎又开始了一波分析, 代码优化.
经过分析本次上线只是添加了 cluster 管道批量操作的代码, 代码肯定出现在这里, 于是开始进行优化.
首先分析线上内存快照, 发现最大占用的是 int 数组, 这个不好分析, 应为属于基本对象, 没法分析. 于是乎私底下在本地进行读集群方法的压测, 再加上代码的分析, 发现每次操作都会对取 cluster 集群的信息, 这个集群的信息包含了所有节点的 ip+port, 以及他们对应的分槽信息, 而这个分槽信息是以 LinkedHashSet 类型存储的, 里面是满满的 Integer 类型的槽. 一共 16384 个, 于是基本确定了, 是每次实时查询 cluster 集群信息导致的. 发现 Jedis 的方法是对集群信息做了缓存的 , 但是只缓存了 100 毫秒. 再加上我们本身程序也包含了大量的业务逻辑. 估计这么一连起来就造成了 YGC 次数过高, 或者年轻代 S 区空间太小. 于是从两个方面去提高程序性能.
发现在修改 JVM 参数配比, 提高年轻代 S 区的内存大小, 并且修改垃圾回收器为 CMS, 发现在本机进行 ab 压测, 最高 tps 到达了 800. 并且在并发 80, 访问量 50000 的情况下, 老年代内存会上升到 100M,YGC 次数能够到达 200. 而哨兵的压测结果 tps 最高到达 2000. 而且老年代内存 50M.YGC 次数也在 30 以内.
于是对 cluster 的代码进行优化, 把每次获取集群节点信息放到了 for 循环里. tps 提高到 1300, 老年代内存稳定在 90M.
我们程序另一条思路是: 把集群的信息缓存到 JVM, 不依靠 Jedis 的缓存 100ms. 而是由我们自己缓存. 但是存在一个问题, 每当集群节点扩容, 分槽或者主下线, 从上线. 我们的程序就检测不到变化了, 于是在程序里开启了异步线程每个 5 秒刷新集群信息, 而且对于每次操作 cluster 做了重试处理, 一旦检测到节点连接创建失败, 发生 MOVED 错误, 就刷新集群信息, 进行操作重试. 发现使用静态缓存集群信息的方式, tps 提高到 2000 左右, 基本和哨兵持平, 而且老年代内存也到达 60M 左右, YGC 次数也降下来了.
在这些过程中, 我学到了什么:
自己尝试搭建了虚拟机的 Redis-cluster 集群 (很早就搭建了, 这回正好用上),Redis 集群分槽.
cluster 管道批量操作, jdk 的一些常用工具命令, 还有 ab 的简单压测
最后重要的是分析问题的能力和思路
来源: https://www.cnblogs.com/lllove/p/10574810.html