生产问题(三)排查OOM
同事的服务OOM重启了,一般组内技术工具和问题研究都是由作者去做的,这次当然也不例外。顺带着把之前经历过的内存溢出、内存泄漏也讲一下。这次和之前的k8s内存溢出记一次特殊的K8S内存溢出_tingmailang的博客-CSDN博客_java k8s 内存溢出不同,监控上很明显是jvm的内存溢出。
一、引言
同事的服务OOM重启了,一般组内技术工具和问题研究都是由作者去做的,这次当然也不例外。顺带着把之前经历过的内存溢出、内存泄漏也讲一下。
这次和之前的k8s内存溢出记一次特殊的K8S内存溢出_tingmailang的博客-CSDN博客_java k8s 内存溢出不同,监控上很明显是jvm的内存溢出。
二、死循环OOM
找运维把dump文件导了一下,在JDK的bin目录下有java官方提供的小工具jvisualvm.exe,双击打开。
点击【文件】->【装入】,选择dump文件
点击【类】查看堆栈
很明显这个类占比太大,点击查看详细信息
九十几万的实例对象,很明显是这个类代码写了死循环,让同事排查他的代码然后改掉了
三、excel大容量oom
这里是门店组那边有个bd把一个excel导入门店系统,解析之后有100多万对象生成,然后把系统冲爆了。
有同学会问,怎么会有系统不检查excel数据量就解析呢?实际上他查了,但是没查全,100多万行都是空数据,也不知道这个bd怎么搞出来的,然后门店系统没有检查是否空行就解析了,虽然行里面只有一些空格没有数据,但是放到对象里面字符还是不少的。
这次事故之后,所有系统被要求严查空数据excel,其他的文件也是一样。但是那次事故搞的很大,因为系统反复重启,那个bd不成功就反复重新导入,形成了死循环。
惊动了上面的大佬,大佬让前端把打到那个机器上的请求全拉了出来,找出时间点反复执行的操作和人员,最后找到了这个bd。
这也是没什么发布,不然还在代码上死磕呢。
四、内存泄漏
oom和内存泄漏总是不分家的,基本上说内存泄漏都是引用链没有断开导致对象没有回收,躲过gc,但是实际上作者见过的情况不会导致oom,反而会导致其他情况。
比较经典的就是ThreadLocal的内存泄漏
1、ThreadLocal分析
线程缓存是java提供的绑定当前线程的存储空间,因此在任意方法进行ThreadLocal 的设置,后续只要属于当前线程的方法都可以取到这个值。
首先通过它的set方法分析
public void set(T value) {
Thread t = Thread.currentThread();
//获取当前线程为键对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//将当前ThreadLocal对象作为基础,业务值设置到ThreadLocalMap维护的数组中
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
//创建当前线程的ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//ThreadLocalMap维护了一个数组,这是因为线程的缓存值可能有多个,许多第三方框架都在使用
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//通过hasn值与数组长度取&获取下标
int i = key.threadLocalHashCode & (len-1);
//将业务值设置到数组中
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
接着通过get方法分析
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程为键对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//将当前ThreaadLocal对象传入getEntry方法,获取线程缓存的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
//根据对象的hash值与存储数组进行&,获取存储值的下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
用完记得remove!由于ThreadLocalMap的Entry是虚引用,线程如果不销毁不会被回收,用完就进行remove会避免内存泄漏风险。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//将虚引用置为null
e.clear();
//清理数组
expungeStaleEntry(i);
return;
}
}
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将该下标对象置为null,便于gc回收
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
//遍历该下标之后所有不为空的ThreadLocal对象
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//如果该ThreadLocal实例的虚引用已经被销毁,将该位置的ThreadLocal置为null
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
2、内存泄漏
的确是有同事没有remove,导致了ThreadLocalMap 中的 Entry 对象将无法被回收,从而导致与之关联的值也无法被回收,造成内存泄漏。
但是他会造成什么后果呢,如果你说会oom那就完了,因为现在的java服务,Tomcat 在处理前端请求时使用了线程池来管理并发请求。Tomcat 的线程池会预先创建一定数量的线程,用于处理请求,避免每个请求都创建新的线程带来的开销。
而在 Spring Boot 中,Controller 层也可以使用线程池来处理请求。当请求到达 Controller 层时,Spring Boot 可以通过配置线程池来处理请求,从而实现并发处理。
需要注意的是,Tomcat 的线程池和 Spring Boot 的 Controller 层线程池是两个独立的概念,分别用于处理不同层面的请求。Tomcat 的线程池主要用于处理前端请求进入 Tomcat 的过程,而 Spring Boot 的 Controller 层线程池主要用于处理业务逻辑的并发请求。
而ThreadLocal如果泄漏了会收到线程级别的覆盖,正常占不了多少内存。但是另外一个后果就出来了,ThreadLocal里面存储的值是之前请求遗留下来的,导致一些aop组件直接获取了错误的鉴权信息,影响了用户操作。
如果能用不一样的角度和示例说出自己的感悟,去更好的分析解决问题,那当然是加分的。
五、总结
对于代码问题导致的OOM其实是最好查的,堆栈信息一看就知道问题出在哪了,只不过写代码的人一叶障目,其他人又不可能在代码review的时候看的那么细。
其他类型的oom相对就不是那么好查了,相关的监控、jvm、数据解析、代码规范、tomcat、spring延伸知识都需要有思考,不然这些示例是不会有切身体会的。
更多推荐
所有评论(0)