【问题标题】:What is a reference value in Java and why does it change?什么是 Java 中的参考值,它为什么会改变?
【发布时间】:2016-05-21 16:37:44
【问题描述】:

来自没有 GC 的语言 (C/C++/Rust..) 我想知道如果重新分配数组会发生什么。

如果我们使用类似 c++ 的语言(伪代码),这被认为是不好的:

Obj *x = xarr[2];
xarr.push(new Obj(12));
do_with(x);

在 C++ 中运行示例http://ideone.com/qk7vcj

在推送之后,x 可能会因为 xarr 的重新分配而指向已释放的内存。

x 基本上只是一个指针大小的整数,用于存储 xarr[2] 的内存地址。

如果我在 java 中做同样的事情。这工作得很好,我想知道为什么?

List<OBJ> list = new ArrayList<>();    
list.add(new OBJ());
list.add(new OBJ());
list.add(new OBJ());

OBJ x = list.get(2);
for (int idx = 0; idx < 1000000; idx++) {
    list.add(new OBJ());    
}
do_it(x);

x 究竟是什么?在数组看似重新分配后,x 的内存地址如何以及为什么会发生变化?

显然 java 没有对数组进行深度复制,因为 x2 无法像在这段代码中那样更改 x,正如您所见,x 的地址也在变化。

private static class OBJ {
    int one;
    String two;

    public OBJ() {
        this.one = 1;
        this.two = "two";
    }
}

public static void do_it(OBJ o) {
    System.out.println("o.two is: " + o.two); 
}

public static void main(String[] args)
{

    List<OBJ> list = new ArrayList<>();
    list.add(new OBJ());
    list.add(new OBJ());
    list.add(new OBJ());

    OBJ x = list.get(2);

    printAddresses("Address x", x);

    for (int idx = 0; idx < 1000000; idx++) {
        list.add(new OBJ());    
    }

    OBJ x2 = list.get(2);
    x2.two = "haha";

    printAddresses("Address x", x);

    do_it(x);

} 

不应该打印出来

Address x: 0x525554440
Address x: 0x550882b80
o.two is: haha

完整的工作示例可以在这里找到http://ideone.com/P3j6xF

所以这引出了一个问题,x 的地址在重新分配列表后如何更改。而所谓的“参考”究竟是什么?我认为 Java 中所谓的“引用”只是一个普通的指针,它具有自动解引用之类的东西并且没有指针算术,因为在 Java 中,所有内容都是通过值而不是通过引用传递的。这在这段代码中很明显http://ideone.com/k4Ijq0

public static void test1(OBJ o) {
    o.one = 2;
}

public static void test2(OBJ o) {
    o = new OBJ();
    o.two = "no reference";
}

public static void main (String[] args) throws java.lang.Exception
{
    OBJ x = new OBJ();
    test1(x);
    test2(x);

    System.out.println("x.one: " + x.one + " x.two: " + x.two);
}   

打印出来

x.one: 2 x.two: two

所以看起来 x 的行为就像一个指针,但如果需要,java 会以某种方式重定向它。这是如何运作的? “引用”这个词比较混乱,为什么会这样称呼它?

【问题讨论】:

  • @Sotirios Delimanolis 这并不能真正解决我的问题。
  • 哦,您对Unsafe 输出感到困惑。这就是当您使用Unsafe 时会发生的情况。当您创建新对象并将其添加到列表中时,JVM 必须执行垃圾回收周期并且可能会移动您的对象。创建更少的对象,您会看到对象将保留在同一位置。 JVM 在 GC 期间根据需要更新这些引用。这对程序员应该是透明的,所以不会暴露(除非你选择使用Unsafe)。
  • Java 中的 ArrayList 不包含对象。它包含对单独分配的对象的引用。即使数组被重新分配,对象也不受影响。 C++ 中最接近的等价物是std::vector&lt;std::shared_ptr&lt;T&gt;&gt;

标签: java arrays pointers


【解决方案1】:

Java Virtual Machine Specification 状态

引用类型分为三种:类类型、数组类型、 和接口类型。 它们的值是动态引用 创建的类实例、数组或类实例或数组 分别实现接口。

同样,Java Language Specification 状态

引用值(通常只是引用)是指向这些值的指针 对象,以及一个特殊的空引用,它不引用任何对象。

换句话说,引用类型的值(或多或少)是对应对象的地址。这显然是从 Java 开发人员那里抽象出来的。您永远不需要知道对象在内存中的位置,因为您不管理内存。 JVM 会这样做。

当你这样做时

OBJ x = new OBJ();

或以其他方式获取参考值

OBJ x = list.get(2);

变量x 仅保存指向实际对象(或可能是nullreference)的引用值。

Java 是一种垃圾收集语言。 Modern garbage collection algorithms use generational and copying strategies。也就是说,他们将在几代人之间移动对象,因为他们决定这些对象的寿命。这一举动是复制和明确的。 GC 会经过一个专用区域,将所有活动对象复制到另一个区域,并将原始对象标记为空闲内存。

这对于我们之前提到的x 变量来说显然是有问题的。如果它指向内存中的一个活动对象并且该内存被“清除”,那么我们正在为问题做好准备。因此,GC 必须遍历所有存储移动对象位置的变量(实例变量、局部变量、数组元素),并在允许程序继续之前更新它们(在 Stop The World 收集期间完成)。

这就是您在Unsafe 代码中看到的内容。

OBJ x = list.get(2);
printAddresses("Address x", x);

当您第一次调用printAddresses 时,存储在x 中的值所引用的对象位于内存中的某个位置。在生成一堆新对象后,触发垃圾收集器,对象被移动到一个新位置并更新所有对它的引用(x 中的值,ArrayList 底层数组中的值)。如果你有更多的内存(或创建的对象更少),这不会发生(还)。

数组重新分配在 Java 中是如何工作的?

这与数组无关,真的。 ArrayList 对象包含一个数组字段(名为 elementData,它引用了一个数组对象。例如

elementData = 0x4000

并且该对象在内部具有对其他对象的引用(数组元素是变量)。

elementData[0] = 0x6720
elementData[1] = 0x6808
elementData[2] = 0x4393
elementData[3] = 0x7121
elementData[4] = 0x2425
elementData[5] = 0x4867
elementData[6] = 0x976
elementData[7] = 0x1082
elementData[8] = 0x4160
elementData[9] = 0x1850

当您达到该元素限制并且ArrayList 必须重新分配数组时,它只是将所有这些引用值复制到一个新数组中。

elementData = 0x8900;
elementData[0] = 0x6720 (same as above)
elementData[1] = 0x6808
elementData[2] = 0x4393
elementData[3] = 0x7121
elementData[4] = 0x2425
elementData[5] = 0x4867
elementData[6] = 0x976
elementData[7] = 0x1082
elementData[8] = 0x4160
elementData[9] = 0x1850
elementData[10] = 0x0000 (something for null)
...
elementData[newLength-1] = 0x0000

当然假设这些对象在垃圾回收周期中没有被移动。如果有,GC 也会更新数组变量。

同样,作为 Java 开发人员,您不需要关心这些。在编写 Java 代码时它很少派上用场。您永远无法直接访问实际的参考值(除非使用Unsafe)。

【讨论】:

    【解决方案2】:

    列表的重新分配不会改变x 的值。在 Java 中,x 将包含对创建对象的引用。如果支持列表的数组被重新分配,那么x 仍然是对同一对象的引用。

    您看到的是由于垃圾收集器而更改的对象地址。您可以看到 x 根本不在列表中的相同结果:

    public static void main(String[] args) {
        List<OBJ> list = new ArrayList<>(10000000);
    
        OBJ x = new OBJ();
    
        printAddresses("Address x", x);
    
        for (int idx = 0; idx < 1000000; idx++) {
            list.add(new OBJ());
        }
    
        printAddresses("Address x", x);
    }
    

    输出:

    Address x: 0x710b05580
    Address x: 0x54d5a19c0
    

    当垃圾收集器完成它的工作时,对象可以在内存中移动。发生这种情况时,任何需要更改的地址都会同时更新。

    另外,在 c++ 中,x 的值是对列表中某个项目的引用,因此如果重新分配列表,则此引用将无效。在 Java 中,x 是列表中项目的副本,因此列表是否被重新分配并不重要。在 Java 中不能引用元素。

    List&lt;OBJ&gt; 在 Java 中实际上是一个对象引用列表。这些对象独立于列表而存在。您可以复制这些引用之一来获得对同一对象的新引用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-25
      • 1970-01-01
      • 2020-05-16
      • 2017-08-01
      • 2010-12-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多