弱引用。当只有弱引用指向对象时,对象不会被删除。

3
在我们的系统中,我们将客户端会话表示为Session类。从历史上看,该类的哈希码是可变的 - 在创建时为0,并在某个时间点更改为用户ID。系统中存在两个会话管理器:
1. 客户端会话管理器 - 保存客户端的活动会话。该管理器内部包含一个简单的ConcurrentHashMap <Long,Session>,其中键是用户ID。 2. 内存会话管理器 - 保存尚未被GC收集的会话。其中有一个WeakHashMap <Session,Long>(这里的值是用户ID)。
当客户端连接并登录时,会发生以下流程:
1. 创建客户端会话。 2. 将会话放入内存会话管理器中(此时哈希码为0,因此它们被放入单个桶中)。 3. 客户端登录并将会话放入客户端会话管理器中,具有正确的哈希码。 4. 关闭客户端会话并从客户端会话管理器中删除。
在收集Session对象的所有强引用后,它必须从WeakHashMap中移除。 (当一个键不再被普通使用时,WeakHashMap中的条目将自动被删除。更确切地说,给定键的映射的存在不会防止该键被垃圾回收器丢弃,即使是最终化和回收。) 但由于某些原因,它们仍然停留在那里。 以下是其代码:
public class MemorySessionManager implements Runnable {
        private static final Logger logger = LoggerFactory.getLogger("checker");

        private Map<Session, Long> sessions;

        public MemorySessionManager() {
            this.sessions = new WeakHashMap<>();
        }

        public synchronized void addSession(Session sess) {
            sessions.put(sess, sess.getId());
        }

        public void run() {
            Set<Session> sessionsToCheck = new HashSet<>();
            synchronized (this) {
                sessionsToCheck.addAll(sessions.keySet());
            }
            for (Session sess : sessionsToCheck) {
                logger.warn("MemorySessionManager: Is still here: " + sess);
            }
        }
   }

简化编程流程和会话类(不包含无用信息)。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ClientSessionManager {

    private Map<Long, Session> sessions;

    public ClientSessionManager() {
       this.sessions = new ConcurrentHashMap<>();
    }

    public void addSession(Session session) {
       sessions.put(session.getUserId(), session);
    }

    public Session removeSession(long code) {
       return sessions.remove(code);
    }
}

public class Session {

    private long userId;

    public void setUserId(long userId) {
        this.userId = userId;
    }

    public long getUserId() {
        return userId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
           return true;
        if (o == null || getClass() != o.getClass())
           return false;

        Session session = (Session) o;

        return userId == session.userId;

   }

   @Override
   public int hashCode() {
       return (int) (userId ^ (userId >>> 32));
   }
}

public class Process {

    private static final ClientSessionManager CLIENT_SESSION_MANAGER = new ClientSessionManager();
    private static final MemorySessionManager MEMORY_SESSION_MANAGER = new MemorySessionManager();

    /**
     * Login user, create its session and register session into appropriate managers.
     * 
     * @param userId id of user
     */
    public void login(long userId) {
        Session session = new Session();
        MEMORY_SESSION_MANAGER.addSession(session);
        session.setUserId(userId);
        CLIENT_SESSION_MANAGER.addSession(session);
    }

    /**
     * Close session of user, remove it from session manager
     * 
     * @param userId id of user
     */
    public void close(long userId) {
        CLIENT_SESSION_MANAGER.removeSession(userId);
    }
}

经过多次GC循环后,仍然保留在内存中。以下是GC日志(G1,混合):

[GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 6815744 bytes, new threshold 4 (max 15)
- age   1:     236400 bytes,     236400 total
- age   2:     350240 bytes,     586640 total
- age   3:    3329024 bytes,    3915664 total
- age   4:    2926992 bytes,    6842656 total
, 0.0559520 secs]
   [Parallel Time: 51.9 ms, GC Workers: 2]
      [GC Worker Start (ms): Min: 73278041.7, Avg: 73278041.8, Max: 73278042.0, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 3.2, Avg: 3.4, Max: 3.7, Diff: 0.5, Sum: 6.9]
      [Update RS (ms): Min: 15.0, Avg: 15.0, Max: 15.0, Diff: 0.0, Sum: 30.1]
         [Processed Buffers: Min: 59, Avg: 65.0, Max: 71, Diff: 12, Sum: 130]
      [Scan RS (ms): Min: 13.6, Avg: 14.3, Max: 15.1, Diff: 1.5, Sum: 28.7]
      [Code Root Scanning (ms): Min: 0.4, Avg: 1.2, Max: 2.0, Diff: 1.5, Sum: 2.4]
      [Object Copy (ms): Min: 17.5, Avg: 17.7, Max: 17.8, Diff: 0.3, Sum: 35.4]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 51.6, Avg: 51.7, Max: 51.8, Diff: 0.2, Sum: 103.4]
      [GC Worker End (ms): Min: 73278093.6, Avg: 73278093.6, Max: 73278093.6, Diff: 0.0]
   [Code Root Fixup: 0.7 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.4 ms]
   [Other: 2.9 ms]
      [Choose CSet: 0.6 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.3 ms]
      [Humongous Register: 0.1 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.9 ms]
   [Eden: 95.0M(95.0M)->0.0B(1223.0M) Survivors: 7168.0K->5120.0K Heap: 512.0M(2048.0M)->363.5M(2048.0M)]
 [Times: user=0.11 sys=0.00, real=0.06 secs]
2016-09-28T10:40:00.815+0000: 73288.384: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 80740352 bytes, new threshold 15 (max 15)
- age   1:    1096960 bytes,    1096960 total
- age   2:     220208 bytes,    1317168 total
- age   3:     349352 bytes,    1666520 total
- age   4:    3325200 bytes,    4991720 tota

在下面的图片中,您可以看到问题会话的根路径,并且可以看到只存在弱引用链。

enter image description here

请帮忙。 或者至少给一些建议。

是的,这是遗留代码,现在已经更改了。我发布了它的初始版本。但无论如何,我不会通过键获取对象 - 我总是选择所有键,正如您可以在代码中看到的那样。 - Andre Minov
1
没有任何确切的保证GC会运行,更不用说执行特定的操作了。依赖于特定GC行为的代码需要修订。为什么引用链这么长? - user207421
@EJP,在插入映射时,哈希码为0,并且所有对象都被放置在同一个桶中 - 因此它只转换为链表。是的,代码必须更改(但这是遗留问题,必须找到问题)。是的,我们依赖GC,但除了WeakReference的javadoc行为之外,我们不期望任何非凡的事情。GC正在运行 - 我添加了日志。 - Andre Minov
2
当您重复从WeakHashMap中获取所有键时,您自己正在恢复对象。只有在您同时迭代它们时,垃圾回收才能将它们视为不可达。 - Holger
@Eugene 我没有把它发布为答案,因为这只是一个可能存在的场景,而且也可能存在其他问题。如果问题提出者确认这是问题所在,我会将其发布为答案。但问问题的人从未回来确认或反驳假设。 - Holger
显示剩余8条评论
1个回答

0

正如Holger在评论中指出的那样,以下内容是错误的。 我暂时将其保留在这里,以便其他人不会走上这条错误的道路。

无论如何,我建议摆脱可变的hashCode和整个MEMORY_SESSION_MANAGER(至少,在使用会话作为键之前更改userId)。

错误...

可变的hashCode...那很糟糕,但只要它在使用后不会改变就可以接受。 但我可以看到

    Session session = new Session();
    MEMORY_SESSION_MANAGER.addSession(session);
    session.setUserId(userId);

你把一个对象放进了 HashMap 作为键,然后立即改变了它的 hashCode。现在,这个行为是未定义的,这个 map 可能会在任何访问时抛出异常或者陷入无限循环。

如果这是 C/C++,它也可能会吃掉你的猫。

实际上发生的事情是,这个键再也找不到了。所以它不能被删除,不管是通过 .remove(key) 还是在清理中。它找不到,所以它不能被删除。就是这样。使用错误的 hashCode 会导致搜索错误的槽位。

在 Session 对象的所有强引用被收集后,它必须从 WeakHashMap 中删除。

只有当你遵守契约时,它才必须遵守契约。而你没有。


1
expungeStaleEntries() 使用存储在 Entry 实例中的哈希码,这是它被添加到映射表时所具有的哈希码。它只能以这种方式工作,因为在将 WeakReference 入队时,对象已经不可达,因此无法查询其哈希码。因此,在插入后修改哈希码仍然是一个非常糟糕的想法,但并不是这个特定问题的原因。 - Holger
@Holger 该死。你说得对。我会考虑一下,很快删除我的回答。 - maaartinus

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