契子:明年就要離開學校找工作了,時間過的真快,想一想這幾年,做了一些事,也有一些事並沒有做好,有很多收穫,也有不少遺憾。感性的話在此不宜多說,既然選擇了程式員這條道路,也要有把它到做事業的態度。在正式找工作前還有幾個月的時間,做東西,嘗試新的技術固然很爽,但是基礎也很重要,在這短短的幾個月的時間裡,我將把以前學過的一些知識,Java,資料結構,演算法,網路,OS&Linux,J2EE等等知識查缺補漏,好好梳理一遍,不光是為了找工作,也是一種必須要堅持的態度。
對於Java知識的整理,基於《Effetive Java》2nd和《Java編程思想》4th輔以JVM和設計模式的相關知識,結合書本上的知識和我的理解進行整理。好了,開始我的一篇——建立和銷毀對象。
1. Java中的構造器:
構造器是一種特殊類型的方法,它和類同名,沒有傳回型別,和new關鍵字結合可以返回對象執行個體的引用。TIJ中說它是一種靜態方法,但是通過位元組碼我們可以看到其實並沒有static關鍵字,它的行為也和其他靜態方法有異(可以訪問非靜態成員變數),因此這種說法並不完全準確,這裡不再深究。 1.1 定義構造器:
一個類可以有多個構造器,如果你沒有定義構造器,Java編譯器會在語義分析的階段,首先添加一個預設構造器。
多個構造器可以通過方法重載(overload)實現,注意只有同方法名和不同參數列表可以區別不同的重載版本,傳回型別並不能區分。
尤其是使用基本型別參數重載時,要注意類型的自動轉換如(char—>int,小轉大)和窄化轉換(強制類型轉換,大轉小),當然會使用最匹配的類型。 1.2 this關鍵字:
通過this指標我們可以訪問類的執行個體變數和方法,但最好是在必要的時候(需要返回或使用該執行個體,內部類訪問外部類同名執行個體變數方法,構造器設定屬性等)使用它,否則你不必添加它,編譯同樣會幫你添加。
在存在多個重載版本的構造器時我們可以在構造器內使用this調用其他構造器,可以避免一些重複的代碼:
public ConstructorTest(int a) { this.a = a; } public ConstructorTest(int a, String s) { this(a); this.s = s; }
PS:在構造器存在很多參數情況下,重疊構造器是一種選擇,但是更好的做法是使用Builder模式,後面會講到。 1.3 static關鍵字:
static(靜態),static方法和static變數是類方法和類變數,它們不能使用this引用,都放在方法區中,供各個線程共用。static變數初始化和static初始化其,會在類載入(隱式載入或顯示載入)後執行一次。
2. 清理,終結對象(finalize)/記憶體回收(CG): 這涉及到很多內容。Java提供了記憶體回收行程,但記憶體流失可能以很隱秘的方式發生(使用引用數組時),同時對於對象中可能使用的一些資源必須在對象不再使用時進行釋放(Connection,FileInputStream等)。
首先對象執行個體作為類的副本存放在Java堆中,在JVM中,一般使用可達性分析進行記憶體回收,也就是說,如果順著引用追溯的話,“活”的對象應該可以到達CG Root(包括,靜態變數,常量引用,棧中的本地變數表以及本地方法棧JNI native方法中的引用)。記憶體回收行程會對不可達對象進行標記,在堆的不同地區使用不同的方法進行回收。
標記-清除:如果只有很少的垃圾的話,它很快,而且簡單,但是如果垃圾很多的話,會產生大量的片段; 複製:我們可以將需要進行記憶體回收的記憶體地區分為2個部分,比如A和B,需要CG時,將A的存活的對象直接複製到B(之前為空白)中,清空A就可以了,不需要考慮片段的問題,實際上在JVM(Hotspot)中分成了3個部分,一般比例可以為8:1:1,它們分別命名為eden,surivor1,surivor2,因為據統計Java程式中95%以上的對象很快就不再使用,因此eden很大,surivor可以較小(存活的對象少)。這實際上多用與新生代的記憶體回收。 標記-整理:有新生代當然也有老年代了,與新生代不同,老年代的對象相對穩定的多,記憶體回收很少,畢竟是經過了minor CG洗禮的不會那麼容易掛掉,開個玩笑。標記-整理與清除的不同的地方在,它並不是直接在原位置清除掉,而是將存活的對象移向一端,之後直接就可以一起清除掉掛了的對象。因為我們也說了老年代的對象回收的少,因此移動的也相對較少。這樣就不會有大多的片段了。
因此我們可以看到,JVM多採用分代回收的方式,對於不同的情況分而治之。
釋放資源,終結和記憶體回收有什麼關係: 首先, 記憶體回收只和記憶體的使用狀況有關,當記憶體不足(或滿足我們設定的條件)時,才會進行CG。 finalize()是什麼時候執行的呢,對於那些不可達的對象,到它們真正被回收 至少需要經過兩次標記階段: (1)首先篩選那些不需要執行finalize方法的對象,沒有override finalize方法的和已經執行過finalize方法的對象,那它們就可以“等死”了,對於finalize尚未執行的對象,它們進入F-Queue隊列,相當於是“死緩”,還有一線生機; (2)F-Queue隊列中的,有一個終結線程專門去調用這些對象的finalize方法(所以finalize方法是一個回調方法),如果在finalize方法有和CG Roots有了關聯,OK,它活了,否則“等死”去。
因此,我們看到 finalize方法依賴直接於記憶體回收和終結線程,終結線程的優先順序很低,這代表它可能很長時間都得不到執行,而記憶體回收也是你無法直接控制的(System.gc和System.runFinalize也是要看JVM臉的),所以 finalize和C++中的解構函式並不是一回事;
而對於資料庫連接、檔案訪問控制代碼等等佔用資料庫資源和系統資源的對象, 我們必須及時的釋放/關閉它們。你可以定義一個close方法,在try-finally中保證必要的關閉得到執行,Java中甚至有Closable介面,FileInputStream,Connection等都實現了它們。
Finalize方法到底有什麼用: (1)你可以在finalize方法檢查close方法是否已經執行,這時一種安全敏感的做法,FileInputStream,Connection,Timer都是這樣做的。 (2)使用JNI時,如果本機物件中要釋放敏感資源,需要顯示override finalize方法,進行釋放。 (3)可以在finalize方法中拯救自己。
如果你要使用它,注意在繼承體系中,要我們手動維持“終止方法鏈”,這和構造器方法是一樣的道理。 總的來說除此以外盡量不要使用finalize方法。
3. 初始化:
如果想真正弄清楚對象初始化,而不是僅僅記住一些像成員變數的初始值這樣的規則,我覺得應該瞭解一個類在第一個建立對象時是如何從位元組碼編程的可用的對象的。
在第一次使用一個類的時候,無論是顯示載入一個類(Class.forName等)還是隱式載入一個類(A.staticVariable,new A())時,首先要有ClassLoader進行載入:
(1)ClassLoader首先通過類名定位到類檔案的位置(通過classpath等),將位元組碼載入到記憶體,通過準備、位元組碼驗證和resolve等環節將等到一個個Class對象,放到方法區中;
(2)在此之後就是類初始化,這是類中的靜態變數和靜態初始化器將按照位置順序進行初始化工作,靜態變數同樣放在方法區中;
(3)如果你進行是執行個體建立的化,接下來的工作首先是在堆上分配記憶體了,具體的方法可能有指標碰撞和空閑列表;
(4)獲得了記憶體空間後,首先全部置零,這也就是為什麼類的成員變數會還有初始值的原因,之後如果指定了初始化值,同樣這裡也是按順序進行的;
(5)最後將執行<init>也就是我們定義使用的構造器來進行我們自訂的初始化過程了,這裡就可以獲得我們想要的對象執行個體的引用了。
所以在類中,各個部分的初始化順序是:靜態變數,靜態初始化器(按位置順序)——>非靜態成員變數(按位置順序)——>構造器;
說完了基本過程,我們來看看在Java中一些具體的類型是怎樣進行初始化的。
3.1 數組初始化: 在Java中數組同樣也是一種對象,但它並不是由某個類執行個體化而來,而是有JVM直接建立的,它的父類是Object,因此你可以在數組上使用Object的方法。 首先來複習下基本的文法: 通過數組初始化器:int[] a = {12,3}; 通過new動態建立:int[] a = new int[5];
對於記憶體回收來說,數組同時也是一種特殊的類型,看下面的例子:
public class MStack { private static final int DEFAULT_SIZE = 20; private Object[] elements = new Object[DEFAULT_SIZE]; private int size = 0; public MStack() { elements = new Object[DEFAULT_SIZE]; } public void push(Object element) { ensureCapacity(); elements[size++] = element; } public Object pop() { if(size == 0) { throw new RuntimeException("empty stack cannot pop"); } return elements[--size]; } public void ensureCapacity() { if(size == elements.length) { elements = Arrays.copyOf(elements, 2 * size + 1); } }}
這是《Effective Java》的一個例子,該例中的Stack在pop是並沒有將已經出棧的引用置為null;這些引用是“到期引用”,這些引用雖然沒有被使用,但是它將隨著Arrays.copy一起被複製到更大的數組中,對於JVM來說它們同樣是存活的對象,但是對我們的應用程式來說這些是無用的。在一個需要長期啟動並執行服務中如果出現這樣的問題很容易導致OOM。
3.2 可變參數列表: JDK1.5的特性,它和數組息息相關。實際上,可變參數列表實際還是通過數組來傳遞一組參數的,我覺得可以看作是一種文法糖。 使用可變參數列表時,如果有多個重載版本,會根據所傳遞的參數類型執行最匹配的版本,但是需要注意一些會產生“衝突”的情況:
public class VarArgsInit { //overload with var argument public static void f(Long...longs) { System.out.println("f_long_varArgs"); } public static void f(Character...characters) { System.out.println("f_character_varArgs"); } public static void f(float f, Character...characters) { System.out.println("f_float_character_varArgs"); } public static void g(float f, Character...characters) { System.out.println("g_float_character_varArgs"); } public static void g(char c, Character...characters) { System.out.println("g_char_character_varArgs"); } public static void main(String[] args) {// f(); //Error:(19, 9) java: reference to f is ambiguous// f(); f(1, 'a'); //OK// f('a', 'b'); //Error:(19, 9) java: reference to f is ambiguous g('a', 'b'); //OK }
這個例子中,f('a','b')會引起編譯錯誤,因為它會同時匹配第3個和第2個f()版本(因為'a'可以轉換成float),解決方案,很簡單g方法的兩個版本就不會有這種衝突。
3.3 枚舉: JDK1.5的添加特性。enum也是類,它派生自Enum抽象類別,但是與普通的類不同的時,編譯器會給它添加一些特性,我覺得可以認為enum是一種具有特殊功能的class: 我們來看看一個枚舉類型的位元組碼:
final enum hr.test.Color { // 所有的枚舉值都是類靜態常量 public static final enum hr.test.Color RED; public static final enum hr.test.Color BLUE; public static final enum hr.test.Color BLACK; public static final enum hr.test.Color YELLOW; public static final enum hr.test.Color GREEN; private static final synthetic hr.test.Color[] ENUM$VALUES; // 初始化過程,對枚舉類的所有枚舉值對象進行第一次初始化 static { 0 new hr.test.Color [1] 3 dup 4 ldc <String "RED"> [16] //把枚舉值字串"RED"壓入運算元棧 6 iconst_0 // 把整型值0壓入運算元棧 7 invokespecial hr.test.Color(java.lang.String, int) [17] //調用Color類的私人構造器建立Color對象RED 10 putstatic hr.test.Color.RED : hr.test.Color [21] //將枚舉對象賦給Color的靜態常量RED。 ......... 枚舉對象BLUE等與上同 102 return}; // 私人構造器,外部不可能動態建立一個枚舉類對象(也就是不可能動態建立一個枚舉值)。 private Color(java.lang.String arg0, int arg1){ // 調用父類Enum的受保護構造器建立一個枚舉對象 3 invokespecial java.lang.Enum(java.lang.String, int) [38]}; public static hr.test.Color[] values(); public static hr.test.Color valueOf(java.lang.String arg0);}
從位元組碼解析中,首先可以看到: (1)它是final的,因此我們無法繼承它; (2)所有枚舉值,都是Color的執行個體,它們都是public static final的; 我們還有看到,編譯器為enum添加了3個方法: (1)私人構造器,保證無法從動態建立一個該類型的枚舉對象;同時我們也無法使用反射建立一個enum類型執行個體:
public enum MEnum { E1; static class A { private A(){ } } public static void main(String[] args) throws Exception { Class<A> a = A.class; Constructor constructor = a.getDeclaredConstructor(); constructor.setAccessible(true); constructor.newInstance(); Class<?> ec = MEnum.class; Constructor constructor1 = ec.getDeclaredConstructor(String.class, int.class); constructor1.setAccessible(true); constructor1.newInstance("YJH", 2); }}
結果:A類可以正常建立,而enum類型,java.lang.IllegalArgumentException: Cannot reflectively create enum objects,因為在class.newInstance中有這樣的檢查:
if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
(2)values靜態方法; (3)valueOf(String)靜態方法:它們都是編譯器為具體的enum類型添加的,你在Enum抽象類別中看不到它們;
enum可以說是嚴格全域不可修改的安全類型,它同樣可以進行安全的序列化而不用擔心不唯一的情況,正因為如此,使用單元素枚舉建立單例對象是一種極佳的方法,同時可以不用擔心反射攻擊。
PS:enum可以和switch結合使用,十分方便;
4. 建立和銷毀對象的實踐: 《Effetive Java》中對於這一塊給出了一些優秀的建議,以後在每一篇終結之後我都附上關於這篇的好的實踐模式和要注意的反模式。學習這些思想和設計模式我覺得對我使用和理解Java中不同模組以及Spring等架構有很大的好處,因為它們都是基於這些的思想和模式建立的,能夠協助我更好的理解它們的結構和功能。
4.1 使用靜態Factory 方法代替構造器: 優點: (1)在具有比較複雜參數的構造器的時候,使用這很難通過重載版本來區別它們之間的功能差別,而使用靜態Factory 方法可以根據功能命名,像Executors中建立不同功能的線程池執行個體一樣,靜態Factory 方法掩蓋了構造器的複雜性; (2)不必在每次調用它們的時候都建立一個新對象,靜態Factory 方法可以用於單例,享元模式,不可變類(final class,final field),不可執行個體化類(private 構造器)這些不同的情境; (3)返回原傳回型別的任何子類型:這一點影響覺得深遠,廣大,首先,在collection包中有Collection,List,Set,Map,Iterator等介面,Collections工具類,提供了很多具有附加功能的集合類實現,而它們都是定義在Collections中的嵌套類,通過靜態Factory 方法返回,還有Iterator,也是基於內部類實現的,通過它來返回,
靜態Factory 方法可以隱藏具體的實現,支援面向介面的編程。 在開發J2EE項目時,經常用到Java Persistence API,websocket API,servlet API等等,它們是J2EE規範的一部分,我們僅僅引用了API,介面,而具體的實現我們可以會用到hibernate,spring的子項目,像servlet和websokcet API,它們的具體實現則多由J2EE 應用伺服器實現它們,另外tomcat8.0也提供了websocket的實現。 這就是“
服務提供者架構(service provider framework)”,提供者提供
Service介面的具體實現,提供者可以使用
提供者註冊介面註冊自己,提供者也可以實現
provider介面或者通過類名直接註冊,用戶端使用者通過
服務提供者(其實就是靜態Factory 方法); (4)本來是用來簡化有泛型參數時對象建立的,不過有了diomand運算式,java已經可以自己推導類型了;
4.2 多個構造器參數時使用Builder模式: 重疊構造器和Bean+setter建立的方式真的不好維護,寫過都知道,builder模式不僅僅可以靈活的組配參數;還可以建立不可變的對象。
public final class HasBuilder { private final int i1; private final int i2; private final String s1; public HasBuilder(Builder builder) { this.i1 = builder.i1; this.i2 = builder.i2; this.s1 = builder.s1; } public static class Builder { private int i1; private int i2; private String s1; public Builder i1(int i1) { this.i1 = i1; return this; } public Builder i2(int i2) { this.i2 = i2; return this; } public Builder s1(String s1) { this.s1 = s1; return this; } public HasBuilder build() { return new HasBuilder(this); } }}
你可以通過在build構建在具體的設值方法裡進行約束檢查。
4.3 建立合適的單例: 大致總結一下,有5種不同的單例模式: (1)餓漢模式; (2)懶漢模式:消極式載入,這就涉及到了安全執行緒的問題,用synchronized方法關鍵字效率太低; (3)基於雙檢鎖的單例:JDK1.5是安全的,
需要通過volitale來保證可見度,一定要有手寫它的能力。; (4)基於靜態內部類的方式:讓靜態內部類持有一個static final的執行個體,因為是內部類,所以自然也就可以消極式載入;
public class SingletonWithInnerClass { private SingletonWithInnerClass() { System.out.println("initialized"); } private static class SingletonHolder { private static final SingletonWithInnerClass s = new SingletonWithInnerClass(); } public static SingletonWithInnerClass getInstance() { return SingletonHolder.s; } public static void main(String[] args) { Class c = SingletonWithInnerClass.class; //這裡並沒有進行初始化 System.out.println("start initialization:"); SingletonWithInnerClass singletonWithInnerClass = SingletonWithInnerClass.getInstance(); }}
這段代碼的輸出結果: start initialization:
initialized
可見是消極式載入的; (5)單元素枚舉的方法,前面已經討論過了,最佳,無償序列化,防止反射攻擊;
4.4 私人構造器防止執行個體化: 對於一些工具類或者存放全域變數來說,使用private構造器可以防止繼承/執行個體化,如果使用介面和抽象類別來實現,是一種反模式;
4.5 避免建立不必要的對象: (1)注意String是有常量池的,它實際上是通過private final char[]來存放的,所以它是不可變的,只有第一次使用這個字串組合的時候才進入常量池: new String("abc");實際上是有兩個字串對象,"abc"是編譯期存在,它已經進入常量池了; (2)對於Calendar這樣的執行個體化代價較大的對象考慮盡量複用; (3)使用自動裝箱類型一定要特別小心,以免在迴圈中因為自動裝箱而建立大量對象,能用基本類型就不要用裝箱類型; (4)小對象的建立和銷毀代價是很小的,因此,使用對象池的時候一定要考慮是不是值得,使用對象池管理不當也可能造成記憶體流失。
4.6 消除到期引用: (1)自己管理記憶體的時候:之前提到的MyStack(自己管理記憶體之外),還有兩個情形容易導致記憶體流失: (2)緩衝:不要讓緩衝的引用成為阻止記憶體回收的唯一原因,盡量使用weakHashMap,它不會影響引用,當然使用它需要注意,只有快取項目的生命週期依賴與它的外部參考時才可以使用它;常見的情況,使用一個後台線程Timer或者ScheduledTreadPoolExecutor或者添加新條目的時候檢查(LinkedHashMap提供了這樣的機制); (3)回調:這種基於觀察者模式的方式都需要監聽器或回調來註冊,因此如果不再合適的時候釋放也會造成泄漏,用弱引用是一種好的做法; 其實看看記憶體流失原因直接起來就是管理不當的引用池,這時由JVM可達性分析機制決定的;