生产问题(三)排查OOM

5 篇文章 0 订阅

一、引言

        同事的服务OOM重启了,一般组内技术工具和问题研究都是由作者去做的,这次当然也不例外。顺带着把之前经历过的内存溢出、内存泄漏也讲一下。

         这次和之前的k8s内存溢出记一次特殊的K8S内存溢出_tingmailang的博客-CSDN博客_java k8s 内存溢出不同,监控上很明显是jvm的内存溢出。

二、死循环OOM

        找运维把dump文件导了一下,在JDK的bin目录下有java官方提供的小工具jvisualvm.exe,双击打开。

31180f982ba54dcfa04438613a7bda3d.png

        点击【文件】->【装入】,选择dump文件

b762383c09184f10b91e0218d1c4b194.png

        点击【类】查看堆栈

cc9c6d99896642068e3d0d9a979c6a59.png

         很明显这个类占比太大,点击查看详细信息

c3ec21cd2d994edeafe620bd47b927ca.png

         九十几万的实例对象,很明显是这个类代码写了死循环,让同事排查他的代码然后改掉了

三、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延伸知识都需要有思考,不然这些示例是不会有切身体会的。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用jmap工具可以对Java应用程序进行内存分析,帮助排查OOM问题。以下是使用jmap工具进行OOM排查的步骤: 1. 确认OOM错误:通过查看应用程序的日志或系统日志,确认是否发生了OOM错误。 2. 获取Java进程ID:使用`jps`命令获取Java进程的ID,例如:`jps -l`。 3. 使用jmap生成堆转储文件:运行以下命令生成堆转储文件(heap dump file): ``` jmap -dump:format=b,file=heapdump.bin <pid> ``` 其中,`<pid>`是Java进程的ID。 4. 分析堆转储文件:使用MAT(Memory Analyzer Tool)等内存分析工具来打开生成的堆转储文件。MAT是一款常用的Java堆内存分析工具,可以下载安装并打开生成的堆转储文件。 5. 查找内存泄漏:在MAT中,可以通过执行一系列分析操作来查找可能的内存泄漏。其中一种常用的分析是通过查看堆转储文件中的对象引用关系来定位可能导致内存泄漏的对象。 6. 分析对象占用内存情况:在MAT中,可以查看对象占用内存的情况,包括对象数量、大小和引用关系等信息。这有助于确定哪些对象占用了大量内存,可能导致OOM。 7. 优化代码和资源释放:根据分析结果,优化代码以避免内存泄漏或者减少内存占用。确保及时释放不再需要的对象和资源,使用合适的数据结构等。 请注意,使用jmap生成堆转储文件会在运行期间暂停Java进程一段时间,请在生产环境中谨慎使用。此外,内存分析工具的使用需要一定的经验和技巧,对于复杂的问题,建议咨询专业的开发人员来进行排查和解决。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胖当当技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值