Java 中的参数传递和引用类型

本文主要分三部分介绍 Java 中的值、指针与引用的概念。
第一部分从编程语言的三种参数传递方式入手,阐释“为什么 Java 中只有值传递”。
第二部分排除自动装箱和自动拆箱的干扰,理解 Integer 等封装类作为参数传值的情形。
第三部分通过简单的示例,展示强引用、软引用、弱引用和虚引用之间的区别。

一、参数传递方式

1.1 值传递

形参是实参的拷贝,改变形参的值并不会影响外部实参的值。
从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。

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 = 1

1.2 指针传递

Java 中没有指针,为了直观展示指针传递,这里使用了 C++ 的例子。
指针从本质上讲是一个变量,变量的值是另一个变量的地址。因此可以说指针传递属于值传递。

#include 
using namespace std;

void fun(int *x) {// 声明指针
   *x += 5; // *x 是取得指针所指向的内存单元,即指针解引用
   // x += 5; 则对实参没有影响
}

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

执行结果为y = 5

Java 中的“指针”

《Head First Java》中关于 Java 参数传递的说明:

Java 中所传递的所有东西都是值,但此值是变量所携带的值。引用对象的变量所携带的是 远程控制而不是对象本身,若你对方法传入参数,实际上传入的是远程控制的拷贝。

《深入理解 JVM 虚拟机》中关于 Sun HotSpot 虚拟机进行对象访问的方式的说明:

如果使用直接指针,那么 Java 堆对象的布局中就必须考虑如何放置访问对象类型数据的相关信息,而 reference 中存储的直接就是对象地址。

Java 中的参数传递和引用类型_第1张图片

在 Java 中声明并初始化一个对象Object object = new Object(),在堆中存储对象实例数据,在栈中存储对象地址,这里的变量 object 相当于 C/C++ 中的指针。

因此,可以通过 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++ 中的例子。

#include 
using namespace std;

void fun(int &x){// 声明一个别名
   x += 5; // 修改的是 x 引用的对象值 &x = y;
}

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

执行结果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 类型

二、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."":()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= 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=设置最大值。

    @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.

三、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 softReference = new SoftReference(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 softReference = new SoftReference(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 weakReference = new WeakReference(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 referenceQueue = new ReferenceQueue<>();
        byte[] allocation01 = new byte[1024 * 1024 * 8];
        PhantomReference 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

你可能感兴趣的:(java)