Java 中的參數傳遞和引用類型

本文主要分三部分介紹 Java 中的值、指針與引用的概念。
第一部分從編程語言的三種參數傳遞方式入手,闡釋「爲何 Java 中只有值傳遞」。
第二部分排除自動裝箱和自動拆箱的干擾,理解 Integer 等封裝類做爲參數傳值的情形。
第三部分經過簡單的示例,展現強引用、軟引用、弱引用和虛引用之間的區別。

1、參數傳遞方式

1.1 值傳遞

形參是實參的拷貝,改變形參的值並不會影響外部實參的值。
從被調用函數的角度來講,值傳遞是單向的(實參->形參),參數的值只能傳入,不能傳出。java

public class IntegerTest01 {

    private static void changeInt(int value) {
        ++value;
    }

    public static void main(String[] args) {
        int a = 1;
        changeInt(a);
        System.out.println("a = " + a);
    }
}

執行結果爲a = 1ios

1.2 指針傳遞

Java 中沒有指針,爲了直觀展現指針傳遞,這裏使用了 C++ 的例子。
指針從本質上講是一個變量,變量的值是另外一個變量的地址。所以能夠說指針傳遞屬於值傳遞。編程

#include <iostream>
using namespace std;

void fun(int *x) {// 聲明指針
   *x += 5; // *x 是取得指針所指向的內存單元,即指針解引用
   // x += 5; 則對實參沒有影響
}

int main() {
   int y = 0;
   fun(&y);// 取地址
   cout<< "y =  "<< y <<endl;
   return 0;
}

執行結果爲y = 5segmentfault

Java 中的「指針」

《Head First Java》中關於 Java 參數傳遞的說明:數組

Java 中所傳遞的全部東西都是值,但此值是變量所攜帶的值。引用對象的變量所攜帶的是 遠程控制而不是對象自己,若你對方法傳入參數,實際上傳入的是遠程控制的拷貝。

《深刻理解 JVM 虛擬機》中關於 Sun HotSpot 虛擬機進行對象訪問的方式的說明:編程語言

若是使用直接指針,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問對象類型數據的相關信息,而 reference 中存儲的直接就是對象地址。

對象訪問方式.png

在 Java 中聲明並初始化一個對象Object object = new Object(),在堆中存儲對象實例數據,在棧中存儲對象地址,這裏的變量 object 至關於 C/C++ 中的指針。ide

所以,能夠經過 Java 對象的引用,達到指針傳遞的效果。函數

public class IntegerTest02 {

    private static void changeInt(int[] value) {
        ++value[0];
    }

    public static void main(String[] args) {
        int[] a = {1};
        changeInt(a);
        System.out.println("a[0] = " + a[0]);
    }
}

執行結果爲a[0] = 2佈局

1.3 引用傳遞

既然 Java 中沒有引用傳遞,那麼到底什麼是引用傳遞呢,看下 C++ 中的例子。ui

#include <iostream>
using namespace std;

void fun(int &x){// 聲明一個別名
   x += 5; // 修改的是 x 引用的對象值 &x = y;
}

int main()
{
   int y = 0;
   fun(y);
   cout<< "y =  "<< y <<endl;
   return 0;
}

執行結果y = 5

C++ 中的引用就是某一變量(目標)的一個別名,對引用的操做與對變量直接操做徹底同樣。
聲明一個引用,不是新定義了一個變量,它只表示該引用名是目標變量名的一個別名,它自己不是一種數據類型,所以引用自己不佔存儲單元,系統也不給引用分配存儲單元

Java 中的引用

Java 中的引用是 reference 類型,相似於 C/C++ 中指針的概念,而跟 C/C++ 中引用的概念徹底不一樣。

在 JDK 1.2 之前,Java 中的引用的定義:若是 reference 類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。

在JDK 1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

進一步的介紹見 Java 中的 Reference 類型

2、Integer 參數傳遞問題

回到開篇值傳遞的例子:

public class IntegerTest01 {

    private static void changeInt(int value) {
        ++value;
    }

    public static void main(String[] args) {
        int a = 1;
        changeInt(a);
        System.out.println("a = " + a);
    }
}

若是把代碼中的 int 類型換成 Integer 對象,結果會怎麼樣?

public class IntegerTest02 {

    private static void changeInteger(Integer value) {
        ++value;
    }

    public static void main(String[] args) {
        Integer a = 1;
        changeInteger(a);
        System.out.println("a = " + a);
    }
}

首先須要排除自動裝箱和自動拆箱的干擾。

2.1 自動裝箱和自動拆箱

package com.sumkor.jdk7.integer02;

public class IntegerTest {
    public static void main(String[] args) {
        Integer a = 1;
        int b = a;
    }
}

使用命令javap -c IntegerTest.class進行反編譯:

Compiled from "IntegerTest.java"
public class com.sumkor.jdk7.integer02.IntegerTest {
  public com.sumkor.jdk7.integer02.IntegerTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1
       5: aload_1
       6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
       9: istore_2
      10: return
}

由此可知:
自動裝箱實際調用的是Integer.valueOf
自動拆箱實際調用的是Integer.intValue

所以,排除自動裝箱、自動拆箱,例子 IntegerTest02 等價於如下寫法:

public class IntegerTest03 {

    private static void changeInteger(Integer value) {
        value = Integer.valueOf(value.intValue() + 1);
    }

    public static void main(String[] args) {
        Integer a = Integer.valueOf(1);
        changeInteger(a);
    }
}

查看 Integer 源碼,可知valueOf()會將形參指向不一樣的 Integer 對象實例。

/**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
   /**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the -XX:AutoBoxCacheMax=<size> option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
    }

2.2 關於 IntegerCache

IntegerCache 在首次使用時被初始化,最小值爲 -128,最大值默認爲 127,也能夠經過 VM 參數-XX:AutoBoxCacheMax=<size>設置最大值。

@Test
    public void test01() {
        Integer a = 1;
        Integer b = 1;
        System.out.println(a == b);

        Integer aa = 128;
        Integer bb = 128;
        System.out.println(aa == bb);
    }

變量ab指向的是同一個IntegerCache.cache,所以比較結果爲true.
變量aabb指向的是不一樣的 Integer 實例,所以比較結果爲false.

3、Java 中的 Reference 類型

《深刻理解 JVM 虛擬機》中對此的介紹爲:

  • 強引用就是指在程序代碼之中廣泛存在的,相似Object object = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。在JDK 1.2以後,提供了 SoftReference 類來實現軟引用。
  • 弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK 1.2以後,提供了 WeakReference 類來實現弱引用。
  • 虛引用也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2以後,提供了 PhantomReference 類來實現虛引用。
Reference 類型的強度跟 JVM 垃圾回收有關,惋惜書上沒有給出實例,本文對此進行補充。

注意,如下例子中,使用 JDK 1.8,且均設置 JVM 參數爲-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
即堆大小爲 20 m,其中新生代大小爲 10 m,按照 1:8 比例分配,Eden 區大小爲 8 m。

3.1 強引用

/**
 * Created by Sumkor on 2018/9/10.
 */
public class StrongReferenceTest {

    public static void main(String[] args) {
        byte[] allocation01 = new byte[1024 * 1024 * 9];
        byte[] allocation02 = new byte[1024 * 1024 * 9];
    }
}

執行結果以下,可知垃圾收集器寧願拋出內存溢出異常,也不會回收正在使用中的強引用:

[GC (Allocation Failure)  11197K->10032K(19456K), 0.0014301 secs]
 [Full GC (Ergonomics)  10032K->9851K(19456K), 0.0072375 secs]
 [GC (Allocation Failure)  9851K->9851K(19456K), 0.0004413 secs]
 [Full GC (Allocation Failure)  9851K->9833K(19456K), 0.0093839 secs]
 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
 at com.sumkor.reference.StrongReferenceTest.main(StrongReferenceTest.java:18)

3.2 軟引用

@Test
public void test01() {

    byte[] allocation01 = new byte[1024 * 1024 * 8];
    SoftReference<byte[]> softReference = new SoftReference<byte[]>(allocation01);
    // 此時,對於這個byte數組對象,有兩個引用路徑,一個是來自SoftReference對象的軟引用,一個來自變量allocation01的強引用,因此這個數組對象是強可及對象。

    System.out.println("softReference.get() = " + softReference.get());
    allocation01 = null;
    // 結束變量allocation01對這個byte數組實例的強引用,此後該byte數組對象變成一個軟可及對象,能夠經過softReference進行訪問
    System.out.println("softReference.get() = " + softReference.get());

    System.gc();
    System.out.println("softReference.get() = " + softReference.get());
}

執行結果以下,可見在觸發 gc 時,內存空間充足,並不會回收軟引用:

softReference.get() = [B@5d6f64b1
 softReference.get() = [B@5d6f64b1
 [GC (System.gc())  14584K->9644K(19456K), 0.0040375 secs]
 [Full GC (System.gc())  9644K->9508K(19456K), 0.0115994 secs]
 softReference.get() = [B@5d6f64b1

再來看內存不足的例子:

@Test
public void test02() {
    byte[] allocation01 = new byte[1024 * 1024 * 8];
    SoftReference<byte[]> softReference = new SoftReference<byte[]>(allocation01);
    // 此時,對於這個byte數組對象,有兩個引用路徑,一個是來自SoftReference對象的軟引用,一個來自變量allocation01的強引用,因此這個數組對象是強可及對象。

    System.out.println("softReference.get() = " + softReference.get());
    allocation01 = null;
    // 結束變量allocation01對這個byte數組實例的強引用,此後該byte數組對象變成一個軟可及對象,能夠經過softReference進行訪問
    System.out.println("softReference.get() = " + softReference.get());

    byte[] allocation02 = new byte[1024 * 1024 * 8];
    System.out.println("softReference.get() = " + softReference.get());
}

可見在觸發 gc 時,內存空間不足,回收軟引用:

softReference.get() = [B@5d6f64b1
 softReference.get() = [B@5d6f64b1
 [GC (Allocation Failure)  14749K->9636K(19456K), 0.0056237 secs]
 [GC (Allocation Failure)  9636K->9684K(19456K), 0.0014787 secs]
 [Full GC (Allocation Failure)  9684K->9508K(19456K), 0.0128735 secs]
 [GC (Allocation Failure)  9508K->9508K(19456K), 0.0006353 secs]
 [Full GC (Allocation Failure)  9508K->1261K(19456K), 0.0107362 secs]
 softReference.get() = null

3.3 弱引用

package com.sumkor.reference;

import java.lang.ref.WeakReference;

/**
 * Created by Sumkor on 2018/9/10.
 */
public class WeakReferenceTest {

    public static void main(String[] args) {

        byte[] allocation01 = new byte[1024 * 1024 * 8];
        WeakReference<byte[]> weakReference = new WeakReference<byte[]>(allocation01);

        System.out.println("weakReference.get() = " + weakReference.get());// [B@154ebadd
        allocation01 = null;
        System.out.println("weakReference.get() = " + weakReference.get());// [B@154ebadd

        System.gc();
        System.out.println("weakReference.get() = " + weakReference.get());// null
    }
}

執行結果以下,可見儘管內存空間充足,垃圾回收器工做時回收掉只被弱引用關聯的對象:

weakReference.get() = [B@14ae5a5
 weakReference.get() = [B@14ae5a5
 [GC (System.gc())  10177K->9008K(19456K), 0.0011390 secs]
 [Full GC (System.gc())  9008K->643K(19456K), 0.0069800 secs]
 weakReference.get() = null

3.4 虛引用

package com.sumkor.reference;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;

/**
 * Created by Sumkor on 2018/9/10.
 */
public class PhantomReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        byte[] allocation01 = new byte[1024 * 1024 * 8];
        PhantomReference<byte[]> phantom = new PhantomReference<>(allocation01, referenceQueue);
        allocation01 = null;

        Thread.currentThread().sleep(3000);
        System.gc();
        Thread.currentThread().sleep(3000);

        Reference<?> poll = referenceQueue.poll();
        System.out.println("poll = " + poll);// java.lang.ref.PhantomReference@5d6f64b1
        System.out.println("phantom.get() = " + phantom.get());
    }
}

執行結果以下,phantom.get()老是爲 null,當 byte 數組對象被垃圾回收器回收時,垃圾收集器會把要回收的對象添加到引用隊列ReferenceQueue,即獲得一個「通知」:

[GC (System.gc())  14742K->9608K(19456K), 0.0025841 secs]
 [Full GC (System.gc())  9608K->9510K(19456K), 0.0117227 secs]
 poll = java.lang.ref.PhantomReference@5d6f64b1
 phantom.get() = null
相關文章
相關標籤/搜索