作者:Lxlxxx
鏈接:https://juejin.cn/post/7221461552343072828
前言
繼上次線上CPU出現了報警,這次服務又開始整活了,風平浪靜了沒幾天,看生產日誌服務的運行的時候,頻繁的出現OutOfMemoryError,就是我們俗稱的OOM,這可還行!
頻繁的OOM直接會造成服務處於一個不可用的情況,通過Skywalking查看鏈路調用,基本全報紅了,基本處於一個癱瘓狀態,因爲生產該服務是分佈式部署,運維當即立斷對該服務進行重啓,因爲是B端的產品,先讓公司業務能用起來了,保證服務的正常使用,然後緊急查看問題,當然這個問題就來到了我這裏,既然分配給我了,咱高低給它查出來,並且修復了。
OutOfMemoryError出現的原因
先來了解下OutOfMemoryError出現的原因,無非就是兩類堆內存空間不足、元空間不足
- 堆內存空間不足:意味着程序存在一直有引用的對象(強引用),主要對象在引用的狀態就無法被GC回收,撐爆了-Xmx堆拓展的最大值,內存不足自然就會觸發堆內存溢出。
- 元空間:Java 8引入了元空間概念,代替了之前堆的永久代,由於元空間屬於堆外內存,不需要有對象引用,通過指針的方式表示類和元數據,之所以引用元空間就是一種JDK的升級優化,避免了永久代的內存溢出。
常見堆內存溢出的幾種情況
- 查詢數據庫返回的數據量過大,加載到內存中導致內存溢出;
- 代碼中出現死循環情況,導致大對象一直被引用不能被GC回收;
- 資源鏈接池、io流在使用完沒有進行手動釋放;
- 靜態集合類裏面存在引用對象,始終存在引用關係,沒有進行清除;
以上屬於常見的幾種堆內存溢出的場景,當然有時候我們的遇到的問題都是稀奇古怪的問題,常見的問題總是很少能遇到…
推薦一個開源免費的 Spring Boot 實戰項目:
現象分析
根據生產環境的報錯日誌來看,這邊屬於Mybatis報出的一個內存溢出情況,通過去看Mybatis源碼發現,底層也是通過一些集合類來存放拼接的sql,那麼當然也有可能出現堆內存溢出,而且在sql體積比較大的情況下,接收sql的集合就會變的非常大,如果回收不了那麼就會導致內存溢出。
由於我們docker容器裏面沒有一些jstack、jmap的工具,並且dump文件也沒有進行保存…導致我無法通過看線程高佔用內存的對象,來分析具體是什麼操作發生的內存溢出,這就難了… 於是只能去網上搜搜看了,沒想到真的給到我一些啓發,並且有點思路大概知道是哪裏的問題。
文章來源於zzzzbw作者寫一篇關於 慘遭DruidDataSource和Mybatis暗算,導致OOM ,很感謝🙏這位作者給我帶來的啓發。 文章作者也遇到了Mybatis帶來的OOM,主要是因爲Mybatis拼接SQL的時候生成的佔位符和參數對象,存放在Map裏,當SQL的參數多導致SQL太長的時候,Map持有這些SQL時間較長,並且多線程同時操作,這時候內存佔用就很高,從而發生OOM
Mybatis源碼分析
通過對DynamicContext類源碼查看,DynamicContext又一個ContextMap 類型的參數bindings,繼承了HashMap相當於一個Map集合,接着看這個類中的getBindings方法,看到了ForEachSqlNode這類調用了getBindings方法,簡單的說就是ForEachSqlNode通過getBindings方法,將SQL參數和參數的佔位符統一put到ContextMap這個集合裏面,主要是這裏面的參數和佔位符無法被GC回收,併發查詢量多的情況下就會導致OOM。
情景復現
隨後我做了線上場景的復現,通過將SQL語句的拼接,將IN裏面的參數變大,然後創建50個線程進行執行,將JVM堆內存設爲-Xmx256m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
這裏看控制檯打印的日誌,服務在頻繁的進行Full GC,導致OOM。
總結
既然發現了問題出現的原因,接下來就是對代碼SQL進行優化,儘量避免在sql拼接的時候體積過大,這裏告誡我們代碼不能亂寫,SQL語句也不能隨意寫啊,有時候把問題想的過於簡單確實會帶來不可預知的風險。
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這纔是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!