如何防止对象被垃圾回收?

38

如何防止对象被垃圾回收?

是否有使用finalize或虚引用或其他方法的途径?

我在面试中被问到这个问题。面试官建议可以使用finalize()方法。

11个回答

36

保持引用。如果您的对象过早地被回收,那么这是您的应用程序设计存在错误的表现。

垃圾收集器仅收集应用程序中没有引用的对象。如果没有任何对象自然引用被收集的对象,请思考为什么它应该被保持存活。

通常情况下您需要保持一个没有引用的对象是单例模式。在这种情况下,您可以使用静态变量。一个可能的单例模式实现如下:

public class Singleton {
  private static Singleton uniqueInstance;

  private Singleton() {
    }

  public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
      uniqueInstance = new Singleton();
    }
    return uniqInstance;
  }
}

编辑:从技术上讲,您可以在终结器中存储一个引用。这将防止对象被收集,直到收集器再次确定没有更多的引用。然而,终结器最多只会被调用一次,因此您必须确保您的对象(包括其超类)在第一次回收后不需要终结。不过,我建议您在实际程序中不要使用这种技术。(这会让像我这样的同事大喊WTF!?;)

  protected void finalize() throws Throwable {
    MyObjectStore.getInstance().store(this);
    super.finalize(); // questionable, but you should ensure calling it somewhere.
  }

2
你可以引发其他对象存储对你的对象的新引用。但请注意,finalize 方法对于任何给定的对象最多只会被调用一次,因此当收集器确定它可以再次被收集时,它不会再次被终结。 - Tobias
"(这会让像我这样的同事大喊WTF!;))是的,我知道,但这是面试官问我的问题,所以我不知道他为什么问我这个问题 :)" - Rahul Garg
也许他想知道当你在 finalizer 中创建一个新的引用时会发生什么(无论是有意还是无意),以及 finalize() 是否会再次被调用。 - Tobias

10
面试官期望听到的技巧答案可能是,通过制造内存泄漏来防止垃圾回收器移除对象。
显然,如果您在某些长期存在的上下文中保留了对该对象的引用,则不会被回收,但这不是 OP 的招聘人员所问的。这不是在 finalize 方法中发生的事情。
为了防止垃圾回收,您可以在 finalize 方法内编写一个无限循环,在其中调用 Thread.yield()(可能是为了防止空循环被优化掉)。
@Override
protected void finalize() throws Throwable { 
    while (true) { 
        Thread.yield(); 
    } 
} 

我参考的文章是Elliot Back写的,其中描述了通过这种方法来强制产生内存泄漏。

这只是表明了finalize方法是有害的的另一种方式。


1
我认为重点不在于 "Thread.yield()" 停止了垃圾回收,而是无限循环导致了停止 - "Thread.yield()" 的作用是防止无限循环占用太多 CPU。 - Score_Under
@Score_Under 很好的发现!谢谢,我重新阅读了文章,并编辑了我的回答。再次感谢。 - CPerkins

8
最好的方法是使用Unsafe,虽然ByteBuffer可能是某些情况下的一种可行解决方案。
还要搜索关键词“非堆内存”。 UnsafeByteBuffer相比的优势:
  • 允许直接表示对象,无需序列化,因此更快
  • 没有边界检查,因此更快
  • 显式释放控制
  • 可以分配超过JVM限制的内存
但是,它并不容易工作。该方法在以下文章中描述:

它们都包括以下步骤:

  • 我们需要一个sizeof运算符,而Unsafe没有。如何创建一个被问到了:在Java中,确定对象大小的最佳方法是什么?。最好的选择可能是instrument API,但这需要您创建一个Jar并使用特殊的命令行选项...

  • 一旦我们有了sizeof,就可以使用Unsafe#allocateMemory分配足够的内存,它基本上是一个malloc并返回一个地址

  • 创建一个常规的堆对象,使用Unsafe#copyMemory将其复制到已分配的内存中。为此,您需要知道堆对象的地址和对象的大小

  • 将一个Object指向已分配的内存,然后将Object强制转换为您的类。

    似乎不可能直接使用Unsafe设置变量的地址,因此我们需要将对象包装成数组或包装器对象,并使用Unsafe#arrayBaseOffsetUnsafe#objectFieldOffset

  • 完成后,使用freeMemory释放已分配的内存

如果我能让它不再发生段错误,我会发布一个示例 :-)

ByteBuffer

相比Unsafe的优势:

  • 稳定性跨越Java版本,而Unsafe可能会出现问题
  • 进行边界检查,因此比Unsafe更安全,后者允许出现内存泄漏和SIGSEGV

JLS says:

直接缓冲区的内容可能驻留在正常垃圾回收堆之外。

使用基本类型的示例:

ByteBuffer bb = ByteBuffer.allocateDirect(8);

bb.putInt(0, 1);
bb.putInt(4, 2);
assert bb.getInt(0) == 1;
assert bb.getInt(4) == 2;

// Bound chekcs are done.
boolean fail = false;
try {
    bb.getInt(8);
} catch(IndexOutOfBoundsException e) {
    fail = true;
}
assert fail;

相关主题:


3
如果对象仍有引用,它将不会被垃圾收集。如果没有任何引用,您就不必担心。
换句话说,垃圾收集器只收集垃圾。让它自行处理即可。

换句话说,不要使用finalize()来玩弄收集器。 - matt b

2
重点是,如果我们将指向对象的真实引用变量设置为null,尽管该类的实例变量指向该对象,但未设置为null。该对象会自动成为垃圾回收的可回收对象。如果要将对象保存到GC,请使用以下代码...
public class GcTest {

    public int id;
    public String name;
    private static GcTest gcTest=null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        System.out.println("In finalize method.");
        System.out.println("In finalize :ID :"+this.id);
        System.out.println("In finalize :ID :"+this.name);

        gcTest=this;

    }

    public static void main(String[] args) {

        GcTest myGcTest=new GcTest();
        myGcTest.id=1001;
        myGcTest.name="Praveen";
        myGcTest=null;

        // requesting Garbage Collector to execute.
        // internally GC uses Mark and Sweep algorithm to clear heap memory.
        // gc() is a native method in RunTime class.

        System.gc();   // or Runtime.getRuntime().gc();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("\n------- After called GC () ---------\n");
        System.out.println("Id :"+gcTest.id);
        System.out.println("Name :"+gcTest.name);


    }

}

输出:

在finalize方法中。
在finalize中:ID:1001
在finalize中:ID:Praveen

------- 在调用GC()之后 --------

ID:1001
姓名:Praveen


2
这似乎是面试中常见的问题之一。finalize() 方法在对象被垃圾回收时运行,因此将某些内容放入其中以防止垃圾回收显得非常反常规。通常,您只需要保留一个引用即可。
我甚至不确定如果您在 finalizer 中为某个东西创建一个新引用会发生什么情况 - 因为垃圾收集器已经决定回收它,那么您最终会得到一个空引用吗?无论如何,这似乎都是一个糟糕的想法。例如:
public class Foo {
   static Foo reference;
  ...
  finalize (){ 
     reference = this; 
  }
}

我怀疑这个方法是否有效,或者它可能依赖于GC实现,或者是"未指定的行为"。尽管看起来很邪恶。


2
我猜你可能在谈论的是你的finalize方法是否将一个指向正在被终结的对象的引用藏起来。如果是这样(如果我对Java语言规范的理解正确),那么finalize方法永远不会再次运行,但对象还没有被垃圾回收。
除非是出于意外的原因,否则这不是我们在现实生活中会做的事情!

1
我想知道他们是否考虑了资源池模式(例如用于网络/数据库连接或线程),在这种模式下,您可以使用finalize将资源返回到池中,以便实际持有资源的对象不会被GC回收。
愚蠢的例子,以类似Java的伪代码形式呈现,并缺少任何类型的同步:
class SlowResourceInternal {
   private final SlowResourcePool parent;
   <some instance data>

   returnToPool() {
       parent.add(this);
   }
}

class SlowResourceHolder {
    private final SlowResourceInternal impl;

    <delegate actual stuff to the internal object>

    finalize() {
        if (impl != null) impl.returnToPool();
    }
}

0
我们有三种方法来实现相同的结果 - 1)增加堆 - Eden 空间大小。 2)创建带有静态引用的 Singleton 类。 3)覆盖 finalize() 方法并永远不让该对象取消引用。

0

我相信这里有一个相关的模式。不确定它是否为工厂模式。但是,你只需要一个对象来创建所有的对象并保存对它们的引用。当你完成使用它们后,在工厂中将其解除引用,让调用变得明确。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接