引言
Java語言自90年代出現以來,由於它的安全性和跨平臺性(即所謂的」Write Once,Run Anywhere」)等特色,深得廣大程序員的青睞,可是同時,Java程序的運行效率問題也是程序員的心病。Java是介於解釋型和編譯型之間的一種語 言,一樣的程序,假如用編譯型語言C來實現,其運行速度通常要比Java快一倍以上。怎樣提升java應用程序的效率是廣大程序員關心問題。本文將從與 Java字節碼的運行過程當中影響性能的相關因素的分析入手,而後,探討一些在Java代碼的設計過程當中具體的有助於提升性能的策略。
1、性能分析
JVM運行時的負載主要集中在字節碼的執行,內存治理,線程治理和其餘的操做幾個方面。
1.1 JVM的結構
JVM中運行的是Java字節碼(Bytecode).class文件,這種class文件除了準肯定義一個類或接口的表示外,還定義了一些與平臺相關的諸如字節順序的具體信息。
Java的數據類型分爲primitive和reference,對於不一樣的數據類型的運算在JVM中的有不一樣的指令去執行,好比 iadd,ladd,fadd就是分別針對int,long,float的加法運算,固然,它們的執行效率也不同, 運行時的數據區,在一個程序運行時,JVM都要爲它定義不一樣的運行數據區,有些數據區在JVM啓動時就建立好了,直到整個JVM退出時才釋放掉,還有一些 數據區的是屬於每一個線程的,它的生命週期與線程相等。
JVM中的邏輯結構有:
PC(program counter)寄存器 ,每一個線程有本身的PC(program counter)寄存器,當JVM執行的方法不是本地(Native)的時,這裏存放當前線程運行的指令的地址,假如是本地(Native)的,PC(program counter)寄存器的值沒有定義。
JVM棧(stack) ,當建立線程時,每一個線程都建立一個屬於本身的棧,用來存放frames(見下面),它存有本地變量,方法調用中的部分結果。
堆(heap) ,JVM中全部線程共享這個堆,類的實例和數組都是從堆中分配內存的,堆是在整個JVM啓動時初始化的。
方法區(Method Area) ,線程間共享,它存放每一個類中的運行時常數池(runtime constant pool),域值和方法數據,以及方法和類的構造函數的代碼,其中包括用於類的非凡方法,實例初始化和接口類型的初始化,
運行時常數池(runtime constant pool) ,是每一個類或接口的class文件中的常數池表在運行時的表示,它包括各類常數如編譯時就知道的數字常量,還有運行時才能肯定的方法和域的引用,相似傳統語言的符號表,
本地(Native )方法棧(Stack) ,用來支持本地(Native)方法調用,這些方法用非Java的語言編寫,須要傳統的"C"棧。
幀(Frames) ,存放方法調用中的數據和部分結果及返回值,執行動態鏈接,分派例外,一個新的Frame在方法被調用時建立,方法調用正常或非正常完成時銷 毀,Frame從每一個線程建立的JVM的棧中分配內存,它屬於每一個線程,每一個Frame有本身的本地變量組,本身的操做棧(Operand Stack)和指向當前方法的運行時常數池的引用,本地變量組和操做棧的大小在編譯的時候就已經肯定,在一個得到控制的線程中只有一個Frame是激活 的,這個Frame爲當前Frame,它的方法爲當前方法,方法所屬的類爲當前類,當這個方法又調用別的方法或結束時,這個當前Frame再也不激活,一個 新的Frame被建立併成爲當前Frame,直到當前方法調用完成後,這個Frame被釋放並返回結果,前一個方法的Frame成爲當前的 Frame,
本地變量 ,每一個方法的Frame包含一組在方法中定義的本地變量,它們的大小在Java編譯時就已肯定。
動態鏈接(Dynamic Linking) ,每一個Frame包含一個指向當前方法的運行時常數池的引用,它經過符號引用(symbolic references)訪問變量和指向被引用的方法,動態鏈接(Dynamic Linking)在運行時將這些方法的符號引用轉爲具體的方法引用,並加載相應的類,它還將變量影射到當前運行時的變量的內存偏移上。
1.2 字節碼(Bytecode)的執行
JVM動態地加載(Loads),鏈接(Links)和初始化(Initializes)類和接口的字節碼,加載(Loading)就是JVM發現具備某 一特定名字的類或接口的二進制表示,並從這個二進制表示在內存中建立出一個類或接口,鏈接(Linking)就是使一個類或接口與JVM的運行時狀態很好 的結合,以便執行它,一個類或接口的初始化就是執行它的初始化方法。
1.3 內存治理
Java是一個面向對象的語言,所以,在JVM的內存中大部分是對象,從上面的分析咱們知道,對象的內存是從堆(heap)分配的,對象內存的回收是由自 動內存治理系統(由叫垃圾收集器-Garbage Collector)來完成的,編程人員是不用顯式的釋放內存的,垃圾收集器Garbage Collector經過記錄指向對象的引用的數目來決定是否釋放對象所佔據的內存空間,當指向某個對象的引用數爲零時,這個對象就能夠釋放了。
1.4 線程治理
Java是一個支持多線程的語言,所以線程的治理是JVM的一個主要工做,每一個線程都有本身的工做內存,線程間的共享變量是存放在整個JVM的主內存中 的,線程間數據的同步經過lock來共享數據並保證數據的一致性,線程間控制的轉移經過對wait,notify等方法的調用來實現。
2、性能設計
經過以上的分析,咱們就如下幾個方面提出一些有關性能設計的策略。
2.1 對象的構造
從上面咱們知道,Java對象的內存是自動治理的,所以,通常認爲,程序員是不用擔憂內存的分配的,但這種想法是不徹底正確的,java經過垃圾收集器 (Garbage Collector)來處理內存分配與釋放的底層操做,程序員不用直接治理內存,這樣防止了因爲內存的錯誤操做致使的數據破壞(corruption), 但並不意味着程序員不用擔憂內存的使用,內存的使用不但會給系統帶來很大的負擔,好比,Java並不阻止程序佔用過多的內存,當對象向堆所請求的內存不足 時,垃圾收集器(Garbage Collector)就會自動啓動,釋放那些引用數爲零的對象所佔用的內存,Java也不會自動釋放無用的對象的引用,假如程序忘記釋放指向對象的引用, 則程序運行時的內存隨着時間的推移而增長,發生所謂內存泄漏(memory leaks),建立對象不但消耗CPU的時間和內存,同時,爲釋放對象內存JVM需不停地啓動垃圾收集器(Garbage Collector),這也會消耗大量的CPU時間。
策略:儘可能避免在被常常調用的代碼中建立對象。
對於集合類(collection),應儘可能初始化它的大小,假如不初始化它的大小,JVM自動給它一個缺省的大小,當你的要求大於這個缺省的大小時,JVM就會從新建立一個新的collection對象,原來的對象就釋放掉,這樣必然會增長JVM的負擔。
當一個類的多個實例在其本地的變量裏訪問一個特定的對象時,最好將這個變量設計爲靜態(static)的,而不是每一個實例中變量裏都存放那個對象的引用。
由於對象的建立是很是昂貴的,因此應儘可能重用,少用new來得到對象的引用,儘可能重用容器對象(Vector,Hashtable)等而不老是建立新的對象拋棄舊的對象,但必定要注重釋放容器對象中所保存的指向別的對象的引用。
儘可能使用primitive數據類型。
當只是訪問一個類的某個方法時,不要建立該類的對象,而是將該方法設計成一個static的方法。
儘可能簡化類的繼續關係和設計簡單的構造函數。
建立簡單數據類型的數組要比初始化一個這樣的數組快,建立一個複雜類型的數組要比克隆一個這樣的數組快。
2.2 字符串(String)
String在Java程序中被普遍使用,String對象是不可改變的,例如: String str="testing"; str=str+"string"; 這個"testing"String一旦建立,就不能更改,但指向這個String的引用str能夠改變,str原來指向"testing",通過第二個 運算後,改成指向新的String"testingstring"了。針對String的這個特性,對於String的使用,咱們有以下策略:
假如字符串在程序中可能被改變,好比增長,接或刪除字符,就應使用StringBuffer,建立具備初始大小的StringBuffer對象,儘可能重用該對象,而不使用"+"操做。
當咱們要分析字符串中的字符時,就不要使用String或StringBuffer,而是使用字符(cbar)數組,別是在循環中分析字符時,更應如此。
儘可能少用StringTokenizer,它的方法的性能比較差。
2.3 輸入輸出(Input/Output)
程序的I/O每每是性能的瓶頸所在,java io定義了兩個基本的抽象類:InputStream和OutputStream,對於不一樣的數據類型好比磁盤,網絡又提供了不一樣的實現,java.io 也提供了一些緩衝流(Buffered Stream),使硬盤能夠很快的讀寫一大塊的數據, 而Java基本的I/O類一次只能讀寫一個字節,但緩衝流(Buffered Stream)能夠一次讀寫一批數據,,緩衝流(Buffered Stream)大大提升了I/O的性能,對象的序列化(serialization)是一個將處於生成期的對象序列化成能夠在流(stream)中讀寫的 數據的過程,象的序列化是一個很是複雜,昂貴的過程,要一個類implements接口 java io Serializable,它就能夠被自動的序列化,針對以上分析,咱們對I/O有以下對策:
·小塊小塊的讀寫數據會很是慢,所以,儘可能大塊的讀寫數據
·使用BufferedInputStream和BufferedOutputStream來批處理數據以提升性能
·對象的序列化(serialization)很是影響I/O的性能,儘可能少用
·對不需序列化的類的域使用transient要害字,以減小序列化的數據量
2.4 循環(Loop)
由於循環中的代碼會被反覆的執行,因此循環中常常是尋找有關性能問題的地方,嵌套的循環更輕易產生性能問題, 在循環中,咱們應該注重以下問題:
·循環常量(Loop Constant),在循環中它的值不會改變,所以,它的值應該在循環外先計算出來。
·本地變量(Local Variable),從上面的分析可知,在方法中使用本地變量比使用對象的屬性消耗較少的資源,在循環中卻不同, 由於循環中的代碼要反覆地被運行,所以,儘可能少地在循環中建立對象和變量。
·儘早結束循環,假如循環體在知足必定條件就能夠結束,就應儘快結束。
2.5 集合類(Collections)
集合類在此Java編程中被普遍地使用,大體上,一個集合類就是將一組對象組裝成一個對象,Java的集合類框架由一些接口和一些爲通用目的而實現 (implementation)的類組成,集合類的基本結構由六個在java.util包內的接口組成,主要有以下結構:
Collection 這是集合類的基本接口,它爲一組對象提供了一些簡單的方法,
List 具備能夠控制的順序,但並無定義或限制按什麼排序。
Set 不能包含重複的元素,
Map 將一個鍵(Key)影射到一個值(Value),不答應有重複的鍵,
除了上述接口以外,java.util還提供了一些爲通用目的而實現的類,如Vector,ArrayList,Hashtable等等,這些類裏,有些 提供了某種排序算法,有的提供了同步的方法,有如此多的集合類,在具體使用過程當中,咱們如何根據本身的須要選擇合適的集合類,將對程序的性能產生很大的影 響,下面將一些經常使用的類進行比較, Vector和ArrayList Vector和ArrayList在使用上很是類似,均可用來表示一組數量可變的對象應用的集合,而且能夠隨機地訪問其中的元素。
它們的區別以下:
Vector的方法都是同步的(Synchronized),是線程安全的(thread-safe),而ArrayList的方法不是,因爲線程的同步必然要影響性能,所以,ArrayList的性能比Vector好。
當Vector或ArrayList中的元素超過它的初始大小時,Vector會將它的容量翻倍,而ArrayList只增長50%的大小,這樣,ArrayList就有利於節約內存空間。
Hashtable和HashMap
它們的性能方面的比較相似 Vector和ArrayList,好比Hashtable的方法是同步的,而HashMap的不是。
ArrayList和LinkedList
對於處理一列數據項,Java提供了兩個類ArrayList和LinkedList,ArrayList的內部實現是基於內部數組Object[],所 以從概念上講,它更象數組,但LinkedList的內部實現是基於一組鏈接的記錄,因此,它更象一個鏈表結構,因此,它們在性能上有很大的差異。
(1)從上面的分析可知,在ArrayList的前面或中間插入數據時,你必須將其後的全部數據相應的後移,這樣必然要花費較多時間,因此,當你的操做是 在一列數據的後面添加數據而不是在前面或中間,而且須要隨機地訪問其中的元素時,使用ArrayList會提供比較好的性能。
(2)而訪問鏈表中的某個元素時,就必須從鏈表的一端開始沿着鏈接方向一個一個元素地去查找,直到找到所需的元素爲止,因此,當你的操做是在一列數據的前面或中間添加或刪除數據,而且按照順序訪問其中的元素時,就應該使用LinkedList了。
(3)假如在編程中,1,2兩種情形交替出現,這時,你能夠考慮使用List這樣的通用接口,而不用關心具體的實現,在具體的情形下,它的性能由具體的實現來保證。
設置集合類的初始大小
在Java集合框架中的大部分類的大小是能夠隨着元素個數的增長而相應的增長的,咱們彷佛不用關心它的初始大小,但假如咱們考慮類的性能問題時,就必定要 考慮儘量地設置好集合對象的初始大小,這將大大提升代碼的性能,好比,Hashtable缺省的初始大小爲101,載入因子爲0.75,即假如其中的元 素個數超過75個,它就必須增長大小並從新組織元素,因此,假如你知道在建立一個新的Hashtable對象時就知道元素的確切數目如爲110,那麼,就 應將其初始大小設爲110/0.75=148,這樣,就能夠避免從新組織內存並增長大小。
2.6 方法(Methods) 從上面的JVM的結構分析能夠看出,Java程序在執行的過程當中就是一個初始化對象和調用其方法過程,其中對方法的調用花費了不少資源,這些資源都用來轉移線程控制,傳遞參數,返回結果和建立用於存放本地變量及中間結果的幀棧(stack frame)。 代碼嵌入(Inlining) 因爲方法的調用須要消耗大量的資源,所以,Java編譯器能夠將一些方法調用轉化爲代碼嵌入(Inlining),就是將一段代碼對一個方法的調用轉化爲 將該方法的代碼在編譯時嵌入到調用處,這樣,因爲減小了方法的調用,就能夠大大提升代碼的性能,當將一個方法聲明爲 final,static,private時,編譯器就會自動的使用代碼嵌入技術將該方法代碼在編譯時嵌入到調用處。 同步(Synchronized)方法 在多線程訪問共享數據時,爲了保證數據的一致性,就必然要使用同步技術,但從上面的分析可知,使用同步方法比使用非同步方法的性能要低,所以,咱們應儘可能少使用同步方法?調用同步方法的代碼自己就不須要再同步了。