1. 程式人生 > >誤用Freemarker標籤和SpringJDBC預編譯功能導致的記憶體洩露問題分析

誤用Freemarker標籤和SpringJDBC預編譯功能導致的記憶體洩露問題分析

一. 問題描述

        本人所在的專案組專案已經執行快一年了,功能效能都比較穩定。但是最近釋出了一個版本,只是業務上增加了一些資料量,最終效果卻是在持續執行的過程中,出現OOM異常。之前也發過一些版本,做過一些類似的調整,都沒有出現效能問題,而這個版本卻出現了,著實手忙腳亂了一陣子。

二. 專案背景

         專案是一個為前臺提供服務的後臺應用。出問題的功能主要做的事情是:通過各個表的基礎資料,進行計算,得到一個結果資料,併入庫。而且明確是在執行這個功能的時候出的問題。我們在上線之前,也做過效能測試,持續跑了很久也沒有出現這樣的問題。

        所用的技術是Spring(包括jdbc,都是3.0以上的版本)。        資料庫用的MySQL,資料庫操作使用類似mybatis的方式:結合了FreeMarker和sql標籤。

        資料入庫的方式,一般都是使用單條insert語句sql標籤,再通過batchUpdate方法,在同一個事務中執行批量sql呼叫操作。我們為了提高結果資料的入庫效率,使用insert into tablename (colum1,…,columN)values (…),(…),…,(…) 這種方式,將資料合併到一條sql中入庫。但是mybatis的:param(冒號加引數名是SpringJDBC接受的一種傳參方式)不支援傳遞 (…),(…),…,(…) 這樣的引數(因為SpringJDBC會將:param解析成?佔位符,然後insert into tablename (colum1,…,columN)values ? 這個語句不符合PreparedStatement的語法,報異常)。於是我們採用${param}方式,在FreeMarker層解析,直接拼接出包括具體值的完整的insert語句。

        作業系統Linux,JDK1.6版本,應用伺服器用的是IBM的websphere叢集。

三. 問題定位

        在一年前釋出初始版本前,我們也曾出現過OOM異常。當時的解決方案,一方面調大JVM啟動記憶體和最大記憶體,另一方面,在運算過程中用來儲存計算結果的List、Map之類的“大集合”,手動clear操作,減輕GC壓力(為什麼能減輕?和GC策略有關,不細說了)。

        時隔一年,又出現這樣的問題,我們第一反應還是檢查有沒有正確處理新增的“大集合”。結果是沒問題的。

        看來想通過程式碼走讀來解決問題是行不通了,只好試著做一下記憶體分析、執行緒分析、GC分析(老實講,沒怎麼用過,現學現賣)。

        先看GC分析。用的是IBM提供的ga16.jar工具(IBM Pattern Modeling and Analysis Tool forJava Garbage Collector)。拉了一段時間的GC日誌分析了下,結果如下圖:


        圖1

        從功能開始執行,雖然有GC,但是消耗的速度大於GC的程度。這樣看來,即使分配的記憶體再調大一些,也許能解決當下的問題,但是將來如果結果資料繼續增大,可能還會出現同樣的問題,治標不治本。

        再看執行緒分析的結果。使用工具jca452.jar(IBM Thread and Monitor Dump Analyzer for Java)分析javaCore檔案:


圖2

圖3

       看起來資料庫操作那塊有問題的。我們排查了資料庫當時的執行情況,是非常良好的:系統負載正常,連線數正常。因此,初步確定是服務端操作資料庫那塊有問題。

最後我們再分析當時的記憶體dump檔案。工具是ha426.jar(IBM HeapAnalyzer)。通過此工具的“洩露嫌疑”排序功能發現,消耗記憶體較多的是org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate這個類裡面的某個LinkedHashMap物件佔用記憶體較大。我們看看NamedParameterJdbcTemplate原始碼,把相關程式碼段摳出來:

<span style="font-size:18px;">public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations {

...

	/** Cache of original SQL String to ParsedSql representation */
	private final Map<String, ParsedSql> parsedSqlCache =
			new LinkedHashMap<String, ParsedSql>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
				@Override
				protected boolean removeEldestEntry(Map.Entry<String, ParsedSql> eldest) {
					return size() > getCacheLimit();
				}
			};

...

	/**
	 * Obtain a parsed representation of the given SQL statement.
	 * <p>The default implementation uses an LRU cache with an upper limit
	 * of 256 entries.
	 * @param sql the original SQL
	 * @return a representation of the parsed SQL statement
	 */
	protected ParsedSql getParsedSql(String sql) {
		if (getCacheLimit() <= 0) {
			return NamedParameterUtils.parseSqlStatement(sql);
		}
		synchronized (this.parsedSqlCache) {
			ParsedSql parsedSql = this.parsedSqlCache.get(sql);
			if (parsedSql == null) {
				parsedSql = NamedParameterUtils.parseSqlStatement(sql);
				this.parsedSqlCache.put(sql, parsedSql);
			}
			return parsedSql;
		}
	}

}</span>

        此類實現一個介面,成員屬性只有parsedSqlCache是LinkedHashMap型別的。通過屬性的註釋,以及和它直接相關的方法簽名看出,parsedSqlCache是用來快取預編譯的sql的。

        那麼為什麼這個parsedSqlCache會很大呢?這和我們處理SQL標籤的順序有關。我們會在呼叫getParsedSql方法之前,對SQL標籤進行FreeMarker解析,將${param}替換掉。這樣,getParsedSql方法的sql入參就變成insert into tablename (colum1,…,columN) values (…),(…),…,(…)這樣的一個超長SQL了。要知道,我們結果表字段很多,而且每個欄位也都很大,這樣組成的超長記錄數SQL,一條SQL就能達到幾十兆。因為每個insert拼接的條目記錄不一樣,每一條SQL都是一條新的,在parsedSqlCache沒有值,因此每來一條SQL都會put到parsedSqlCache中,parsedSqlCache的size預設為256。這樣算來,記憶體消耗是很大的,而且這些SQL的控制代碼被parsedSqlCache持有,是強引用,因此GC也回收不了。

        癥結找到了,就好辦了。要麼調整FreeMarker的解析順序,要麼調整SQL標籤的使用方式。考慮到FreeMarker解析那塊是共方法,修改的話影響很大,最終我們選擇使用:param方式傳參,構建單條insert固定句式,然後在一個事務中批量執行不同記錄的insert操作。問題得以解決。

        除了以上兩種方法以外,還可以通過調整NamedParameterJdbcTemplate類屬性的方式,不使用SQL快取措施。仔細看下getParsedSql方法的第一行:if (getCacheLimit() <= 0) 。這個判斷,如果parsedSqlCache的size設定為空,則直接解析入參SQL,不做快取處理。但是這樣的話,常規SQL處理也沒有預處理快取了,會降低SQL執行效率。看怎麼取捨吧!

        以上是這次記憶體洩露問題的一個小結,希望對大家有所啟發。

相關推薦

誤用Freemarker標籤SpringJDBC編譯功能導致記憶體洩露問題分析

一. 問題描述         本人所在的專案組專案已經執行快一年了,功能效能都比較穩定。但是最近釋出了一個版本,只是業務上增加了一些資料量,最終效果卻是在持續執行的過程中,出現OOM異常。之前也發過一些版本,做過一些類似的調整,都沒有出現效能問題,而這個版本卻出現了,著實

MySQL的編譯功能

.cn 支持 right amp print div 服務 nec rom 1、預編譯的好處   大家平時都使用過JDBC中的PreparedStatement接口,它有預編譯功能。什麽是預編譯功能呢?它有什麽好處呢?   當客戶發送一條SQL語句給服務器後,服務器總是

MySQL的編譯功能簡述

預編譯的好處 大家平時都使用過JDBC中的PreparedStatement介面,它有預編譯功能。什麼是預編譯功能呢?它有什麼好處呢? 當客戶傳送一條SQL語句給伺服器後,伺服器總是需要校驗SQL語句的語法格式是否正確,然後把SQL語句編譯成可執行的函式,最後才是執行SQL

使用PreparedStatement 防止SQL攻擊 實現編譯功能提高效能

public boolean login(String username,String password) throws Exception { String driverClassName="com.mysql.jdbc.Driver"; S

C語言中的三大編譯功能

這三種預處理包括:巨集定義、檔案包含、條件編譯。 巨集定義是C語言提供的三種預處理功能的其中一種。巨集定義和操作符的區別是:巨集定義是替換,不做計算,也不做表示式求解。 巨集定義又稱為巨集代換、巨集替換,簡稱“巨集”。 格式: 其中的識別符號就是所謂的符號常量,也

由列舉引起的對編譯巨集編譯的理解思考

話不多說,先上一段神奇的程式碼! #include <stdio.h> #include <stdlib.h> enum t { a, b, }; int main() { printf("b = %d\n"

補間動畫屬性動畫記憶體洩露分析

在使用屬性動畫的時候,我們知道如果不在頁面結束的時候釋放掉動畫,就會引起記憶體洩露。 簡單的說就是ValueAnimator在AnimationHandler註冊自己的AnimationFrameCallback,AnimationFrameCallback介面

Android記憶體洩露檢測工具實際開發中遇到的記憶體洩露問題解析

介紹 記憶體洩露是平常開發中經常遇到的,有些時候稍不注意就會發生,而且還不易察覺,這就需要工具來幫助檢測。本文主要介紹記憶體檢測工具和我在開發中遇到的記憶體洩露問題和解決方案。 記憶體洩露的原理 具體的原理涉及到虛擬機器垃圾回收機制知識,這裡只為下文作

JAVA記憶體洩露分析解決方案及WINDOWS自帶檢視工具

Java記憶體洩漏是每個Java程式設計師都會遇到的問題,程式在本地執行一切正常,可是佈署到遠端就會出現記憶體無限制的增長,最後系統癱瘓,那麼如何最快最好的檢測程式的穩定性,防止系統崩盤,作者用自已的親身經歷與各位分享解決這些問題的辦法.作為Internet最流行的程式語言之一,Java現正非常流行.我們的網

Java中隱藏的this變數區域性變數可能引發的記憶體洩露問題

背景 眾所周知,在Java中,成員方法內可以使用this來引用當前物件,使用起來特別方便。但是在JVM中方法是在方法區中,所有的類的物件都共用了一個方法區,那麼JVM是怎麼知道this是指向哪個物件的呢? 其實為了實現這一功能,Java的處理方式很簡單,在編

關於valgrind的安裝記憶體洩露分析

程式的安裝 如果使用的是tar包安裝. valgrind# wget http://valgrind.org/downloads/valgrind-3.9.0.tar.bz2# tar -jxvf valgrind-3.9.0.tar.bz2# cd valgrind-3.

酷播V4更新了,支持PC端移動端的視頻功能(收費覽視頻功能

href img 收費 oss www. bsp com 蘋果 免費 感覺要變天了,灰蒙蒙的。好久沒有下雨了... [酷播V4]永久免費的酷播V4,更新了html5和flash播放器的優先級選項,效果: 效果演示:http://www.cuplayer.com/CuP

Part5 數據的共享與保護 5.6多文件結構編譯命令

靜態數據成員 其它 pac object c void 使用 內聯 組合 getx C++程序的一般組織結構:   1 一個工程可以劃分為多個源文件:     類聲明文件(.h文件)     類實現文件(.cpp文件)     類的使用文件(main()所在的.cpp

【StatementPreparedStatement有什麽區別?哪個性能更好?編譯語句,防止sql註入問題】

dstat () 驅動程序 對象 生成 from result 查詢語句 驅動 答:與Statement相比,①PreparedStatement接口代表預編譯的語句,它主要的優勢在於可以減少SQL的編譯錯誤並增加SQL的安全性(減少SQL註射攻擊的可能性);②Prepar

mysql sp 練習遊標編譯

prepare book from oop hand locate exist ont alloc create procedure Jack_count_cur_dual() BEGIN DECLARE tb_name VARCHAR(50); DECLARE done

Freemarker輸出$html標籤等特殊符號

場景:程式設計師都不喜歡看文件,而更喜歡抄例子。所以,我們把平臺組的元件都做成例子供別人參考。我們前端展示層使用的是freemarker,所以遇到這個問題,比如我們要讓前端顯示freemarker自己的原始碼時就有問題了(因為我們例子程式的頁面也是使用freemarker)。遇到的問題如下:  

js中的編譯作用域鏈

JavaScript執行三部曲 指令碼執行js引擎都做了什麼呢? 語法分析 預編譯 解釋執行 1.語法分析分析語法是不是錯了 2,在語句執行的時候會進行預編譯 3.在編譯完了進行語句執行 下面就是編譯的主要步驟 三。預編譯的過程(分四步): 1.

JavaScript 詳解編譯原理(其他語言很不一樣)

JavaScript 預編譯原理 今天用了大量時間複習了作用域、預編譯等等知識 看了很多博文,翻開了以前看過的書(好像好多書都不會講預編譯) 發現當初覺得自己學的很明白,其實還是存在一些思維誤區 (很多博文具有誤導性) 今晚就整理了一下凌亂的思路 先整理一下預編譯的知識吧,日後有時間再把作用

C語言檔案操作 編譯命令

//read檔案 int main(){ char *path = "D:\\friends.txt"; FILE *fp = fopen(path, "r"); char buff[500]; while (fgets(buff,50,fp)){ printf("%s\n", b

小專案使用less編譯css的常用東西步驟總結

Less使用 http://lesscss.cn/(Less 中文網) 下載編譯工具(koala) 安裝 開啟後如圖   初始化設定 點選設定進行語言設定,關閉重啟 點選加號進行程式碼專案匯入,如上圖所示 右擊此專案設定輸出路徑,雙擊此專案進行less設定   Les