资讯专栏INFORMATION COLUMN

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

gnehc / 2950人阅读

摘要:强引用执行结果如下,可知垃圾收集器宁愿抛出内存溢出异常,也不会回收正在使用中的强引用软引用此时,对于这个数组对象,有两个引用路径,一个是来自对象的软引用,一个来自变量的强引用,所以这个数组对象是强可及对象。

本文主要分三部分介绍 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 中声明并初始化一个对象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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77076.html

相关文章

  • Java的方法参数传递——值传递or引用传递

    摘要:有种流行的观点说的另外一个特殊之处在于,在方法调用传参数时,是按值传递的,其他普通对象是引用传递。然而这种说法是大大错误的,至少是完全误解了值传递和引用传递的概念。方法调用传参只有一种传递就是值传递。 上篇文章说到Java的String是比较特殊的对象,它是不可变的。 有种流行的观点说String的另外一个特殊之处在于,在方法调用传参数时,String是按值传递的,其他普通对象是引用传...

    Berwin 评论0 收藏0
  • 辨析Java方法参数中的传递引用传递

    摘要:引用数据类型指针存放在局部变量表中,调用方法的时候,副本引用压栈,赋值仅改变副本的引用。方法执行完毕,不再局部变量不再被使用到,等待被回收。 小方法大门道 小瓜瓜作为一个Java初学者,今天跟我说她想通过一个Java方法,将外部变量通过参数传递到方法中去,进行逻辑处理,方法执行完毕之后,再对修改过的变量进行判断处理,代码如下所示。 public class MethodParamsPa...

    Aomine 评论0 收藏0
  • Java基础知识储备一:Java的值传递引用传递

    摘要:每个栈帧中包括局部变量表用来存储方法中的局部变量非静态变量函数形参。操作数栈虚拟机的解释执行引擎被称为基于栈的执行引擎,其中所指的栈就是指操作数栈。指向运行时常量池的引用存储程序执行时可能用到常量的引用。 本篇文章转自微信公众号:Java后端技术 学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出...

    frontoldman 评论0 收藏0
  • 这一次,彻底解决Java的值传递引用传递

    摘要:操作数栈虚拟机的解释执行引擎被称为基于栈的执行引擎,其中所指的栈就是指操作数栈。基本数据类型的静态变量前面提到方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常 本文旨在用最通俗的语言讲述最枯燥的基本知识 学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而...

    Lavender 评论0 收藏0
  • java中传值方式的个人理解

    摘要:接下了,我们调用方法,来尝试改变的值以此验证中的传值方式。我们将作为实参传给方法,形参来接受这个实参,在这里就体现出了两种传参方式的不同。中只有值传递这一种方式,只不过对于引用类型来说,传递的参数是对象的引用罢了。 前言 这几天在整理java基础知识方面的内容,对于值传递还不是特别理解,于是查阅了一些资料和网上相关博客,自己进行了归纳总结,最后将其整理成了一篇博客。 值传递 值传递是指...

    vvpvvp 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<