1. 程式人生 > >Java 學習筆記(3)——函數

Java 學習筆記(3)——函數

位置 byte 局部變量 垃圾回收 turn 溢出 中修改 java程序 length

之前的幾篇文章中,總結了java中的基本語句和基本數據類型等等一系列的最基本的東西,下面就來說說java中的函數部分

函數基礎

在C/C++中有普通的全局函數、類成員函數和類的靜態函數,而java中所有內容都必須定義在類中。所以Java中是沒有全局函數的,Java裏面只有普通的類成員函數(也叫做成員方法)和靜態函數(也叫做靜態方法)。這兩種東西在理解上與C/C++基本一樣,定義的格式分別為:

public static void test(arglist){

}
public void test(arglist){

}

基本格式為:修飾符 [static] 返回值 函數名稱 形參列表

修飾符主要是用來修飾方法的訪問限制,比如public 、private等等;如果是靜態方法需要加上static 如果是成員方法則不需要;後面是返回值,Java函數可以返回任意類型的值;函數名用來確定一個函數,最後形參列表是傳遞給函數的參數列表。

函數中的內存分布

Java中函數的使用方式與C/C++中基本相同,這裏就不再額外花費篇幅說明它的使用,我想將重點放在函數調用時內存的分配和使用上,更深一層了解java中函數的運行機制。

我們說在X86架構的機器上,每個進程擁有4GB的虛擬地址空間。Java程序也是一個進程,所以它也擁有4GB的虛擬地址空間。每當啟動一個Java程序的時候,由Java虛擬機讀取.class 文件,然後解釋執行其中的二進制字節碼。啟動java程序時,在進程列表中看到的是一個個的Java虛擬機程序。
java虛擬機在加載.class 文件時將它的4GB的虛擬地址空間劃分為5個部分,分別是棧、堆、方法區、本地方法棧、寄存器區。其中重點需要關註前3個部分。

  • 棧:與C/C++中棧的作用相同,就是用來保存函數中的局部變量和實參值的。
  • 堆:與C/C++中堆的作用相同,用來存儲Java中new出來的對象
  • 方法區:用來保存方法代碼和方法名與地址的這麽一張表,類似於C/C++中的函數表

基本數據類型作為函數的參數

class Demo{
    public static void main(String[] args){
        int n = 10;
        test(10);
        System.out.println(n);
    }

    public static void test(int i){
        System.out.println(i);
        i++;
    }
}

上述代碼在函數中改變了形參值,那麽在調用之後n的值會不會發生變化呢?答案是:不會變化,在C/C++中很好理解,形參i只是實參n的一個拷貝,i改變不會改變原來的n。這裏我們從內存的角度來回答這個問題
技術分享圖片

如上圖所示,方法區中存儲了兩個方法的相關信息,main和test,在調用main的時候,首先從方法區中查找main函數的相關信息,然後在棧中進行參數入棧等操作。然後初始化一個局部變量n,接著調用test函數,調用test函數時首先根據方法區中的函數表找到方法對應的代碼位置,然後進行棧寄存器的偏移為函數test分配一個棧空間,接著進行參數入棧,這個時候會將n的值——10拷貝到i所在內存中。這個時候在test中修改了i的值,改變的是形參中拷貝的值,與n無關。所以這裏n的值不變

引用類型作為函數參數

class Demo{
    public static void main(String[] args){
        String s = "Hello";
        test(s);
        System.out.println(s); //"Hello"
    }

    public static void test(String s){
        System.out.println(s);  //"Hello"
        s = "World";
    }
}

在C/C++中,經常有這麽一句話:“按值傳遞不能改變實參的值,按引用傳遞可以改變實參的值”,我們知道String 是一個引用,那麽這裏傳遞的是String的引用,我們在函數內部改變了s的值,在外部s的值是不是也改變了呢?我們首先估計會打印一個 "Hello"、一個"World"; 實際運行結果卻是打印了兩個 "Hello",那麽是不是有問題呢?Java中到底存不存在按引用傳遞呢?為了回答這個問題,我們還是來一張內存圖:
技術分享圖片

從上面的內存圖來看,在函數中修改的仍然是形參的值,而對實參的值完全沒有影響。如果想做到在函數中修改實參的值,請記住一點:拿到實參的地址,通過地址直接修改內存。

下面再來看一個例子:

class Demo{
    public static void main(String[] args){
        int[] array = new int[]{1, 2, 3, 4, 5};
        test(array);
        for(int i = 0; i < array.length; i++){
            System.out.print(array[i]);
        }
        
        System.out.println(); //98345
    }

    public static void test(int[] array){
        for(int i = 0; i < array.length; i++){
            System.out.print(array[i]);
        }
        
        System.out.println(); //12345
        array[0] = 9;
        array[1] = 8;
    }
}

運行這個實例,可以看到這裏它確實改變了,那麽這裏它發生了什麽?跟上面一個字符串的例子相比有什麽不同呢?還是來看看內存圖
技術分享圖片

這段代碼執行的過程中經歷了3個主要步驟:

  • new一個數組對象,並且將數組對象的地址賦值給array 實參
  • 調用test函數時將array實參中保存的地址復制一份壓入函數的參數列表中
  • 在test函數中,通過這個地址值來修改對應內存中的內容

這段代碼與上面兩段本質上的區別在於,這段代碼通過引用類型中保存的地址值找到並修改了對應內存中內容,而上面的兩段代碼僅僅是在修改引用類型這個變量本身的值。

說到傳遞引用類型,那麽我就想到在C/C++中一個經典的漏洞——緩沖區溢出漏洞,那麽java程序中是否也存在這個問題呢?這裏我準備了這樣一段代碼:

class Demo{
    public static void main(String[] args){
        byte[] buf = new byte[7];
        test(buf);
    }

    public static void test(byte[] buf){
        for(int i = 0; i < 10; i++){
            buf[i] = (byte)i;
        }
    }
}

如果是在C/C++中,這段代碼可以正常執行只是最後可能會報錯或者崩潰,但是賦值是成功的,這也就留給了黑客可利用的空間。

在Java中執行它會發現,它會報一個越界訪問的異常,也就說這裏賦值是失敗的,不能直接往內存裏面寫,也就不存在這個漏洞了。

返回引用類型

Java方法返回基本類型的情況很簡單,也就是將函數返回值放到某塊內存中,然後進行一個復制操作。這裏重點了解一下它在返回引用類型時與C/C++不同的點

在C/C++中返回一個類對象的時候,會調用拷貝構造將需要返回的類對象拷貝到對應保存類對象的位置,然後針對函數中的類對象調用它的析構函數進行資源回收,那麽Java中返回類對象會進行哪些操作?

C/C++中返回一個類對象的指針時,外部需要自己調用delete或者其他操作進行析構。java中的類對象都是引用類型,在函數外部為何不需要額外調用析構呢?帶著這些問題,來看下面這段代碼:

class Demo{
    public static void main(String[] args){
        String s = test();
        System.out.println(s);
    }

    public static String test(){
//      return new String("hello world");
        return "Hello World";
    }
}

這段代碼 不管是用new也好還是直接返回也好,效果其實是一樣的,下面是對應的內存分布圖
技術分享圖片

這段代碼首先在函數test中new一個對象,此時對應在堆內存中開辟一塊空間來保存"hello world" 值,然後保存內存地址在寄存器或者其他某個位置,接著將這個地址值拷貝到main函數中的s中,最後回收test函數的棧空間。

這裏實質上是返回了一個堆中的地址值,這裏就回答了第一個問題:在返回類對象的時候其實返回的值對象所在的堆內存的地址。

接著來回答第二個問題:java中資源回收依賴與一個引用計數。每當對地址值進行一次拷貝時計數器加一,當回收拷貝值所在內存時計數器減一。這裏在返回時,先將地址值保存到某個位置(比如C/C++中是將返回值保存在eax寄存器中)。此時計數器 + 1;然後將這個值拷貝到 main 函數的s變量中,此時計數器的值再 + 1,變為2,接著回收test函數棧空間,計數器 - 1,變為1,在main函數指向完成之後,main的棧空間也被回收,此時計數器 - 1,變為0,此時new出來的對象由Java的垃圾回收器進行回收。


Java 學習筆記(3)——函數