//获取 java进程的 pid
jps -l
虚拟机统计信息监视工具
//每2秒 获取一次 gc 信息
jstat -gcutil pid 2000
//查看存活对象情况 导出日志信息
//查看内存信息 实例个数 以及 占用内存大小
jmap -histo:live pid > ./livelog.txt
//查看年轻代, 老年代 内存配置 和 使用情况
//查看内存堆栈信息
//8.0以前
jmap -heap pid > ./log.txt
//8.0以后
jhsdb jmap --heap --pid pid > ./heaplog.txt
//打印堆栈信息
jstack -l pid > ./stacklog.txt
最近用户量激增 后台说我这个推送服务 占用CPU和内存激增
用 top 指令查看 显示我的java进程 占用200%的CPU和30G的虚拟内存
虽然我是Android开发 但是同时负责推送服务器的维护,仅仅是维护 平时就加些功能 新接入加厂商推送之类 并没有悉知的读过代码
这次异常 不得不让我从头过一遍源码
重头过一遍源码后 每次下发厂商推送消息都会从数据库查一下该用户绑定的厂商推送信息
如果每次推送都查询数据库 肯定是不行的 于是加了个缓存
用 ConcurrentHashMap 做了个缓存队列 将用户id和厂商推送信息绑定
当要下发推送时先找缓存队列里有没有 有就直接从缓存里拿 没有再查 查到之后加到缓存队列里
按照这个思路修改后 重新部署到服务器
CPU降下来了 从 200% 降到了 30%-40%左右
这个肯定还有待优化的地方 但是目前问题已经解决
接下来就是内存占用过高的
经过实时监控发现 刚启动程序虚拟内存只占用了 900M
紧接着已肉眼可见的速度增长 当涨到40G也就是分配的磁盘空间时 就产生OOM
为什么会 内存溢出 因为系统已经没有内存分配给这个程序
也就是说 程序里什么线程或者对象没有释放掉 导致一直占用内存 也就是内存泄漏 最终导致内存溢出
想到这里 我先用
jps -l
查看程序的进程的 pid
然后 用
jmap -histo:live pid > ./livelog.txt
打印了 当前进程下 内存的实际占用情况
发现并没有什么异常 整个进程数据占用的内存只有400M左右
接下来 使用
jstack -l pid > ./stacklog.txt
打印堆栈信息
不看不知道 一看吓一跳
好家伙 3000个苹果推送的线程 锁死 和 4000个google推送的线程 锁死
并且还在不断上涨 也就是说 每发一条google推送 或 苹果推送 就会生成一个线程来执行这个任务
经过进一步观察 苹果的还好 APNS的库 用的okhttp3 我将 OkHttpClient 单独提出来 所有请求公用一个 OkHttpClient 避免的不停的创建
也就是说 之前每发一条苹果推送 就会创建一个 OkHttpClient 而默认超时是60秒 如果网络波动或者发送异常 就会锁死60秒 等 OkHttpClient 超时后 才会释放掉这个 线程 而我们每秒有几百条请求 所以导致不断积累
不过这些比起来google推送的几万条就是小巫见大巫了
苹果推送的解决了 接下来是 google推送的
和苹果推送同样的策略 但是我发现google推送是已经分装好的
FirebaseMessaging messaging = FirebaseMessaging.getInstance(app);
messaging.send(msgObj);
使用 FirebaseMessaging 直接 send 要下发的消息即可 所以无从下手
再梳理一番发现 由于我们国内的阿里云无法访问国际互联网 导致
google推送鉴权失败 也就是说 每下发一条 google推送 如果没有鉴权 会先去鉴权 由于无法访问google导致鉴权耗时60秒 在这期间 线程会锁死 这还没玩 当google的库发现鉴权超时 会直接报错 但是不知道为什么 FirebaseMessaging无法释放 导致 线程一直锁死
也就是说 google的推送会一直占用 并不会像苹果的推送一样 超时会释放
怎么解决呢 我发现google的库版本太低 是不是库本身的bug 我决定将库提升到最新版
升级了 google 的库 从6.0.0 升级到 目前9.1.1的版本
com.google.firebase
firebase-admin
9.1.1
重新测试 发现果然是库的问题
最新版的库 也就是60秒超时后 可以正常释放 也就取消了线程的占用
但是还是会锁死60秒 这个不能忍
索性 国内禁用掉google推送 反正也收不到 开启没啥P用啊
直接在推送服务器启动前 先判断 能否访问 google 的 80端口
如果能访问 说明在国外 正常初始化 google推送的sdk
如果不能访问 说明在国内 则禁用google推送
public static boolean isOnline() {
Socket server;
try {
server = new Socket();
InetSocketAddress address = new InetSocketAddress("www.google.com", 80);
server.connect(address, 3000);
System.out.println("google 可用!");
return true;
} catch (UnknownHostException e) {
System.out.println("google 解析异常!");
e.printStackTrace();
} catch (IOException e) {
System.out.println("google 连接超时!");
e.printStackTrace();
}
return false;
}
重新打包 上线测试
这波真万事大吉 虚拟内存从20G降到9000M
经过一天的测试 发现没有虚拟内存一直保持在正常范围内 没有暴涨 CPU也正常 打印堆栈信息 也没发现线程锁死
至此 该事故已完美解决
当然 该系统还有待优化的地方 但这都是后话了。
对于一个做Android的 初入后端 这次事故排查让我学到了很多
这次从 线程池 内存泄漏 内存溢出 高并发 等 收获颇丰
我这篇文章 仅仅是展示解决思路 公司代码也不能贴出 所以多多包涵
大家如果遇到类似的问题 可以先看 我上面贴出的参考资料
这些资料帮了我很多(虽然在大量的复制粘贴文章里感觉就像是 屎里淘金)
可以先看看这些参考文章 再看看我的解题思路 应该有所帮助!
当你使用 jmap -heap pid 出现如下报错
Type "GenericGrowableArray", referenced in VMStructs::localHotSpotVMStructs in the remote VM, was not present in th
e remote VMStructs::localHotSpotVMTypes table (should have been caught in the debug build of that VM). Can not continue.
这是由于你的默认JAVA_HOME和程序使用Java的版本不一致导致的
可以使用绝对路径 来访问
C:\Users\itloser\.jdks\openjdk-17.0.1\bin\jmap -heap 6672
接下来你可能会遇到
-heap option used
Cannot connect to core dump or remote debug server. Use jhsdb jmap instead
这表示你使用的Java版本是8.0以后的需要使用
C:\Users\itloser\.jdks\openjdk-17.0.1\bin\jhsdb jmap --heap --pid 6672