Java GC 没有第二次收集“僵尸”对象

Posted

技术标签:

【中文标题】Java GC 没有第二次收集“僵尸”对象【英文标题】:Java GC does not gather a "zombie" object for a second time 【发布时间】:2018-03-20 09:57:53 【问题描述】:

我正在尝试创建一种机制来将对象缓存到内存中,以备将来使用,即使这些对象脱离上下文也是如此。将有一个并行的确定性过程,它将(通过唯一的 ID)指示缓存的对象是否应该再次检索或是否应该完全死亡。这是最简单的示例,带有调试信息以使事情变得更容易:

package com.panayotis.resurrect;

import java.util.Map;
import java.util.HashMap;

public class ZObject 

    private static int IDGEN = 1;

    protected int id;
    private boolean isKilled = false;

    public static final Map<Integer, ZObject> zombies = new HashMap<>();

    public static void main(String[] args) 
        for (int i = 0; i < 5; i++)
            System.out.println("* INIT: " + new ZObject().toString());
        gc();
        sleep(1000);

        if (!zombies.isEmpty())
            ZObject.revive(2);

        gc();
        sleep(1000);

        if (!zombies.isEmpty())
            ZObject.kill(1);

        gc();
        sleep(1000);
        gc();
        sleep(1000);
        gc();
        sleep(1000);
        gc();
        sleep(1000);
    

    public ZObject() 
        this.id = IDGEN++;
    

    protected final void finalize() throws Throwable 
        String debug = "" + zombies.size();
        String name = toString();
        String style;
        if (!isKilled) 
            style = "* Zombie";
            zombies.put(id, this);
         else 
            style = "*** FINAL ***";
            zombies.remove(id);
            super.finalize();
        
        dumpZombies(style + " " + debug, name);
    

    public String toString() 
        return (isKilled ? "killed" : zombies.containsKey(id) ? "zombie" : "alive ") + " " + id;
    

    public static ZObject revive(int peer) 
        ZObject obj = zombies.remove(peer);
        if (obj != null) 
            System.out.println("* Revive      " + obj.toString());
            obj.isKilled = false;
         else
            System.out.println("* Not found as zombie " + peer);
        return obj;
    

    public static void kill(int peer) 
        int size = zombies.size();
        ZObject obj = zombies.get(peer);
        String name = obj == null ? peer + " TERMINATED " : obj.toString();
        zombies.remove(peer);
        dumpZombies("*   Kill " + size, name);
        if (obj != null)
            obj.isKilled = true;
    

    private static void dumpZombies(String baseMsg, String name) 
        System.out.println(baseMsg + "->" + zombies.size() + " " + name);
        for (Integer key : zombies.keySet())
            System.out.println("*             " + zombies.get(key).toString());
    

    public static void gc() 
        System.out.println("* Trigger GC");
        for (int i = 0; i < 50; i++)
            System.gc();
    

    public static void sleep(int howlong) 
        try 
            Thread.sleep(howlong);
         catch (InterruptedException ex) 
        
    

这段代码将创建 5 个对象,复活第一个,然后杀死第一个。我期待着

第一次复活后,由于对象还没有任何引用,通过 finalize 重新进入僵尸状态(它没有)

再次杀死一个对象后,再次通过finalize方法从内存中完全删除

换句话说,finalize 似乎只被调用了一次。我用这段代码检查了这不是 HashMap 对象的副产品:

package com.panayotis.resurrect;

import java.util.HashMap;

public class TestMap 

    private static final HashMap<Integer, TestMap> map = new HashMap<>();

    private static int IDGEN = 1;
    private final int id;

    public static void main(String[] args) 
        map.put(1, new TestMap(1));
        map.put(2, new TestMap(2));
        map.put(3, new TestMap(3));
        map.remove(1);
        System.out.println("Size: " + map.size());
        for (int i = 0; i < 50; i++)
            System.gc();
    

    public TestMap(int id) 
        this.id = id;
    

    protected void finalize() throws Throwable 
        System.out.println("Finalize " + id);
        super.finalize();
    

那么,为什么会有这种行为?我正在使用 Java 1.8

编辑由于这不可能直接实现,有什么想法可以做到这一点吗?

【问题讨论】:

根据您对我的回答的回复,这似乎是XY Problem “由于这不可能直接实现,有什么想法我可以做到这一点吗?” - IMO,你需要清楚地解释“这个”到底是什么。 我将创建一个新问题,谢谢提及 请看这里:***.com/questions/46649865/… What happens to a "finalized" object if I make it available again?的可能重复 【参考方案1】:

你知道吗?

我认为您的陈述要求只需通过并发地图即可满足。

我正在尝试创建一种机制来将对象缓存到内存中,以备将来使用,即使这些对象脱离上下文也是如此。

那只是一个简单的map,以ID为key;例如

Map<IdType, ValueType> cache = new HashMap<>();

当您创建需要缓存的对象时,您只需调用cache.put(id, object)。它会一直缓存,直到您将其删除。

会有一个并行的确定性过程,它将(通过唯一的 ID)指示缓存的对象是应该再次检索还是应该完全死亡。

这是一个调用cache.remove(id)的线程(“并行确定性过程”)。

现在,如果您从缓存中删除了一个对象,并且它仍在其他地方使用(即它仍然可访问),那么它将不会被垃圾回收。但这没关系。但不应该!


但是finalize() 的那些东西呢?

据我所知,它根本不符合您的声明要求。您的代码似乎检测到注定要删除的对象,并使它们可以访问(您的zombies 列表)。这似乎与您的要求相反。

如果finalize() 的目的只是跟踪Zombie 对象何时被实际删除,那么finalize() 只会被调用一次,所以它不能这样做。但是,为什么finalize() 方法会将对象添加到zombie 列表中?

如果您的要求实际上是错误的,并且您真的试图创建“不朽”对象(即无法删除的对象),那么一个普通的Map 会做到这一点。只是不要删除对象的键,它会永远“活着”。


现在将缓存实现为普通映射可能会造成内存泄漏。有几种方法可以解决这个问题:

1234563有关详细信息,请参阅 javadocs。

您可以将缓存实现为HashMap&lt;SoftReference&lt;IdType&gt;, ValueType&gt;,并使用ReferenceQueue 删除其引用已被GC 破坏的缓存条目。 (请注意,当某个键不再强可达,并且内存不足时,GC 会破坏软引用。)

【讨论】:

不幸的是,它不是一个简单的缓存系统,也不是线程,它是一些本地代码,可以持有未知时间的引用,也可能在未知时间释放它。棘手的部分是需要通知本机系统 Java 不再需要它,但只要本机部分需要,Java 对象就必须保持活动状态是很重要的。 [也许为这种情况创建一个单独的问题? ] 好吧,也许您需要重申您的要求。事实上,如果你提出一个全新的问题,我认为你会得到更中肯的答案。专注于解释你的实际需求......而不是要求解释为什么 finalize 没有做你认为应该做的事情。 我这里写的是实际需求:***.com/questions/46649865/…【参考方案2】:

这正是指定的行为:

Object.finalize()

在为对象调用finalize 方法后,不会采取进一步的行动,直到 Java 虚拟机再次确定没有任何方法可以让尚未访问的任何线程访问该对象。死亡,包括其他准备完成的对象或类可能执行的操作,此时该对象可能会被丢弃。

finalize 方法永远不会被 Java 虚拟机为任何给定对象多次调用。

您似乎对finalize() 方法的作用有错误的理解。此方法不会释放对象的内存,声明一个自定义的重要finalize() 方法实际上是防止对象的内存被释放,因为它必须保存在内存中才能执行该方法和之后,直到垃圾收集器确定它再次变得无法访问。不再调用finalize() 并不意味着对象没有被释放,它意味着它会被释放而不再调用finalize()

没有自定义finalize() 方法或具有“普通”终结方法(为空或仅由super.finalize() 对另一个普通终结器的调用组成)的类实例根本不会通过终结队列,并且两者都是,分配更快,回收更快。

这就是为什么你永远不应该尝试只为内存实现对象缓存,结果总是比 JVM 自己的内存管理效率低。但是,如果您正在管理一个真正昂贵的资源,您可以通过将其分成两种不同类型的对象来处理它,一个为应用程序提供 API 的前端,当应用程序不使用它时,它可能会被垃圾收集,以及描述实际资源的后端对象,应用程序不会直接看到它,并且可能会被重用。

这意味着资源足够昂贵,足以证明这种分离的重要性。否则,它就不是真正值得缓存的资源。

// front-end class
public class Resource 
    final ActualResource actual;

    Resource(ActualResource actual) 
        this.actual = actual;
    
    public int getId() 
        return actual.getId();
    
    public String toString() 
        return actual.toString();
    

class ActualResource 
    int id;

    ActualResource(int id) 
        this.id = id;
    

    int getId() 
        return id;
    

    @Override
    public String toString() 
        return "ActualResource[id="+id+']';
    

public class ResourceManager 
    static final ReferenceQueue<Resource> QUEUE = new ReferenceQueue<>();
    static final List<ActualResource> FREE = new ArrayList<>();
    static final Map<WeakReference<?>,ActualResource> USED = new HashMap<>();
    static int NEXT_ID;

    public static synchronized Resource getResource() 
        for(;;) 
            Reference<?> t = QUEUE.poll();
            if(t==null) break;
            ActualResource r = USED.remove(t);
            if(r!=null) FREE.add(r);
        
        ActualResource r;
        if(FREE.isEmpty()) 
            System.out.println("allocating new resource");
            r = new ActualResource(NEXT_ID++);
        
        else 
            System.out.println("reusing resource");
            r = FREE.remove(FREE.size()-1);
        
        Resource frontEnd = new Resource(r);
        USED.put(new WeakReference<>(frontEnd, QUEUE), r);
        return frontEnd;
    
    /**
     * Allow the underlying actual resource to get garbage collected with r.
     */
    public static synchronized void stopReusing(Resource r) 
        USED.values().remove(r.actual);
    
    public static synchronized void clearCache() 
        FREE.clear();
        USED.clear();
    

请注意,管理器类可能有任意方法来控制资源的缓存或手动释放,以上方法只是示例。如果你的 API 支持前端失效,例如在调用close()dispose() 或类似名称后,可以立即显式释放或重用,而无需等待下一个 gc 循环。虽然 finalize() 只被调用了一次,但您可以在此处控制重用周期的数量,包括排队 0 次的选项。

这是一些测试代码

static final ResourceManager manager = new ResourceManager();
public static void main(String[] args) 
    Resource r1 = manager.getResource();
    Resource r2 = manager.getResource();
    System.out.println("r1 = "+r1+", r2 = "+r2);
    r1 = null;
    forceGC();

    r1 = manager.getResource();
    System.out.println("r1 = "+r1);
    r1 = null;
    forceGC();

    r1 = manager.getResource();
    System.out.println("r1 = "+r1);

    manager.stopReusing(r1);

    r1 = null;
    forceGC();

    r1 = manager.getResource();
    System.out.println("r1 = "+r1);

private static void forceGC() 
    for(int i = 0; i<5; i++ ) try 
        System.gc();
        Thread.sleep(50);
     catch(InterruptedException ex)

这很可能(System.gc() 仍然不能保证有效果)打印:

allocating new resource
allocating new resource
r1 = ActualResource[id=0], r2 = ActualResource[id=1]
reusing resource
r1 = ActualResource[id=0]
reusing resource
r1 = ActualResource[id=0]
allocating new resource
r1 = ActualResource[id=2]

【讨论】:

确实是这样:处理非常昂贵的资源,否则无法检索这些资源,否则我知道所有这些努力都不值得,实际上并没有优化。我之所以没有采用这种双类解决方案,是因为如果我们创建一个新的继承前端对象会怎样?那么前端对象中存储的数据不会丢失吗? 您要重用的每一个数据都必须可以通过后端资源对象访问。这并不排除镜像;两者都可能引用这些数据。但是,必须注意不要让 gc 在其他数据仍在使用时收集前端对象,因为激进的优化允许这样做,即使所有访问似乎都是通过前端对象从源代码的观点。说真的,在大多数情况下,手动资源管理更简单、更健壮,即使尝试使用 JVM 的可达性分析来管理资源非常诱人…… 我知道,但问题是这样的——正如我对另一条评论所说的那样,也许最好创建一个新问题来清楚地说明需求,而不是试图用错误的假设来解决问题。 我把实际需求写在这里:***.com/questions/46649865/…【参考方案3】:

您不应该实现 finalize 方法,因为 GC 只会为每个实例调用一次。

所以如果 GC 会找到要删除的对象,它会调用 finalize。然后它会再次检查是否有新的参考。它可能会找到一个并将对象保存在内存中。

在下一次运行时,同一个对象将再次没有任何引用。 GC 只会杀死它,它不会再次调用 finalize。

【讨论】:

我知道“我不应该重写 finalize 方法”的所有原因。有一篇关于它的好文章。 web.archive.org/web/20111015052835/http://java.sun.com/…但这不是问题 但是...不,这正是您所要求的。你写了It seems, in other words, that finalize is called only once.So, why this behavior?。您的问题的答案是,GC 只会调用 finalize 一次。这不是错误,它是 VM 需要满足的特定要求。 那么,把事情放到上下文中,有没有办法像这样 gc 一个“僵尸”引用? 这是一个有趣的用例。我没有 100% 合适的答案,但也许其中一个参考对象可能对您有所帮助:docs.oracle.com/javase/7/docs/api/java/lang/ref/Reference.html 请参阅幻影、软参考和弱参考这三种子类型。 它们似乎都不起作用:Phantom 被调用 after finalize 被调用,Soft 是一种防止 OutOfMemory 和 Weak 的方法......嗯......它没有真正保持一致的参考

以上是关于Java GC 没有第二次收集“僵尸”对象的主要内容,如果未能解决你的问题,请参考以下文章

Java之GC 如何工作

JVM的垃圾回收机制详解和调优

java的垃圾收集,GC 是什么?为什么要有GC?

JDK源码阅读 | 聊一聊GC收集器与内存分配策略

Java开发中啥是垃圾回收?

了解JVM中的GC