#
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
三種形式的整型常量數(shù)據(jù):
1.十進(jìn)制
2.八進(jìn)制,以o開頭O123
=1×82+1×81+3×80=十進(jìn)制的83
3.十六進(jìn)制 ,以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
所有的整數(shù)類型(除了char 類型之外)都是有符號的整數(shù)
因為, java的byte是8bit(位),就是8個0/1 來表示。
但是第一位是符號位,表示正數(shù)還是負(fù)數(shù)。所以:
0000 0001表示1, (1×
20)
0000 0000表示0, (0×
20)
計算機(jī)中負(fù)數(shù)的二進(jìn)制碼是是負(fù)數(shù)的絕對值取反,然后加1.
例如-1的二進(jìn)制:
-1的絕對值是1(0000 0001);
取反是(1111 1110);
再加 1(0000 0001 );
結(jié)果是(1111 1111)
要對一個負(fù)數(shù)的二進(jìn)制進(jìn)行解碼,首先對其所有的位取反,然后加1。
例如-1的 二進(jìn)制 (1111 1111)
取反: 0000 0000 是0
再加1:(0+1=1)
符號位是1,是負(fù)數(shù),所以是-1
1000 0000 表示-128, (解碼過程:位取反是0111 1111==》127,然后加1==》128,符號位為1,是負(fù)數(shù),表示-128)
軟件在安裝時,到底做了些什么? 大家每天都在用電腦,可能也經(jīng)常在自己的電腦上安裝軟件。就算自己沒安裝過,至少也看到人家安裝過軟件。在這里,我不是想教你怎么安裝軟件,而是想向你展示,軟件在安裝的過程中,到底都做了些什么動作?為什么有些軟件要安裝,直接拷貝過去卻不能用?為什么一些軟件安裝或卸載之后要重啟。下面要討論的就是這些問題。
首先,我們探討一下軟件安裝的共通部分,說共通,就是在不同版本的操作系統(tǒng)上,如WINDOWS98,WIN2K和WINXP等上它們都有共同點的地方。這個文章也試圖不針對具體的某個操作系統(tǒng),而對共同的規(guī)律來探討,不過我自己用的是WINDOWS98,所以有時一些例子可能會用WINDOWS98上的實例來說明,而大多數(shù)情況下這些特***在WIN2K和WINXP上也是類似的。
那么,我先來歸納一下,典型的軟件安裝過程都有可能做哪些事情。由于我們是討論軟件在安裝時的行為,所以開始安裝前的設(shè)置和選項我們就暫不討論,只說到軟件真正開始安裝那個時候起的動作:
①文件從安裝源位置拷貝到目標(biāo)位置。
②往系統(tǒng)目錄寫入一些必要的動態(tài)連接庫(DLL)。(可選)
③往系統(tǒng)注冊表中寫入相應(yīng)的設(shè)置項。(可選)
④建立開始菜單里的程序組和桌面快捷方式。(可選)
⑤其他動作。(可選)
下面我們再詳細(xì)來分析上面歸納出來的這些動作:
1)拷貝軟件本身需要的文件。源位置指軟件未安裝之前的位置,例如光盤,下載的目錄等,目標(biāo)位置指你指定的安裝位置。
這是幾乎所有的軟件安裝過程一定會做的一件事。而如果一個軟件,在安裝時只要這一步,不需要后面的其他幾步,我們可以認(rèn)為這個軟件就是綠色軟件。或者反過來說綠色軟件就是只要拷貝文件,不需要依賴于某個DLL,或者它依賴的DLL在幾乎所有的系統(tǒng)中都一定有的,并且它也不依賴于注冊表里面的設(shè)置項的軟件。
2)這一步,可以說至少有一半軟件在安裝時都會做,一些軟件,需要用到某個DLL,特別是那些軟件作者開發(fā)的DLL,或者系統(tǒng)中不常用的DLL,一般都會隨軟件的安裝拷到系統(tǒng)目錄。所謂系統(tǒng)目錄,在WIN98下一般是在WINDOWS\SYSTEM這個目錄,而WIN2K是在WINNT\SYSTEM32,WINXP是在WINDOWS\SYSTEM32。還有,一些軟件如QQ游戲,中游等,它們也用到一些DLL,由于這些DLL只是這個軟件自己用到,別的其他軟件不會用到,所以它們并不一定存在于系統(tǒng)目錄,而是放在軟件安裝目錄里面,這樣的DLL已經(jīng)在上一步中被拷貝,所以和這一步說的情況不一樣。
3)這一步同樣至少有一半軟件會做,一般在安裝前用戶的設(shè)置和一些選項,在安裝時就會把這些設(shè)置寫到注冊表里。另外就是有時在上一步把DLL拷貝到系統(tǒng)目錄時,一些DLL需要向系統(tǒng)注冊,這些DLL的注冊信息也會寫在注冊表里。還有,一些軟件有時可能安裝時并不寫注冊表,而是在第一次運行時才把一些設(shè)置寫到注冊表。
4)這個非常簡單,大概不需要怎么解釋。建立這些快捷方式一方面是便于用戶執(zhí)行,另外在時也會把卸載的快捷方式放在程序組里。關(guān)于卸載后面我們再來討論。
5)這個就是除了上面說的以外的其他情況。例如有些軟件安裝時會先把所有文件(或一部分文件)先解壓到臨時目錄,那么安裝完之后就要把這些文件刪除掉。
那么我們再總結(jié)一下:
一、一個典型的軟件在安裝過程一般都會執(zhí)行上面的1-4項。這樣可以認(rèn)為是一個完整的安裝過程。
二、除了第1項之外,其他的都不是必要的。只需要第一項的軟件,我們可以把它叫做綠色軟件。
三、有些軟件安裝時是執(zhí)行了1、2、4,有些軟件是執(zhí)行了1、3、4,有些軟件是執(zhí)行了1、4。
四、一個特殊的情況,一般的驅(qū)動程序,只會執(zhí)行2和3,沒有1和4。
五、理論上,任何軟件,如果你非常確切地知道了它在上面的那幾步都具體做了些什么,特別是2和3,那么,理論上你可以把這個軟件的安裝文件拷貝到另一臺機(jī)子,把必要的DLL從系統(tǒng)目錄拷貝到那一臺機(jī)子的系統(tǒng)目錄,再把注冊表里軟件寫入的項目導(dǎo)出來(必要時還要修改一下)再導(dǎo)入到那臺機(jī)子的注冊表中,那么,就算不是綠色軟件,你也能這樣把它移植給另一臺機(jī)。但有時特別是一些共享軟件,一般都會有注冊表中設(shè)置比較隱蔽的項目,不容易查找,所以除非你對系統(tǒng)非常熟悉,否則不是綠色軟件的軟件要移植還是有一定的難度的。
那么,下面我們再來看看,為什么一些軟件安裝后要重啟。
在WINDOWS操作系統(tǒng)上,一般一個正在運行中的程序,操作系統(tǒng)是不讓你修改它的,修改包括替換,改動和刪除。那么有時,一些軟件需要向系統(tǒng)目錄中寫入一個DLL,而系統(tǒng)目錄中原來已經(jīng)有同名的DLL并且這個DLL目前正在被系統(tǒng)使用,因此不能用新版本去替換它,這個時候就需要重啟,在重啟的過程中,在這個DLL舊的版本被使用之前用新版本替換它。這就是為什么要重啟的原因。
你能看到這里,說明你很有耐心,并且對技術(shù)的探討很有興趣,那么我就再說得更詳細(xì)些。在WIN98中,上面說的這個替換是由系統(tǒng)的一個工具來實現(xiàn)的,這個工具叫WININIT.EXE。安裝程序在檢測到需要寫入的DLL或其他程序文件正在使用時,會把要寫入的DLL文件先定一個臨時的文件名,然后在WINDOWS目錄中往WININIT.INI寫入一個改寫項,比如,一個叫ABCD.DLL的動態(tài)連接庫現(xiàn)在正在使用中,而安裝程序要往系統(tǒng)中寫入新版本的ABCD.DLL,這時安裝程序會把新版本ABCD.DLL先定一個臨時文件名,例如AAAA.LLL,然后在WININIT.INI中的[rename]一節(jié)中寫入這一項: 篩l罉枓犮
C:\windows\system\abcd.dll=C:\windows\system\aaaa.lll CX=B)
這樣,在重啟時,進(jìn)入WINDOWS圖形界面之前,WININIT.EXE在檢測到WINDOWS目錄中有WININIT.INI存在時,就執(zhí)行里面的操作,在上面的例子中,是用C:\windows\system\aaaa.lll去替換掉C:\windows\system\abcd.dll這個文件,并且把WININIT.INI改名為WININIT.BAK。
另外,有些軟件,在安裝時,是把所有文件包括SETUP.EXE解壓到臨時文件里面再執(zhí)行SETUP.EXE進(jìn)行安裝的,按理來說安裝完要把所有的臨時文件刪除掉,這個操作當(dāng)然也是由安裝程序SETUP.EXE來完成,但它自己正在運行,也刪不了它自己,所以也要重啟來刪除,做法和上面差不多,只是改成類似這樣子的: 怦S?vH燁?
NUL=C:\WINDOWS\TEMP\SETUP.EXE
在WIN2K和WINXP中,存在類似的機(jī)制,不過并不是用WININIT.EXE和WININIT.INI來實現(xiàn),具體的做法我也不是很清楚,長期以來我大多數(shù)時候都是在用WIN98,所以沒認(rèn)真研究過,但軟件安裝過程要重啟的現(xiàn)象在2K和XP上是仍然存在的,原理也是在重啟時替換或修改正在使用的文件,只是實現(xiàn)的方式不同。
最后,我們再來看看有關(guān)卸載方面的內(nèi)容。一般卸載有好幾種方式:
1)早期的安裝程序,一般會在安裝過程記錄了上面說的安裝過程的1234四個步驟中具體拷貝的文件和DLL以及注冊表項,把它保存在INSTALL.LOG之類的文件中,再在軟件的安裝目錄(或WINDOWS目錄中)放一個UNINST.EXE之類的卸載程序。然后要么在程序組里為這個UNINST.EXE建一個快捷方式,要么在注冊表中為這個UNINST.EXE建一個快捷方式(這誑刂潑姘宓奶砑由境絳蚓湍蕓吹餃砑男對叵?,并把INSTALL.LOG做為它的參數(shù),這樣就實現(xiàn)卸載了。
2)現(xiàn)在比較多的安裝程序是用新版的INSTALLSHIELD生成的,安裝時的記錄和卸載程序一般是會放在C:\Program Files\InstallShield Installation Information這個文件夾(隱藏屬***)里,同樣也會在程序組和注冊表中建立卸載項。
另外,在卸載時,也會遇到文件(一般是DLL文件)正在使用的情況。所以有時卸載的時候也要重啟,就是要在重啟過程中刪掉這些正在使用的DLL文件。
關(guān)于軟件的安裝過程,大概就想到這里,以后再有想到什么的,我再補(bǔ)充,大家有什么看不懂的也可以把問題提出來。
安裝新
軟件前,打開注冊表編輯器,選擇“注冊表→導(dǎo)出注冊表文件”,利用“全部”選項,將結(jié)果文件保存為Before.txt(不要使用REG擴(kuò)展名)。安裝新
軟件或進(jìn)行用戶想跟蹤的其他任何更改后,打開注冊表編輯器,再導(dǎo)出整個注冊表,這一次將導(dǎo)出的文件命名為After.txt文件。接著打開MS-DOS命令窗口,轉(zhuǎn)換到有那兩個文本文件的目錄中,然后執(zhí)行以下命令:
FC Before.txt After.txt > Diff.txt
關(guān)閉DOS窗口,在“記事本”中打開Diff.txt文件,這里會顯示在注冊表所發(fā)現(xiàn)的所有不同之處。
我們在解析配置文件的時候,常常會為路徑發(fā)愁,我就遇到過這樣的情況

如上圖所示:
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()方法可以得到了當(dāng)前類的Class對象,也可以用ParseProperties.class.getClass()方法來實現(xiàn)同樣的效果。之后調(diào)用其getResourceAsStream("database.properties")方法來解析配置文件。getResourceAsStream()方法解析文件時候的相對路徑是當(dāng)前類的包路徑。
就當(dāng)前的包來說,zy.pro.sc.db對應(yīng)的路徑是src/zy/pro/sc/db。由于我們要解析的文件和解析類在同一目錄下,所以我們的路徑是"database.properties"。
如果我們的解析文件和解析類不在同一目錄下呢,如以下目錄結(jié)構(gòu):

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

圖二

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

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

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

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

如上圖所示,這個HashMap中的內(nèi)容主要有menuTreeNodes為key,value為ArrayList的對象和以czrydminfo為key,value為HashMap對象的數(shù)據(jù)。
圖七

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

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

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

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

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

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

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

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

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

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

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

如圖十八所示,在6月7日周二早上,大約8:30左右,Web應(yīng)用服務(wù)器作了一次Full GC,用了10秒的時間,把內(nèi)存回收到了10%的位置,為后續(xù)的使用騰出了90%的內(nèi)存空間。內(nèi)存回收仍然比較徹底,說明基本沒有內(nèi)存泄漏問題。
經(jīng)過這幾天的監(jiān)控分析,我們可以看出:
- Web應(yīng)用服務(wù)器的內(nèi)存使用已經(jīng)比較合理,內(nèi)存在工作日的占用在20%至30%之間,約1G的內(nèi)存占用,有2G的內(nèi)存空間富裕;而在空閑時間(周日,每天的凌晨等)內(nèi)存可以回收到10%,有90%的內(nèi)存空間富裕;
- Web應(yīng)用服務(wù)器的Full GC的次數(shù)明顯減少了并且每次Full GC占用的時間也很少,基本控制在10-20秒之間,有的甚至在10秒以內(nèi),明顯改善了內(nèi)網(wǎng)應(yīng)用服務(wù)器內(nèi)存的使用;
- 從6月2日重新部署之后,Web應(yīng)用服務(wù)器沒有出現(xiàn)宕機(jī)重啟的現(xiàn)象。
總結(jié)
通過本文,我們可以看到,內(nèi)存的泄露將會導(dǎo)致服務(wù)器的宕機(jī),系統(tǒng)性能就更別說了。對于系統(tǒng)內(nèi)存泄露問題應(yīng)該從服務(wù)器GC日志方面進(jìn)行早診斷,使用工具早確認(rèn)并提出解決方案,排除內(nèi)存泄露問題,提高系統(tǒng)性能,以規(guī)避項目風(fēng)險。
Sample1,利用Menifest文件讀取jar中的文件
/*
1.文件目錄
test--
--a.text
--b.gif
2. Menifest文件內(nèi)容:
Manifest-Version: 1.0
abc: test/a.txt
iconname: test/Anya.jpg
注意:manifest.mf文件最后一行要打一回車
Another Notification:
如果manifest文件內(nèi)容是:
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包相對的路徑,發(fā)布時把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中 文件的內(nèi)容
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();
}
}