Java基礎知識總結(一)創建和銷燬對象

契子:明年就要離開學校找工作了,時間過的真快,想一想這幾年,做了一些事,也有一些事並沒有做好,有很多收穫,也有不少遺憾。感性的話在此不宜多說,既然選擇了程序員這條道路,也要有把它到做事業的態度。在正式找工作前還有幾個月的時間,做東西,嘗試新的技術固然很爽,但是基礎也很重要,在這短短的幾個月的時間裏,我將把以前學過的一些知識,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 使用靜態工廠方法代替構造器:

優點:
(1)在具有比較複雜參數的構造器的時候,使用這很難通過重載版本來區別它們之間的功能差別,而使用靜態工廠方法可以根據功能命名,像Executors中創建不同功能的線程池實例一樣,靜態工廠方法掩蓋了構造器的複雜性;
(2)不必在每次調用它們的時候都創建一個新對象,靜態工廠方法可以用於單例,享元模式,不可變類(final class,final field),不可實例化類(private 構造器)這些不同的場景;
(3)返回原返回類型的任何子類型:這一點影響覺得深遠,廣大,首先,在collection包中有Collection,List,Set,Map,Iterator等接口,Collections工具類,提供了很多具有附加功能的集合類實現,而它們都是定義在Collections中的嵌套類,通過靜態工廠方法返回,還有Iterator,也是基於內部類實現的,通過它來返回,靜態工廠方法可以隱藏具體的實現,支持面向接口的編程
在開發J2EE項目時,經常用到Java Persistence API,websocket API,servlet API等等,它們是J2EE規範的一部分,我們僅僅引用了API,接口,而具體的實現我們可以會用到hibernate,spring的子項目,像servlet和websokcet API,它們的具體實現則多由J2EE 應用服務器實現它們,另外tomcat8.0也提供了websocket的實現。
這就是“服務提供者框架(service provider framework)”,提供者提供Service接口的具體實現,提供者可以使用提供者註冊接口註冊自己,提供者也可以實現provider接口或者通過類名直接註冊,客戶端使用者通過服務訪問接口(其實就是靜態工廠方法);
(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可達性分析機制決定的;



發佈了40 篇原創文章 · 獲贊 10 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章