1. 程式人生 > >02-方法傳參和初始化與垃圾回收清除

02-方法傳參和初始化與垃圾回收清除

例子 nal 成員 計數器 不用 override 內容 繼續 ava

1.方法參數傳值

1.1 方法傳參

方法參數分為三種:1,基本類型; 2,String類型;3,引用類型。 實例如下:
public  void changeParam(int i,String str,StringBuilder sb,StringBuilder sb2){
    i=1;
    str="1";
    sb.append("1");
    sb2=new StringBuilder("1");
}
@Test
public void change(){
    int i = 0;
    String str = "0";
    StringBuilder sb 
= new StringBuilder("0"), sb2 = new StringBuilder("0"); changeParam(i, str, sb,sb2); System.out.format("i:%-3d str:%-3s sb:%-3s sb2:%-3s", i, str, sb, sb2); } 結果: i:0 str:0 sb:01 sb2:0
執行change方法後,i依然是0,str依然是0,sb則變成了"01",原因如下:
  1. 基本類型方法傳參是以復制值的形式,即change中的i變量復制給changeParam的i,但實際上changeParam的i擁有change的i的值,但changeParam中的i無論怎麽變則不會改變change的i的值;
  2. String雖然是引用類型,但是String自己本身的特性是不能被改變,所以要改變String的值的話,則在java裏面是生成一個新的String,再把此值賦值給str,所以str這種參數和方傳參並沒有什麽關系,就算是在一個方法內也是如此。
  3. 引用類型參數情況分為2種:
    1. sb引用,在changeParam中sb修改屬性後,在change中的sb也一同跟著改變,原因是因為change方法和changeParam方法中的sb引用指向的是同一個對象,所以它們修改對象時,自然會在該對象的所有引用中體現。
    2. sb2雖然調用changeParam方法時也是傳遞的是StringBuilder的引用,但是在changeParam中sb2重新指向新的對象,所以此時changeParam和change中的引用指向的不再是同一個對象了,所以它們之間的改變是互不影響的。

1.2 可變參數可會發生的問題

在java中方法傳值使用不當可能會出問題,如可參數可能會發生的問題,先了解下方法的基本概念,在java中調用方法時,通過類型下的方法名+參數類型來區分調用的是哪個方法,如果同一個類下的方法名一樣,那麽方法參數和參數數量則就成了判斷方法的區別;調用時後臺實現方式是通過RTTI機制,關於RTTI以後會有筆記會將具體什麽情況。 可變參數方法是接收同一種類型參數但沒有固定具體數量,如xx(Integer ... args),Integer就是可以傳入多個,接收時以數組形式接收。如下:
//參數類型後面是3個省略後就是可變參數
public void changeParam(String ... args){
    System.out.println(Arrays.deepToString(args));
    System.out.println(args[0]);
}
@Test
public void changeParamTest(){
    changeParam("a");
    changeParam("a","b","c");
}
結果:
[a]
a
[a, b, c]
a
可變參數可能會出現的問題如下:
/**
 * 可變參數可能會發生的問題
 * 當changeParam方法參數一個是Integer args一個是Integer ... args,編譯器是通過的
 * 當程序調用changParam方法但只有一個參數,而我此時是需要調用的是帶有Integer ... args方法
 * 但是程序會認為是我調用的Integer args方法,並執行該方法
 * @param args
 */
public void changeParam(Integer args){
    System.out.println("Integer:"+args);
}
public void changeParam(Integer ...args){
    System.out.println("Integer...:"+args);
}
//解決方式
public void changeParam(Long type,Integer ...args){
    for (Integer val:args) {
        System.out.println("int:"+val);
    }
}

@Test
public void changeParamRun(){
    changeParam(1L,1);
}
本示例changeParam(Integer args)和changeParam(Integer ...args)方法是不同的方法,但原本要調用(Integer ...args)方法,但只傳1個值的情況下就會變成執行(Integer args)方法,這就是很嚴重的問題,執行的結果和目的方法不一致。最佳的解決方案是多設定參數,設定表示區分。如(Long type,Integer ...args),通過參數類型區分方法。

2.程序初始化流程

public class HelloA {
    public HelloA() {System.out.println("HelloA");}
    { 
        System.out.println("I‘m A class"); 
    }
    static { 
        System.out.println("static A"); 
    }
}
public class HelloB extends HelloA {
    public HelloB() {
        System.out.println("HelloB");
    }
    { 
        System.out.println("I‘m B class"); 
    }
    static {
        System.out.println("static B"); 
    }
}
public class Test {
    public static void main(String[]args){
        new HelloB();
    }
}
結果:
static A
static B
I‘m A class
HelloA
I‘m B class
HelloB
結果流程如下: 父類靜態區域(靜態變量和靜態塊內容)》》當前類的靜態區域(靜態變量和靜態塊內容)》》父類{}代碼塊和成員變量》》父類構造方法》》子類{}代碼塊和成員變量》》子類構造方法 靜態區域只執行初始化一次,無論創建多少對象都只執行一次。

3.java垃圾回收機制

程序員都了解初始化的重要性,同時也應該了解清理工作,java的一大優點就是幫助我們處理初始化和運行時給程序分配資源,並且在程序資源吃緊時清理資源,保證程序能夠健康的運行。慶幸的時這些事情java已經幫我們做了,這可是“幫了大忙",我們在剛上手java代碼時不需要考慮如何分配內存,如何清理內存,只需要關註我們自身的程序的內容即可。當然為了以後寫出更優秀的代碼。了解內存分配和清理內存還是很有必要的。

3.1 finalize方法的作用

垃圾回收器會回收通過new生成放在堆裏的對象,但對於一些特殊生成的對象資源始終都不會被清理,例如靜態區域之類,那麽可以通過finalize進行清理,finalize方法是垃圾回收器執行時先調用finalize方法,然後在下次垃圾回收進行垃圾回收。在finalize方法中我們可以將本來不會被清理的東西手動打上標記(例如靜態變量對象不會被垃圾回收器回收掉,可以在finalize中把此變量置為null),或者將一些強引用的對象設置為null,讓垃圾回收器再下次執行時把這些資源回收掉。 代碼 模擬實現方式如下:
class HandlerTest{
    static HandlerTest staticBl=new HandlerTest();//設置靜態變量
    boolean checkedOut=false;
    /**
     * 覆寫finalize方法
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        //控制清理條件,且清理內容
        if(checkedOut){
            System.out.println("執行清理");
            /**
             * java垃圾回收器只回收new出來的對象放在堆裏面,
             * 而靜態區域是不做處理,那麽可以通過finalize方法將其作為處理,
             * 並再下一次垃圾回收時回收
             */
            staticBl=null;
            super.finalize();
            flag=false;
        }
    }
    @Override
    public String toString() {
        return "staticBl";
    }
}
static boolean flag=true;
@Test
public void runGC(){
    while (flag){
        HandlerTest handler=new HandlerTest();
        handler.checkedOut=true;

        HandlerTest handler2=new HandlerTest();
        System.out.println(HandlerTest.staticBl);
        System.gc();
    }
}
結果:
staticBl
staticBl
staticBl
staticBl
staticBl
staticBl
執行清理
staticBl
執行清理
執行清理
執行清理
執行清理
執行清理
如上示例正常垃圾回收時都不會處理HandlerTest的staticBl變量,通過覆寫finalize()方法當垃圾回收時,使其為null,以便下次垃圾回收將其回收掉。但是finalize方法通過垃圾回收器調用,把一些正常不會被垃圾回收器回收掉且不再使用的對象放在finalize中讓其清理,但是垃圾回收器是在垃圾回收器執行的時候執行的,所以finalize執行時機是不確定的;因為java虛擬機未面臨內存耗盡的情況下,它是不會浪費時間去執行垃圾回收以恢復內存的。一般情況下,finalize方法盡量不用。

3.2 內存分配

生成對象就要為對象分配內存, java分配內存方式就是就像隊列形式,按照《java編程思想》說法是像一個傳送帶,每分配一個對象,它就會向前移動一個,java的分配內存的方式和速度要比C快的多,但並非完全像傳送帶,當內存資源將耗盡時,java垃圾回收器則幫助整理內存,假設以一共有10個空間,假設依次生成了8個,但是8個中間有3個空間已經不用了被標識為垃圾,那麽如果還繼續生成的話,只能再生成2個,那麽最多只有7個有效的(8-3+2),這3個已經是無用的空間了,空間越多浪費的空間越多。 那麽整理傳送帶的工作也是由java垃圾回收器實現的,目的是為了垃圾回收會將無用的回收掉,就是把上面那3個標識為垃圾的空間,並整理堆中對象使其緊湊排列,使其分配的內存更多,分配的效率更高。所以說垃圾回收器對於提高對象的創建速度,具有顯著的效果。

3.3 垃圾回收如何工作

垃圾回收最重要的是判斷哪些對象是垃圾對象(不再使用的對象),哪些對象是正在使用對象,把垃圾對象清理掉,把還在使用的對象保留下來。判斷依據就是對象釋放還被引用,如果對象沒有被引用綁定,則說明此對象已經沒有可能再被使用了,則就是垃圾對象。下面只是簡單說明下,java的垃圾回收器是非常復雜的功能,以後有機會再深入了解。
3.3.1 引用記數技術
java並不是使用引用記數技術,但可以簡單了解下,什麽是引用記數技術?一個對象可能被多個引用綁定,每綁定一個引用則引用計數器加1,對象的引用離開作用域或者被設置為null時引用減1,當為0時表示該對象已經沒有任何引用綁定了。等同於垃圾對象了。所以當記數值為0時,立即釋放對象。 雖然管理引用記數的開銷不大,但每次生成引用綁定對象都有執行,這種開銷在整個程序生命周期中將持續發生。但整個方式有個缺陷,如果對象之間存在循環引用,就可能出現“對象應該被回收,但引用計數卻不為零”,那麽就不能回收掉。
3.3.2 遍歷引用鏈條追蹤對象
思路是這樣的:任何“活”的對象,一定能夠追溯到其存活在堆棧或靜態存儲區之中的引用,因此,如果從堆棧和靜態存儲區開始,遍歷所有引用,就能找到其綁定的所有對象,例如找到某個引用綁定的對象後,再追蹤該對象包含的所有引用的對象。等同於樹狀的金字塔,從上面一層一層的遍歷,找到所有的“活的”對象。既然找到的都是活的對象,反過來沒有找到的就是死的對象,因此就可以被自動回收掉了
3.3.3 區分活的對象和死的對象後,垃圾回收器是如何清理的?
不同的虛擬機版本有多種處理方式,有一種做法叫做“停止-復制(stop-and-copy)” 停止-復制: 顧名思義,這種方式就是先暫停程序的運行,避免新的垃圾產生,也能避免意外問題,通過遍歷引用鏈條能找到所有的"活"的對象,就把所有存活的對象從當前堆復制到另一堆,當對象被復制到新堆時,它們時一個挨著一個,保持緊湊排列,並且修正原本的引用重新指向新堆裏面原本對應的對象地址。而沒有被復制的都是垃圾,一下子清理掉。 此方式的缺點如下
  1. 得有兩個完全隔離的堆,得在這兩個堆之間來回搗騰,即假設實際運行時需要100個空間,那麽還得預留此空間單純是用來搗騰,效率低下,
  2. 復制,當程序趨於穩定,沒有多少垃圾產生,甚至沒有垃圾。盡管如此,復制式回收器仍會將所有內存一處復制到另一處,假如有100個對象,其中有5個是垃圾對象,那麽還是得把95個移動到另一個堆中,只為了清除那5個垃圾。性能浪費
標記-清掃(mark-and-sweep):
為了避免復制式的少了量垃圾時的性能資源浪費,一些java虛擬機在這種沒有多少垃圾時切換另一種工作模式,稱為“標記-清掃(mark-and-sweep)”,一般情況下,標記-清掃方式速度很慢,但處理少量垃圾時速度很快。標記-清掃也是必須程序先暫停。 同樣是通過遍歷引用鏈條,遍歷時找到所有存活的對象,每找到一個對象,就給此對象設一個標記,所有標記工作完成的時候,開始清理工作,清理時過程中,沒有標記的需都被釋放,不會發生任何復制動作。所以剩下的堆空間不是連續的,垃圾回收器希望得到連續空間的話,就得重新整理剩下的對象。 總結 停止-復制和標記-清掃: 舉個很不恰當的例子,就比如你要打掃家裏的衛生時,你讓這個房間停止住人,如果房間垃圾比較多些,就把不是垃圾的東西搬到另一個房間裏面並規規矩矩的排列好,然後一股腦得把這個原房間垃圾全清理掉,等新房間垃圾也多的時候再搬回來,來回折騰。這就是"停止-復制", 而如果垃圾比較少,就用不著把不是垃圾的東西把到另一個房間了,因為總共才幾個垃圾,搬到其他房間那空垃圾就掃幹凈了,則就是把不是垃圾的和是垃圾的東西區分開,然後把垃圾清掃出去,這是“標記-清掃”,當然這個例子很不恰當。
3.3.4 自適應,分代的
堆內存是虛擬機管理內存的最大的一塊,垃圾回收器執行時都要遍歷堆內存的所有的對象,遍歷這些對象所花費的代價太大,嚴重影響GC的效率。內存分代就是為了這個目的。它的作用就是把這些對象分類,哪些不用每次都遍歷,哪些需要頻繁遍歷。 虛擬機把內存分配新生代,老年代,永久代。新建的對象會在新生代中分配內存,多次回收仍存活下來的對象存放在老年代,而靜態屬性和類信息存放在永久代中,新生代的對象存活時間短,在新生代區域中頻繁GC,老年代對象生命周期長,內存回收頻率較低,永久代一般不進行垃圾回收。根據不同的年代的特點采用不同的收集方式,提升收集效率。

4 總結:

引用和對象的關系,以及對象初始化都是java程序中非常需要註重的點,像上面所說java虛擬機判斷哪些是活的對象都是通過遍歷引用鏈條的方式,但僅僅依賴引用來判斷哪些對象是需要清理,哪些對象是不需要清理是不夠得。例如當內存吃緊時,而且引用都綁定著對象,那麽就沒有多少垃圾對象可以清理,但又內存資源緊張,那麽我們需要區分強引用,軟引用,弱引用和虛引用了。通過此方式清理些可以被清理的對象(例如緩存對象數據等等)。這些以後筆記會詳情描述。

02-方法傳參和初始化與垃圾回收清除