第28章 享元模式
28.1 內存溢出,司空見慣
下午,我正在開會中,老大推門進來。
“三兒,出來一下。”
我剛出會議室門口,老大就發話了。
“郎當(姓朗,順口就叫郎當)的那個報考系統又crash了一臺機器,兩天已經宕了4次了,你這邊還有緊急的事情沒有?……沒有,那趕快過去頂一下,就運行三天的程序,兩天宕了4次,還怎么玩?!”
我馬上收拾東西,沖到馬路上攔了出租車,同時打電話給郎當。
“三哥,廠商人員已經定位出了,OutOfMemory內存溢出,沒查到有內存泄漏的情況,現在還在跟蹤……是突然暴漲的,都是在繁忙期出現問題的……”
內存溢出對Java應用來說實在是太平常了,有以下兩種可能。
● 內存泄漏
無意識的代碼缺陷,導致內存泄漏,JVM不能獲得連續的內存空間。
● 對象太多
代碼寫得很爛,產生的對象太多,內存被耗盡。現在的情況是沒有內存泄漏,那只有一種原因——代碼太差把內存耗盡。
到現場后,郎當給我介紹了一下系統情況。該系統是一個報考系統,其中有一個模塊負責社會人員報名,該模塊對全國的考試人員只開放3天,并且限制報考人員數量。第一天9點開始報考,系統慢得像蝸牛,基本上都不能訪問,后來設置了HTTP Server的并發數量,稍有緩解,40分鐘后宕了一臺機器,10分鐘后,又掛了一臺,下午3點又掛了一臺,看樣子晚上要讓郎當去寺廟燒燒香了。
該系統一共有8臺應用服務器,基本上CPU繁忙程度都在60%以上,HTTP的最大并發是2000,平均分配到每臺應用服務器上沒有太大的壓力,于是懷疑是代碼問題,然后詳細了解了一下業務和數據流邏輯,基本的業務操作過程清楚了,先登錄(沒有賬號的,則要先注冊),登錄后,需要填寫以下信息:
● 考試科目,選擇框。
● 考試地點,選擇框,根據科目不同,列表不同。
● 準考證郵寄地址,輸入框。
還有其他一堆信息,我們以這三者作為代表來講解。信息填寫完畢后,點擊確認,報名就結束了。簡單程序的業務邏輯也確實是這樣,為什么出現Crash情況呢?那肯定是和壓力有關系!
我們先把這個過程的靜態類圖畫出來,如圖28-1所示。

圖28-1 報考系統類圖
很簡單的工廠方法模式,表現層通過工廠方法模式創建對象,然后傳遞給業務層和持久層,最終保存到數據庫中,為什么要使用工廠方法模式而不用直接new一個對象呢?因為是在框架下編程,必須有一個對象工廠(ObjectFactory,Spring也有對象工廠)。我們先來看報考信息,如代碼清單28-1所示。
代碼清單28-1 報考信息
public?class?SignInfo?{
?????//報名人員的ID
?????private?String?id;
?????//考試地點
?????private?String?location;
?????//考試科目
?????private?String?subject;
?????//郵寄地址
?????private?String?postAddress;
?????public?String?getId()?{
?????????????return?id;
?????}
?????public?void?setId(String?id)?{
?????????????this.id?=?id;
?????}
?????public?String?getLocation()?{
?????????????return?location;
?????}
?????public?void?setLocation(String?location)?{
?????????????this.location?=?location;
?????}
?????public?String?getSubject()?{
?????????????return?subject;
?????}
?????public?void?setSubject(String?subject)?{
?????????????this.subject?=?subject;
?????}
?????public?String?getPostAddress()?{
?????????????return?postAddress;
?????}
?????public?void?setPostAddress(String?postAddress)?{
?????????????this.postAddress?=?postAddress;
?????}
}
它是一個很簡單的POJO對象(Plain Ordinary Java Object,簡單Java對象)。我們再來看工廠類,如代碼清單28-2所示。
代碼清單28-2 報考信息工廠
public?class?SignInfoFactory?{
?????//報名信息的對象工廠
?????public?static?SignInfo?getSignInfo(){
?????????????return?new?SignInfo();
?????}
}
工廠類就這么簡單?非也,這是我們的教學代碼,真實的ObjectFactory要復雜得多,主要是注入了部分Handler的管理。表現層是如何創建對象的,如代碼清單28-3所示。
代碼清單28-3 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
?????????????//從工廠中獲得一個對象
?????????????SignInfo?signInfo?=?SignInfoFactory.getSignInfo();
?????????????//進行其他業務處理
?????}
}
就這么簡單,但是簡單為什么會出現問題呢?而且這樣寫也沒有問題呀,很標準的工廠方法模式,應該不會有大問題,然后又看了看系統廠商提供的分析報告,報告中指出:內存突然由800MB飆升到1.4GB,新的對象申請不到內存空間,于是出現OutOfMemory,同時報告中還列出宕機時刻內存中的對象,其中SignInfo類的對象就有400MB,瘋子,絕對是瘋子!報告都沒有看嘛!
問題找到了,我拉郎當過來談話,“廠商不是分析出原因了嘛,人家已經指出SignInfo類的對象占用了400MB多的內存,這是怎么回事?”
“三哥,這是很正常的,這么大的訪問量,產生出這么多的SignInfo對象也是應該的,內存中有這么多對象并不表示這些對象正在被使用呀,估計很大一部分還沒有被回收而已,垃圾回收器什么時候回收內存中的對象這是不確定的。你看,并發200多個,這可是并發數量……”
我想了想,也確實是這么回事。既然已經定位是內存中對象太多,那就應該想到使用一種共享的技術減少對象數量,那怎么共享呢?
大家知道,對象池(Object Pool)的實現有很多開源工具,比如Apache的commons-pool就是一個非常不錯的池工具,我們暫時還用不到這種重量級的工具,我們自己來設計一個共享對象池,需要實現如下兩個功能。
● 容器定義
我們要定義一個池容器,在這個容器中容納哪些對象。
● 提供客戶端訪問的接口
我們要提供一個接口供客戶端訪問,池中有可用對象時,可以直接從池中獲得,否則建立一個新的對象,并放置到池中。
設計思路有了,那我們池中對象的標準是什么呢?你想想看,如果你把所有的對象都放到池中,那還有什么意義?內存早就給你撐爆了!這么多對象,必然有一些相同的屬性值,如幾十萬SignInfo對象中,考試科目就4個,考試地點也就是30多個,其他的屬性則是每個對象都不相同的,我們把對象的相同屬性提取出來,不同的屬性在系統內進行賦值處理,是不是就可以建立一個池了?話無須多說,我們以類圖來表示,如圖28-2所示。

圖28-2 增加對象池的類圖
做一個很小的改動,增加了一個子類,實現帶緩沖池的對象建立,同時在工廠類上增加了一個容器對象HashMap,保存池中的所有對象。我們先來看產品子類,如代碼清單28-4所示。
代碼清單28-4 帶對象池的報考信息
public?class?SignInfo4Pool?extends?SignInfo?{
?????//定義一個對象池提取的KEY值
?????private?String?key;
?????//構造函數獲得相同標志
?????public?SignInfo4Pool(String?_key){
?????????????this.key?=?_key;
?????}
?????public?String?getKey()?{
?????????????return?key;
?????}
?????public?void?setKey(String?key)?{
?????????????this.key?=?key;
?????}
}
很簡單,就是增加了一個key值,為什么要增加key值?為什么要使用子類,而不在SignInfo類上做修改?好,我來給你解釋為什么要這樣做,我們剛剛已經分析了所有的SignInfo對象都有一些共同的屬性:考試科目和考試地點,我們把這些共性提取出來作為所有對象的外部狀態,在這個對象池中一個具體的外部狀態只有一個對象。按照這個設計,我們定義key值的標準為:考試科目+考試地點的復合字符串作為唯一的池對象標準,也就是說在對象池中,一個key值唯一對應一個對象。
注意 在對象池中,對象一旦產生,必然有一個唯一的、可訪問的狀態標志該對象,而且池中的對象聲明周期是由池容器決定,而不是由使用者決定的。
你可能馬上就要提出了,為什么不建立一個新的類,包含subject和location兩個屬性作為外部狀態呢?嗯,這是一個辦法,但不是最好的辦法,有兩個原因:
● 修改的工作量太大,增加的這個類由誰來創建呢?同時,SignInfo類是否也要修改呢?你不可能讓兩段相同的POJO程序同時出現在同一模塊中吧!
● 性能問題,我們會在擴展模塊中講解。
說了這么多,我們還是繼續來看程序,工廠類如代碼清單28-5所示。
代碼清單28-5 帶對象池的工廠類
public?class?SignInfoFactory?{
?????//池容器
?????private?static?HashMap<String,SignInfo>?pool?=?new?HashMap<String,SignInfo>();
?????//報名信息的對象工廠
?????@Deprecated
?????public?static?SignInfo(){
??????????return?new?SignInfo();
?????}
?????//從池中獲得對象
?????public?static?SignInfo?getSignInfo(String?key){
??????????//設置返回對象
??????????SignInfo?result?=?null;
??????????//池中沒有該對象,則建立,并放入池中
??????????if(!pool.containsKey(key)){
???????????????System.out.println(key?+?"----建立對象,并放置到池中");
???????????????result?=?new?SignInfo4Pool(key);
???????????????pool.put(key,?result);
??????????}else{
???????????????result?=?pool.get(key);
???????????????System.out.println(key?+"---直接從池中取得");
??????????}
??????????return?result;
?????}
}
方法都很簡單,不多解釋。讀者需要注意一點的是@Deprecated注解,不要有刪除投產中代碼的念頭,如果方法或類確實不再使用了,增加該注解,表示該方法或類已經過時,盡量不要再使用了,我們應該保持歷史原貌,同時也有助于版本向下兼容,特別是在產品級研發中。
我們再來看看客戶端是如何調用的,如代碼清單28-6所示。
代碼清單28-6 場景類
public?class?Client?{
?????public?static?void?main(String[]?args)?{
??????????//初始化對象池
??????????for(int?i=0;i<4;i++){
???????????????String?subject?=?"科目"?+?i;
???????????????//初始化地址
???????????????for(int?j=0;j<30;j++){
????????????????????String?key?=?subject?+?"考試地點"+j;
????????????????????SignInfoFactory.getSignInfo(key);
???????????????}
??????????}
??????????SignInfo?signInfo?=?SignInfoFactory.getSignInfo("科目1考試地點1");
?????}
}
運行結果如下所示:
科目3考試地點25----建立對象,并放置到池中
科目3考試地點26----建立對象,并放置到池中
科目3考試地點27----建立對象,并放置到池中
科目3考試地點28----建立對象,并放置到池中
科目3考試地點29----建立對象,并放置到池中
科目1考試地點1---直接從池中取得
前面還有很多的對象創建提示語句,不再復制。通過這樣的改造后,我們想想內存中有多少個SignInfo對象?是的,最多120個對象,相比之前幾萬個SignInfo對象優化了非常多。細心的讀者可能注意到了SignInfo4Pool類基本上沒有跑出我們的視線范圍,僅僅在工廠方法中使用到了,盡量縮小變更引起的風險,想想看我們的改動是不是很小,只要在展示層中拼一個字符串,然后傳遞到工廠方法中就可以了。
通過這樣的改造后,第三天系統運行得非常穩定,CPU占用率也下降了,而且以后再也沒有出現類似問題,這就是享元模式的功勞。
- 前言
- 第一部分 大旗不揮,誰敢沖鋒——6大設計原則全新解讀
- 第1章 單一職責原則
- 1.2 絕殺技,打破你的傳統思維
- 1.3 我單純,所以我快樂
- 1.4 最佳實踐
- 第2章 里氏替換原則
- 2.2 糾紛不斷,規則壓制
- 2.3 最佳實踐
- 第3章 依賴倒置原則
- 3.2 言而無信,你太需要契約
- 3.3 依賴的三種寫法
- 3.4 最佳實踐
- 第4章 接口隔離原則
- 4.2 美女何其多,觀點各不同
- 4.3 保證接口的純潔性
- 4.4 最佳實踐
- 第5章 迪米特法則
- 5.2 我的知識你知道得越少越好
- 5.3 最佳實踐
- 第6章 開閉原則
- 6.2 開閉原則的廬山真面目
- 6.3 為什么要采用開閉原則
- 6.4 如何使用開閉原則
- 6.5 最佳實踐
- 第二部分 真刀實槍 ——23種設計模式完美演繹
- 第7章 單例模式
- 7.2 單例模式的定義
- 7.3 單例模式的應用
- 7.4 單例模式的擴展
- 7.5 最佳實踐
- 第8章 工廠方法模式
- 8.2 工廠方法模式的定義
- 8.3 工廠方法模式的應用
- 8.4 工廠方法模式的擴展
- 8.5 最佳實踐
- 第9章 抽象工廠模式
- 9.2 抽象工廠模式的定義
- 9.3 抽象工廠模式的應用
- 9.4 最佳實踐
- 第10章 模板方法模式
- 10.2 模板方法模式的定義
- 10.3 模板方法模式的應用
- 10.4 模板方法模式的擴展
- 10.5 最佳實踐
- 第11章 建造者模式
- 11.2 建造者模式的定義
- 11.3 建造者模式的應用
- 11.4 建造者模式的擴展
- 11.5 最佳實踐
- 第12章 代理模式
- 12.2 代理模式的定義
- 12.3 代理模式的應用
- 12.4 代理模式的擴展
- 12.5 最佳實踐
- 第13章 原型模式
- 13.2 原型模式的定義
- 13.3 原型模式的應用
- 13.4 原型模式的注意事項
- 13.5 最佳實踐
- 第14章 中介者模式
- 14.2 中介者模式的定義
- 14.3 中介者模式的應用
- 14.4 中介者模式的實際應用
- 14.5 最佳實踐
- 第15章 命令模式
- 15.2 命令模式的定義
- 15.3 命令模式的應用
- 15.4 命令模式的擴展
- 15.5 最佳實踐
- 第16章 責任鏈模式
- 16.2 責任鏈模式的定義
- 16.3 責任鏈模式的應用
- 16.4 最佳實踐
- 第17章 裝飾模式
- 17.2 裝飾模式的定義
- 17.3 裝飾模式應用
- 17.4 最佳實踐
- 第18章 策略模式
- 18.2 策略模式的定義
- 18.3 策略模式的應用
- 18.4 策略模式的擴展
- 18.5 最佳實踐
- 第19章 適配器模式
- 19.2 適配器模式的定義
- 19.3 適配器模式的應用
- 19.4 適配器模式的擴展
- 19.5 最佳實踐
- 第20章 迭代器模式
- 20.2 迭代器模式的定義
- 20.3 迭代器模式的應用
- 20.4 最佳實踐
- 第21章 組合模式
- 21.2 組合模式的定義
- 21.3 組合模式的應用
- 21.4 組合模式的擴展
- 21.5 最佳實踐
- 第22章 觀察者模式
- 22.2 觀察者模式的定義
- 22.3 觀察者模式的應用
- 22.4 觀察者模式的擴展
- 22.5 最佳實踐
- 第23章 門面模式
- 23.2 門面模式的定義
- 23.3 門面模式的應用
- 23.4 門面模式的注意事項
- 23.5 最佳實踐
- 第24章 備忘錄模式
- 24.2 備忘錄模式的定義
- 24.3 備忘錄模式的應用
- 24.4 備忘錄模式的擴展
- 24.5 最佳實踐
- 第25章 訪問者模式
- 25.2 訪問者模式的定義
- 25.3 訪問者模式的應用
- 25.4 訪問者模式的擴展
- 25.5 最佳實踐
- 第26章 狀態模式
- 26.2 狀態模式的定義
- 26.3 狀態模式的應用
- 第27章 解釋器模式
- 27.2 解釋器模式的定義
- 27.3 解釋器模式的應用
- 27.4 最佳實踐
- 第28章 享元模式
- 28.2 享元模式的定義
- 28.3 享元模式的應用
- 28.4 享元模式的擴展
- 28.5 最佳實踐
- 第29章 橋梁模式
- 29.2 橋梁模式的定義
- 29.3 橋梁模式的應用
- 29.4 最佳實踐
- 第三部分 誰的地盤誰做主 ——設計模式PK
- 第30章 創建類模式大PK
- 30.1 工廠方法模式VS建造者模式
- 30.2 抽象工廠模式VS建造者模式
- 第31章 結構類模式大PK
- 31.1 代理模式VS裝飾模式
- 31.2 裝飾模式VS適配器模式
- 第32章 行為類模式大PK
- 32.1 命令模式VS策略模式
- 32.2 策略模式VS狀態模式
- 32.3 觀察者模式VS責任鏈模式
- 第33章 跨戰區PK
- 33.1 策略模式VS橋梁模式
- 33.2 門面模式VS中介者模式
- 33.3 包裝模式群PK
- 第四部分 完美世界 ——設計模式混編
- 第34章 命令模式+責任鏈模式
- 34.2 混編小結
- 第35章 工廠方法模式+策略模式
- 35.2 混編小結
- 第36章 觀察者模式+中介者模式
- 36.2 混編小結
- 第五部分 擴展篇
- 第37章 MVC框架
- 37.2 最佳實踐
- 第38章 新模式
- 38.1 規格模式
- 38.2 對象池模式
- 38.3 雇工模式
- 38.4 黑板模式
- 38.5 空對象模式
- 附錄 23種設計模式彩圖