#
Special case: primitive types
Java determines the size of each primitive type. These sizes don’t change from one machine
architecture to another as they do in most languages. This size invariance is one reason Java
programs are more portable than programs in most other languages.
All numeric types are signed, so don’t look for unsigned types.
The size of the boolean type is not explicitly specified; it is only defined to be able to take the literal values true or false.
The “wrapper” classes for the primitive data types allow you to make a non-primitive object on the heap to represent that primitive type. For example:
char c = 'x';
Character ch = new Character(c);
Or you could also use:
Character ch = new Character('x');
Java SE5 autoboxing will automatically convert from a primitive to a wrapper type:
Character ch = 'x';
and back:
char c = ch;
The reasons for wrapping primitives will be shown in a later chapter.
High-precision numbers
Java includes two classes for performing high-precision arithmetic: BigInteger and
BigDecimal. Although these approximately fit into the same category as the “wrapper” classes, neither one has a primitive analogue.
Both classes have methods that provide analogues for the operations that you perform on
primitive types. That is, you can do anything with a BigInteger or BigDecimal that you can with an int or float, it’s just that you must use method calls instead of operators. Also, since there’s more involved, the operations will be slower. You’re exchanging speed for accuracy.
BigInteger supports arbitrary-precision integers. This means that you can accurately represent integral values of any size without losing any information during operations.
BigDecimal is for arbitrary-precision fixed-point numbers; you can use these for accurate
monetary calculations, for example.
OX123=1×162+2×161+3×160
三種形式的整型常量數據:
1.十進制
2.八進制,以o開頭O123
=1×82+1×81+3×80=十進制的83
3.十六進制 ,以ox開頭 OX123=1×162+2×161+3×160
1.The hidden implementation
The goal of the class creator is to build a class that exposes only what is necessary to the client programmer and keeps everything else hidden. Why?
(1)Becuase if it is hidden, the client programmer can't access it, which means that the class creator can change the hidden portion at will withou worring about the impact on anyone else.
(2)The hidden portion usually reprsents the tender insides of an object that could easily be corrupted by a careless or uninformed client programmer, so hiding the implementation reduces program bugs.
2.Reusing the implementation
The simplest way to reuse a class is to just use an object of that class directly, but you can also place an object of that class inside a new class. We call this "creating a member object." Your new class can be made up of any mumber and type of other objectss, in any combination that you need to achieve the fuctionality desired in your new class. Because you are composing a new class from existing classes, this conception is called composition. Compositon is often referred to as a "has-a" relationship, as "A car has an engine."
Because inheritance is so important in OOP, it is often highly emphasized, and the new programmer can get the idea that inheritance should be used everywhere. This can result in awkward and overly complicated designs. Instead, you should first look to composition when creating new classes, since it is simpler and more flexible. If you take this approach,your designer will be cleaner. Once you have had some experience, it will be reasonably obvious when you need inheritance.
3.Inheritance
You have two ways to differentiate your new derived class from the original base class.
The first is quite straightforward: You simply add brand new methods to the derived class. This means that the base class simply didn't as much as you wanted it to, so you added more methods. This simple and primitive use for inheritance is, at times, the perfect solution to your problem. However, you should look closely for the posiblilty that your base class might also need these additional methods. This process of discovery and iteration of your design happens regularly in OOP.
The second and more important way to differentiate your new class is to change the behavior of an existing base-class method. This is referred to as overriding that method. To override a method, you simply create a new definition for the method in the derived class. You are saying, "I am using the same interface method here, but I want it to do something different for my new type."
4.Is-a vs. is-like-a relationships
5.Interchangeable objects with polymorphism
6The single rooted hierarchy
All objects have a single rooted hierarchy can be guaranteed to have certain functionality. You know you can perform certain basic operations on every object in your system. All objects can easy be created on the heap, and argument passing is greatly simplified.
A single rooted hierarchy makes it much easier to implement a garbage collector, which is one of the fundamental improvements of Java over C++. And since information about the type of an object is guaranteed to be in all objects, you'll never end up with an object whose type you cannot determine. This is especially important with system-level operations, such as exception handling, and to allow greater flexibility in programming.
7.Containers
8.Parameterized types(generics)
One of the big changes in Java SE5 is the addition of parameterized types, called generics in java. you will recongize the use of generics by angle brackets with types inside.
9.Object creation & lifetime
How can you possibly know when to destroy the objects?
(1).C++ takes the approach that control of efficiency is the most important issue, so it give the programmer a choice.
(2).Java, in heap
10 Exception handling: dealing with errors
所有的整數類型(除了char 類型之外)都是有符號的整數
因為, java的byte是8bit(位),就是8個0/1 來表示。
但是第一位是符號位,表示正數還是負數。所以:
0000 0001表示1, (1×
20)
0000 0000表示0, (0×
20)
計算機中負數的二進制碼是是負數的絕對值取反,然后加1.
例如-1的二進制:
-1的絕對值是1(0000 0001);
取反是(1111 1110);
再加 1(0000 0001 );
結果是(1111 1111)
要對一個負數的二進制進行解碼,首先對其所有的位取反,然后加1。
例如-1的 二進制 (1111 1111)
取反: 0000 0000 是0
再加1:(0+1=1)
符號位是1,是負數,所以是-1
1000 0000 表示-128, (解碼過程:位取反是0111 1111==》127,然后加1==》128,符號位為1,是負數,表示-128)
軟件在安裝時,到底做了些什么? 大家每天都在用電腦,可能也經常在自己的電腦上安裝軟件。就算自己沒安裝過,至少也看到人家安裝過軟件。在這里,我不是想教你怎么安裝軟件,而是想向你展示,軟件在安裝的過程中,到底都做了些什么動作?為什么有些軟件要安裝,直接拷貝過去卻不能用?為什么一些軟件安裝或卸載之后要重啟。下面要討論的就是這些問題。
首先,我們探討一下軟件安裝的共通部分,說共通,就是在不同版本的操作系統上,如WINDOWS98,WIN2K和WINXP等上它們都有共同點的地方。這個文章也試圖不針對具體的某個操作系統,而對共同的規律來探討,不過我自己用的是WINDOWS98,所以有時一些例子可能會用WINDOWS98上的實例來說明,而大多數情況下這些特***在WIN2K和WINXP上也是類似的。
那么,我先來歸納一下,典型的軟件安裝過程都有可能做哪些事情。由于我們是討論軟件在安裝時的行為,所以開始安裝前的設置和選項我們就暫不討論,只說到軟件真正開始安裝那個時候起的動作:
①文件從安裝源位置拷貝到目標位置。
②往系統目錄寫入一些必要的動態連接庫(DLL)。(可選)
③往系統注冊表中寫入相應的設置項。(可選)
④建立開始菜單里的程序組和桌面快捷方式。(可選)
⑤其他動作。(可選)
下面我們再詳細來分析上面歸納出來的這些動作:
1)拷貝軟件本身需要的文件。源位置指軟件未安裝之前的位置,例如光盤,下載的目錄等,目標位置指你指定的安裝位置。
這是幾乎所有的軟件安裝過程一定會做的一件事。而如果一個軟件,在安裝時只要這一步,不需要后面的其他幾步,我們可以認為這個軟件就是綠色軟件。或者反過來說綠色軟件就是只要拷貝文件,不需要依賴于某個DLL,或者它依賴的DLL在幾乎所有的系統中都一定有的,并且它也不依賴于注冊表里面的設置項的軟件。
2)這一步,可以說至少有一半軟件在安裝時都會做,一些軟件,需要用到某個DLL,特別是那些軟件作者開發的DLL,或者系統中不常用的DLL,一般都會隨軟件的安裝拷到系統目錄。所謂系統目錄,在WIN98下一般是在WINDOWS\SYSTEM這個目錄,而WIN2K是在WINNT\SYSTEM32,WINXP是在WINDOWS\SYSTEM32。還有,一些軟件如QQ游戲,中游等,它們也用到一些DLL,由于這些DLL只是這個軟件自己用到,別的其他軟件不會用到,所以它們并不一定存在于系統目錄,而是放在軟件安裝目錄里面,這樣的DLL已經在上一步中被拷貝,所以和這一步說的情況不一樣。
3)這一步同樣至少有一半軟件會做,一般在安裝前用戶的設置和一些選項,在安裝時就會把這些設置寫到注冊表里。另外就是有時在上一步把DLL拷貝到系統目錄時,一些DLL需要向系統注冊,這些DLL的注冊信息也會寫在注冊表里。還有,一些軟件有時可能安裝時并不寫注冊表,而是在第一次運行時才把一些設置寫到注冊表。
4)這個非常簡單,大概不需要怎么解釋。建立這些快捷方式一方面是便于用戶執行,另外在時也會把卸載的快捷方式放在程序組里。關于卸載后面我們再來討論。
5)這個就是除了上面說的以外的其他情況。例如有些軟件安裝時會先把所有文件(或一部分文件)先解壓到臨時目錄,那么安裝完之后就要把這些文件刪除掉。
那么我們再總結一下:
一、一個典型的軟件在安裝過程一般都會執行上面的1-4項。這樣可以認為是一個完整的安裝過程。
二、除了第1項之外,其他的都不是必要的。只需要第一項的軟件,我們可以把它叫做綠色軟件。
三、有些軟件安裝時是執行了1、2、4,有些軟件是執行了1、3、4,有些軟件是執行了1、4。
四、一個特殊的情況,一般的驅動程序,只會執行2和3,沒有1和4。
五、理論上,任何軟件,如果你非常確切地知道了它在上面的那幾步都具體做了些什么,特別是2和3,那么,理論上你可以把這個軟件的安裝文件拷貝到另一臺機子,把必要的DLL從系統目錄拷貝到那一臺機子的系統目錄,再把注冊表里軟件寫入的項目導出來(必要時還要修改一下)再導入到那臺機子的注冊表中,那么,就算不是綠色軟件,你也能這樣把它移植給另一臺機。但有時特別是一些共享軟件,一般都會有注冊表中設置比較隱蔽的項目,不容易查找,所以除非你對系統非常熟悉,否則不是綠色軟件的軟件要移植還是有一定的難度的。
那么,下面我們再來看看,為什么一些軟件安裝后要重啟。
在WINDOWS操作系統上,一般一個正在運行中的程序,操作系統是不讓你修改它的,修改包括替換,改動和刪除。那么有時,一些軟件需要向系統目錄中寫入一個DLL,而系統目錄中原來已經有同名的DLL并且這個DLL目前正在被系統使用,因此不能用新版本去替換它,這個時候就需要重啟,在重啟的過程中,在這個DLL舊的版本被使用之前用新版本替換它。這就是為什么要重啟的原因。
你能看到這里,說明你很有耐心,并且對技術的探討很有興趣,那么我就再說得更詳細些。在WIN98中,上面說的這個替換是由系統的一個工具來實現的,這個工具叫WININIT.EXE。安裝程序在檢測到需要寫入的DLL或其他程序文件正在使用時,會把要寫入的DLL文件先定一個臨時的文件名,然后在WINDOWS目錄中往WININIT.INI寫入一個改寫項,比如,一個叫ABCD.DLL的動態連接庫現在正在使用中,而安裝程序要往系統中寫入新版本的ABCD.DLL,這時安裝程序會把新版本ABCD.DLL先定一個臨時文件名,例如AAAA.LLL,然后在WININIT.INI中的[rename]一節中寫入這一項: 篩l罉枓犮
C:\windows\system\abcd.dll=C:\windows\system\aaaa.lll CX=B)
這樣,在重啟時,進入WINDOWS圖形界面之前,WININIT.EXE在檢測到WINDOWS目錄中有WININIT.INI存在時,就執行里面的操作,在上面的例子中,是用C:\windows\system\aaaa.lll去替換掉C:\windows\system\abcd.dll這個文件,并且把WININIT.INI改名為WININIT.BAK。
另外,有些軟件,在安裝時,是把所有文件包括SETUP.EXE解壓到臨時文件里面再執行SETUP.EXE進行安裝的,按理來說安裝完要把所有的臨時文件刪除掉,這個操作當然也是由安裝程序SETUP.EXE來完成,但它自己正在運行,也刪不了它自己,所以也要重啟來刪除,做法和上面差不多,只是改成類似這樣子的: 怦S?vH燁?
NUL=C:\WINDOWS\TEMP\SETUP.EXE
在WIN2K和WINXP中,存在類似的機制,不過并不是用WININIT.EXE和WININIT.INI來實現,具體的做法我也不是很清楚,長期以來我大多數時候都是在用WIN98,所以沒認真研究過,但軟件安裝過程要重啟的現象在2K和XP上是仍然存在的,原理也是在重啟時替換或修改正在使用的文件,只是實現的方式不同。
最后,我們再來看看有關卸載方面的內容。一般卸載有好幾種方式:
1)早期的安裝程序,一般會在安裝過程記錄了上面說的安裝過程的1234四個步驟中具體拷貝的文件和DLL以及注冊表項,把它保存在INSTALL.LOG之類的文件中,再在軟件的安裝目錄(或WINDOWS目錄中)放一個UNINST.EXE之類的卸載程序。然后要么在程序組里為這個UNINST.EXE建一個快捷方式,要么在注冊表中為這個UNINST.EXE建一個快捷方式(這誑刂潑姘宓奶砑由境絳蚓湍蕓吹餃砑男對叵?,并把INSTALL.LOG做為它的參數,這樣就實現卸載了。
2)現在比較多的安裝程序是用新版的INSTALLSHIELD生成的,安裝時的記錄和卸載程序一般是會放在C:\Program Files\InstallShield Installation Information這個文件夾(隱藏屬***)里,同樣也會在程序組和注冊表中建立卸載項。
另外,在卸載時,也會遇到文件(一般是DLL文件)正在使用的情況。所以有時卸載的時候也要重啟,就是要在重啟過程中刪掉這些正在使用的DLL文件。
關于軟件的安裝過程,大概就想到這里,以后再有想到什么的,我再補充,大家有什么看不懂的也可以把問題提出來。
安裝新
軟件前,打開注冊表編輯器,選擇“注冊表→導出注冊表文件”,利用“全部”選項,將結果文件保存為Before.txt(不要使用REG擴展名)。安裝新
軟件或進行用戶想跟蹤的其他任何更改后,打開注冊表編輯器,再導出整個注冊表,這一次將導出的文件命名為After.txt文件。接著打開MS-DOS命令窗口,轉換到有那兩個文本文件的目錄中,然后執行以下命令:
FC Before.txt After.txt > Diff.txt
關閉DOS窗口,在“記事本”中打開Diff.txt文件,這里會顯示在注冊表所發現的所有不同之處。
我們在解析配置文件的時候,常常會為路徑發愁,我就遇到過這樣的情況

如上圖所示:
ParseProperties.
java是配置文件database.properties的解析類,那么我們怎樣去取得它的路徑并解析起配置呢?看解析類ParseProperties的源代碼如下:
package zy.pro.sc.db;
import java.util.*;
import java.io.*;
public class ParseProperties {
Properties properties = new Properties();
public ParseProperties() {
try{
this.parseProp();
}catch(Exception e){
e.printStackTrace();
}
}
public Properties parseProp()throws IOException {
InputStream is=this.getClass().getResourceAsStream("database.properties");
/*
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream in = classLoader.getSystemResourceAsStream(fileName);
*/
properties.load(is);
is.close();
return null;
}
public String getProperties(String propStr){
return properties.getProperty(propStr);
}
public static void main(String[] args) {
ParseProperties pp=new ParseProperties();
String driver=pp.getProperties("jdbc.driver");
System.out.println(driver);
}
}
看粗體部分,this.getClass()方法可以得到了當前類的Class對象,也可以用ParseProperties.class.getClass()方法來實現同樣的效果。之后調用其getResourceAsStream("database.properties")方法來解析配置文件。getResourceAsStream()方法解析文件時候的相對路徑是當前類的包路徑。
就當前的包來說,zy.pro.sc.db對應的路徑是src/zy/pro/sc/db。由于我們要解析的文件和解析類在同一目錄下,所以我們的路徑是"database.properties"。
如果我們的解析文件和解析類不在同一目錄下呢,如以下目錄結構:

那么先看一下我們解析類的代碼:
InputStream is=this.getClass().getResourceAsStream("/database.properties");
解析路徑變成了"/database.properties", “/“表示取當前類所在的包的根路徑下的database.properties文件,也就是相對于ParsePropertie.class的包的根路徑下的 database.properties文件。
用這種方法更有靈活性。此地要認真體會。不用這種方法,你的解析類只能在目錄結構不發生改變的情況下使用。否則將發生異常。例如:你的解析類在servlet中調用的時候就會拋出找不到文件的異常。
此路徑的定位方法也可以使用于解析XML的文件。詳細情況同上。
JProfiler是一款Java的性能監控工具。可以查看當前應用的
對象、對象引用、內存、CPU使用情況、線程、線程運行情況(阻塞、等待等),同時可以查找應用內存使用得熱點,即:哪個對象占用的內存比較多;或者CPU熱點,即:哪兒方法占用的較大得CPU資源。我使用的是4.3.2版本,以前試用過3**版本,不過那個bug比較多,容易死,4**版本穩定多了。
有了上面那些信息對于系統的調優會有很大幫助。這里提供有幾篇文章供參考:獲取、介紹,簡單入門,使用JProfiler解決實際問題。這幾篇文章基本介紹了常見東西了,下面說點心得。
- JProfiler監控是要消耗系統資源的,所以一般情況下不要用于性能測試時候的監控。
- 如果要用于相對大壓力情況下,可以有選擇的打開監控項,不用所有都打開。主要有兩個,一個是內存監控,打開的情況下可以查找內存分配熱點。一個是CPU監控,打開的情況下可以查看CPU使用熱點。
如圖所示,紅筆標注部分。如果兩個都關閉的話,還是可以跑一定壓力的,同時還可以監控對象數量。
- 個人認為最好用的(也是用的最多的)是查詢當前的對象的數量。數量監控很重要,如果你使用了單例,那么你只會看到有一個對象存在,如果多了就說明程序有問題了。同樣,如果應用進行一系列操作,檢查一下該銷毀的對象是否還繼續存在,如果沒有釋放,就得考慮是否存在內存溢出了。
- JProfiler還提供了一個比較好的檢查內存溢出得工具。他可以查找某個對象的引用情況,即:當你發現某個該釋放掉的對象沒有釋放,就可以看一下哪個實例在引用它,找到了根即找到了溢出點。
具體操作如下:在 “Memory Views”界面中右鍵選擇你要監控的對象,選擇第一項“Take Heap Snapshot for Selection”,選擇完成后會進入“Heap Walker”界面,界面下面提供幾個功能,選擇“References”即可 。如圖:
- JProfiler提供不同的觀察粒度,提供對類的監控、對包的監控、對J2EE組件的監控,同時過濾器也比較好用,直接定位你關注的包或類即可。
- JProfiler的監控可能與應用之間存在一定時間差,所以有些時候需要等待刷新,才能顯示正確系統情況。
在中間件應用服務器的整體調優中,有關于等待隊列、執行線程,EJB池以及數據庫連接池和Statement Cache方面的調優,這些都屬于系統參數方面的調優,本文主要從另外一個角度,也就是從應用的角度來解決中間件應用服務器的內存泄露問題,從這個角度來提高系統的穩定性和性能。
項目背景
問題描述
某個大型項目(Use Case用例超過300個),在項目上線后,其Web應用服務器經常宕機。表現為:
1. 應用服務器內存長期不合理占用,內存經常處于高位占用,很難回收到低位;
2. 應用服務器極為不穩定,幾乎每兩天重新啟動一次,有時甚至每天重新啟動一次;
3. 應用服務器經常做Full GC(Garbage Collection),而且時間很長,大約需要30-40秒,應用服務器在做Full GC的時候是不響應客戶的交易請求的,非常影響系統性能。
Web應用服務器的物理部署
一臺Unix服務器(4CPU,8G Memory)來部署本Web應用程序;Web應用程序部署在中間件應用服務器上;部署了一個節點(Node),只配置一個應用服務器實例(Instance),沒有做Cluster部署。
Web應用服務器啟動腳本中的內存參數
MEM_ARGS="-XX:MaxPermSize=128m -XX:MaxNewSize=512m -Xms3096m
-Xmx3096m -XX:+Printetails -Xloggc:./inwebapp1/gc.$$" |
可以看出目前生產系統中Web應用服務器的內存分配為3G Memory。
Web應用服務器的重要部署參數
參數名稱 |
參數值 |
參數解釋 |
kernel.default(Thread Count) |
120 |
執行線程數目,是并發處理能力的重要參數 |
Session Timeout |
240分鐘(4小時) |
HttpSession會話超時 |
分析
分析方法
內存長期占用并導致系統不穩定一般有兩種可能:
1. 對象被大量創建而且被緩存,在舊的對象釋放前又有大量新的對象被創建使得內存長期高位占用。
- 表現為:內存不斷被消耗、在高位時也很難回歸到低位,有大量的對象在不斷的創建,經過很長時間后又被回收。例如:在HttpSession中保存了大量的分頁查詢數據,而HttpSession的會話超時時間設置過長(例如:1天),那么在舊的對象釋放前又有大量新的對象在第二天產生。
- 解決辦法:對共享的對象可以采用池機制進行緩存,避免各自創建;緩存的臨時對象應該及時釋放;另一種辦法是擴大系統的內存容量。
2. 另一種情況就是內存泄漏問題
- 表現為:內存回收低位點不斷升高(以每次內存回收的最低點連成一條直線,那么它是一條上升線);內存回收的頻率也越來越高,內存占用也越來越高,最終出現"Out of Memory Exception"的系統異常。
- 解決辦法:定位那些有內存泄漏的類或對象并修改完善這些類以避免內存泄漏。方法是:經過一段時間的測試、監控,如果某個類的對象數目屢創新高,即使在JVM Full GC后仍然數目降不下來,這些對象基本上是屬于內存泄漏的對象了。
問題定位
這里請看5月份 Web應用服務器的內存回收圖形:
《注意:5月18日早上10點重新啟動了Web服務器,5月20日早上又重新啟動了Web服務器。》
- 在Web應用重要部署參數中,我們知道:Session的超時時間為4個小時,我們在監控平臺也觀測到:在18日晚上10點左右所有的會話都過期了,從圖形一中也能看出18日晚上確實系統的內存有回收到40%(就象股票的高位跳水);
- 從圖形一(5月18日)中我們也能看到Full GC回收后的內存占用率走勢(紅色曲線),上午基本平滑上升到20%(內存占用率),中午開始上升到30%,下午上升到40%
- 從圖形二(5月19日)中我們也能看到Full GC回收后的內存占用率走勢(紅色曲線),上午又上升到了60%,到下午上升到了70%。
- 從黃色曲線(GC花費的時間,以秒為單位),Full GC的頻率也在增快,時間耗費也越來越長,在圖形一中基本高位在20秒左右,到19日基本都是30-40秒之間了。
圖形一 5月18日

圖二

通過上述分析,我們基本定位到了Web應用服務器的內存在高位長期占用的原因了:是內存泄露!并且正是由于這個原因導致系統不穩定、響應客戶請求越來越慢的。
解決方法
方法如下:
- 我們從圖形二中發現,在8.95(將近9點鐘)到9.66(將近9點40)期間有幾次Full GC,但是有內存泄漏,從占用率40%上升到50%左右,泄漏了大約10%的內存,約300M;
- 我們在自己搭建的Web應用服務器平臺(應用軟件版本和生產版本一致)做這一階段相同的查詢交易;表明對同一個黑盒(Web應用)施加同樣的刺激(相同的操作過程和查詢交易)以期重現現象;
- 我們使用Jprofiler工具對Web應用服務器的內存進行實時監控;
- 做完這些交易后,用戶退出系統,并等待Web應用服務器的HttpSession超時(我們這里設置為15分鐘);
- 我們對Web應用服務器做了兩次強制性的內存回收操作。
發現如下:
圖三

如圖三所示,內存經過HttpSession超時后,并強制gc后,仍然有大量的對象沒有釋放。例如:gov.gdlt.taxcore.comm.security.MenuNode,仍然有807個實例沒有釋放。
我們繼續追溯發現,這些MenuNode首先存放在一個ArrayList對象中,然后發現這個ArrayList對象又是存放在WHsessionAttrVO對象的Map中,WHsessionAttrVO 對象又是存放在ExternalSessionManager的staic Map中(名稱為sessionMap),如圖四所示。
圖四

我們發現gov.gdlt.taxcore.taxevent.xtgl.comm.WHsessionAttrVO中保存了EJBSessionId信息(登錄用戶的唯一標志,由用戶id+登錄時間戳組成,每天都不同)和一個HashMap,這個HashMap中的內容有:
- ArrayList: 內有MenuTreeNodes(菜單樹節點)
- HashMap: 內有操作人員代碼信息
- CurrentVersion:當前版本號
- CurrentTime:當前系統時間
WHsessionAttrVO這個對象的最終存放在ExternalSessionManager的static Map sessionMap中,由于ExternalSessionManager是一個全局的單實例,不會釋放,所以它的成員變量sessionMap中的數據也不會釋放,而Map中的Key值為EJBSessionId,每天登錄的用戶EJBSessionId都不同,就造成了每天的登錄信息(包括菜單信息)都保存在sessionMap中不會被釋放,最終造成了內存的泄漏。
圖五

如上圖所示:WHsessionAttrsVO對象中除了有一個String對象(內容是EJBSessionId),還有一個HashMap對象。
圖六

如上圖所示,這個HashMap中的內容主要有menuTreeNodes為key,value為ArrayList的對象和以czrydminfo為key,value為HashMap對象的數據。
圖七

如上圖所示:menuTreeNodes為key,value為ArrayList對象中包含的對象有許多的MenuNode對象,封裝的都是用戶的菜單節點。
圖八

如上圖所示,最頂層(Root)的初始對象為一個ExternalSessionManager對象,其中的一個成員變量為static (靜態的),名稱為:sessionMap,這個對象是singleton方式的,全局只有一個。
初步估量
我們從圖形一和圖形二中可以看出,每天應用服務器損失大約40%的內存,大約1G左右。
從圖形四可以看出,當前用戶(Id=24400001129)有807個菜單項(每個菜單項為一個MenuNode 對象實例,圖形四中的這個實例的size為592 Byte),這些菜單數據和用戶基本登錄信息(czrydmInfo HashMap)也都存放在WHsessionAttrVO對象中,當前這個WHsessionAttrVO對象的size為457K。
我們做如下估算:
假設平均每天有4千人(估計值,這個數值僅僅是5月19日峰值的1/2左右)登錄系統(有重復登錄的現象,例如:上午登錄一次,中午退出系統,下午登錄一次),以平均每人占用200K(估計值,是用戶id=24400001129 的Size的1/2左右)來計算,一天泄漏的內存約800M,比較符合目前內存泄漏的情況。當然,這種估計仍然需要經過實踐的檢驗,方法是:當這次發現的內存泄漏問題解決后看系統是否還有其它內存泄漏問題。
方案
ExternalSessionManager類是當初某某軟件商設計的用來解決Web服務器負載均衡的模塊,這個類主要用來保存客戶的基本登錄信息(包括會話的EJBSessionId),以維護多個Web服務器之間的會話信息一致。
改進方案有兩種:
-
從架構設計方面改進
實現Web層的負載均衡有很多標準的實現方式。例如:采用負載均衡設備(硬件或軟件)來實現。
如果采用新的Web層的負載均衡方式,那么就可以去掉ExternalSessionManager這個類了。
-
從應用實現方面改進
保留當前的Web層的負載均衡設計機制,僅僅從應用實現方面解決內存泄漏問題,首先菜單信息不應該保存在ExternalSessionManager中。其次,增加對ExternalSessionManager類中用戶會話登錄信息的清除,有幾種方式可以選擇:
- 被動方式,當HttpSession會話超時(或過期)被Web應用服務器回收時清除相應的ExternalSessionManager中的過期會話登錄信息。
- 主動方式,可以采用任務定時清理每天的過期會話登錄信息或線程輪詢清理。
- 采用新的會話登錄信息存儲方式,ExternalSessionManager的sessionMap中的key值不再以EJBSessionId作為鍵值,而是以用戶id(EJBSessionId的前11位)代替。由于用戶id每天都是一樣的,所以不會造成內存泄漏。保存得登錄信息也不再包含菜單節點信息,而只是登錄基本信息。最多也只是保存整個系統所有的用戶id及其基本登錄信息(大約每個用戶的登錄信息只有1.5K左右,而目前這個系統的營業網點用戶為1萬左右,所以大約只占用Web服務器15M內存)。
實施情況
采用的方案:某某軟件商采用了新的會話登錄信息存貯方案,即:ExternalSessionManager的成員變量sessionMap中不再保存用戶菜單信息,只保存基本的登錄信息;存儲方式采用用戶id(11位)作為鍵值(key)來保留用戶基本登錄信息。
基本分析:由于基本登錄信息只有1K左右,而目前內網登錄的用戶總數也只有8887個,所以只保存了大約10M-15M的信息在內存,占用量很小,并且不會有內存泄漏。用戶菜單信息保存在session中,如果用戶退出時點擊logout頁面,那么應用服務器可以很快地釋放這部分內存;如果用戶直接關閉窗口,那么保存在session中的菜單信息只有等會話超時后才會由系統清除并回收內存。
監控狀況:
圖九

如圖九所示,ExternalSessionManager中只保留了簡單的登錄信息(Map中保存了WHsessionAttrVO對象),包括:當前版本(currentversion),操作人員代碼基本信息(czrydmInfo),當前時間(currenttime)。
圖十

如圖十所示,這個登錄用戶的基本信息只有1368 bytes,大約1.3K
圖十一

如圖十一所示,一共有兩個用戶(相同的用戶id)登錄系統,當一個用戶使用logout頁面退出時,保留在session中的菜單信息(MenuNode)立刻釋放了,所以Difference一欄減少了806個菜單項。
圖十二

如圖十二所示,當另外一個會話超時后,應用服務器回收了整個會話的菜單信息(MenuNode),圖上已經沒有MenuNode對象了。并且由于是同一個用戶登錄,所以保留在ExternalSessionManager成員變量sessionMap中的對象WHsessionAttrVO只有一個(id=24400001129),而沒有產生多個,沒有因為多次登錄而產生多個對象的后果,避免了內存泄漏問題的出現,解決了前期定位的內存泄漏問題。
圖十三

如圖十三所示,經過gc內存回收后,發現內存回收比較穩定,基本都回收到了最低點,也證明了內存沒有泄露。
結論與建議:從測試情況看,解決了前期定位的內存泄漏問題。
生產系統實施后的監控與分析
經過調優后,我們發現:在2005年6月2日晚9點40左右重新部署、啟動了Web應用服務器(采用了新的調優方案)。經過幾天的監控運行,發現Web應用服務器目前運行基本穩定,目前沒有出現新的內存泄漏問題,下列圖示說明了這一點
圖十四 2005年6月2日

如圖十四所示,6月2日晚21.7(21點42分)重新啟動應用服務器,內存占用很少,大約為15%(請看紅色曲線),每次GC消耗的時間也很短,大約在5秒以內(請看黃色曲線)。
圖十五 2005年6月3日周五

如圖十五所示,在6月3日周五的整個工作日內,內存的回收基本到位,回收位置控制在20%-30%之間,也就是在600M-900M之間(請看紅色曲線的最低點),始終可以回收2G的內存供應用程序使用,每次GC的時間最高不超過20秒,Full GC平均在10秒左右,時間消耗比較短(請看黃色曲線)。
圖十六2005年6月5日周日

如圖十六所示,在周日休息日期間,Web應用服務器全天只做了大約4次Full GC(黃色曲線中的小山峰),時間都在10秒以內;大的Full GC后,內存只占用10%,內存回收很徹底。
圖十七 2005年6月6日周一

如圖十七所示,在周一工作日期間,內存回收還是不錯的,基本可以回收到30%(見紅色曲線的最低點),即:占用900M內存空間,剩余2G的內存空間;Full GC的時間大部分控制在20秒以內,平均15秒(見黃色曲線)。
圖十八 2005年6月7日周二

如圖十八所示,在6月7日周二早上,大約8:30左右,Web應用服務器作了一次Full GC,用了10秒的時間,把內存回收到了10%的位置,為后續的使用騰出了90%的內存空間。內存回收仍然比較徹底,說明基本沒有內存泄漏問題。
經過這幾天的監控分析,我們可以看出:
- Web應用服務器的內存使用已經比較合理,內存在工作日的占用在20%至30%之間,約1G的內存占用,有2G的內存空間富裕;而在空閑時間(周日,每天的凌晨等)內存可以回收到10%,有90%的內存空間富裕;
- Web應用服務器的Full GC的次數明顯減少了并且每次Full GC占用的時間也很少,基本控制在10-20秒之間,有的甚至在10秒以內,明顯改善了內網應用服務器內存的使用;
- 從6月2日重新部署之后,Web應用服務器沒有出現宕機重啟的現象。
總結
通過本文,我們可以看到,內存的泄露將會導致服務器的宕機,系統性能就更別說了。對于系統內存泄露問題應該從服務器GC日志方面進行早診斷,使用工具早確認并提出解決方案,排除內存泄露問題,提高系統性能,以規避項目風險。
Sample1,利用Menifest文件讀取jar中的文件
/*
1.文件目錄
test--
--a.text
--b.gif
2. Menifest文件內容:
Manifest-Version: 1.0
abc: test/a.txt
iconname: test/Anya.jpg
注意:manifest.mf文件最后一行要打一回車
Another Notification:
如果manifest文件內容是:
Manifest-Version: 1.0
Main-Class: com.DesignToolApp
Class-path: lib/client.jar lib/j2ee.jar
在MANIFEST.MF文件的最后,要留兩個空行(也就是回車),才可以識別到Class-Path這一行,如果只有一個空行,那么只識別到Main-Class這一行。Class-Path中的庫名用空格格開,使用和jar包相對的路徑,發布時把jar包和其他用到的類庫一起交給用戶就可以了。
3.打jar包
test.jar
*/
String iconpath = jar.getManifest().getMainAttributes().getValue("abc");
InputStream in = jar.getInputStream(jar.getJarEntry(iconpath));
//Image img = ImageIO.read(in);
InputStreamReader isr = new InputStreamReader(in);
BufferedReader reader = new BufferedReader(isr);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
Sample2,讀取JAR 文件列表及各項的名稱、大小和壓縮后的大小
public class JarFileInfoRead {
public static void main (String args[])
throws IOException {
String jarpath="d://temp//test.jar";
JarFile jarFile = new JarFile(jarpath);
Enumeration enu = jarFile.entries();
while (enu.hasMoreElements()) {
process(enu.nextElement());
}
}
private static void process(Object obj) {
JarEntry entry = (JarEntry)obj;
String name = entry.getName();
long size = entry.getSize();
long compressedSize = entry.getCompressedSize();
System.out.println(name + "\t" + size + "\t" + compressedSize);
}
}
Sample3,讀取JAR中 文件的內容
public class JarFileRead {
public static void main (String args[])
throws IOException {
String jarpath="d://temp//test.jar";
JarFile jarFile = new JarFile(jarpath);
Enumeration enu = jarFile.entries();
while (enu.hasMoreElements()) {
JarEntry entry = (JarEntry)enu.nextElement();
String name = entry.getName();
//System.out.println(name);
if(name.equals("test/a.txt")){
InputStream input = jarFile.getInputStream(entry);
process(input);
}
}
jarFile.close();
}
private static void process(InputStream input)
throws IOException {
InputStreamReader isr =
new InputStreamReader(input);
BufferedReader reader = new BufferedReader(isr);
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
}