1/*
2 * Copyright (c) 1996, 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package sun.rmi.transport;
26
27import java.io.InvalidClassException;
28import java.lang.ref.PhantomReference;
29import java.lang.ref.ReferenceQueue;
30import java.net.SocketPermission;
31import java.rmi.UnmarshalException;
32import java.security.AccessController;
33import java.security.PrivilegedAction;
34import java.util.HashMap;
35import java.util.HashSet;
36import java.util.Iterator;
37import java.util.List;
38import java.util.Map;
39import java.util.Set;
40import java.rmi.ConnectException;
41import java.rmi.RemoteException;
42import java.rmi.dgc.DGC;
43import java.rmi.dgc.Lease;
44import java.rmi.dgc.VMID;
45import java.rmi.server.ObjID;
46
47import sun.rmi.runtime.Log;
48import sun.rmi.runtime.NewThreadAction;
49import sun.rmi.server.UnicastRef;
50import sun.rmi.server.Util;
51
52import java.security.AccessControlContext;
53import java.security.Permissions;
54import java.security.ProtectionDomain;
55
56/**
57 * DGCClient implements the client-side of the RMI distributed garbage
58 * collection system.
59 *
60 * The external interface to DGCClient is the "registerRefs" method.
61 * When a LiveRef to a remote object enters the VM, it needs to be
62 * registered with the DGCClient to participate in distributed garbage
63 * collection.
64 *
65 * When the first LiveRef to a particular remote object is registered,
66 * a "dirty" call is made to the server-side distributed garbage
67 * collector for the remote object, which returns a lease guaranteeing
68 * that the server-side DGC will not collect the remote object for a
69 * certain period of time.  While LiveRef instances to remote objects
70 * on a particular server exist, the DGCClient periodically sends more
71 * "dirty" calls to renew its lease.
72 *
73 * The DGCClient tracks the local reachability of registered LiveRef
74 * instances (using phantom references).  When the LiveRef instance
75 * for a particular remote object becomes garbage collected locally,
76 * a "clean" call is made to the server-side distributed garbage
77 * collector, indicating that the server no longer needs to keep the
78 * remote object alive for this client.
79 *
80 * @see java.rmi.dgc.DGC, sun.rmi.transport.DGCImpl
81 *
82 * @author  Ann Wollrath
83 * @author  Peter Jones
84 */
85final class DGCClient {
86
87    /** next sequence number for DGC calls (access synchronized on class) */
88    private static long nextSequenceNum = Long.MIN_VALUE;
89
90    /** unique identifier for this VM as a client of DGC */
91    private static VMID vmid = new VMID();
92
93    /** lease duration to request (usually ignored by server) */
94    private static final long leaseValue =              // default 10 minutes
95        AccessController.doPrivileged((PrivilegedAction<Long>) () ->
96            Long.getLong("java.rmi.dgc.leaseValue", 600000));
97
98    /** maximum interval between retries of failed clean calls */
99    private static final long cleanInterval =           // default 3 minutes
100        AccessController.doPrivileged((PrivilegedAction<Long>) () ->
101            Long.getLong("sun.rmi.dgc.cleanInterval", 180000));
102
103    /** maximum interval between complete garbage collections of local heap */
104    private static final long gcInterval =              // default 1 hour
105        AccessController.doPrivileged((PrivilegedAction<Long>) () ->
106            Long.getLong("sun.rmi.dgc.client.gcInterval", 3600000));
107
108    /** minimum retry count for dirty calls that fail */
109    private static final int dirtyFailureRetries = 5;
110
111    /** retry count for clean calls that fail with ConnectException */
112    private static final int cleanFailureRetries = 5;
113
114    /** constant empty ObjID array for lease renewal optimization */
115    private static final ObjID[] emptyObjIDArray = new ObjID[0];
116
117    /** ObjID for server-side DGC object */
118    private static final ObjID dgcID = new ObjID(ObjID.DGC_ID);
119
120    /**
121     * An AccessControlContext with only socket permissions,
122     * suitable for an RMIClientSocketFactory.
123     */
124    private static final AccessControlContext SOCKET_ACC;
125    static {
126        Permissions perms = new Permissions();
127        perms.add(new SocketPermission("*", "connect,resolve"));
128        ProtectionDomain[] pd = { new ProtectionDomain(null, perms) };
129        SOCKET_ACC = new AccessControlContext(pd);
130    }
131
132    /*
133     * Disallow anyone from creating one of these.
134     */
135    private DGCClient() {}
136
137    /**
138     * Register the LiveRef instances in the supplied list to participate
139     * in distributed garbage collection.
140     *
141     * All of the LiveRefs in the list must be for remote objects at the
142     * given endpoint.
143     */
144    static void registerRefs(Endpoint ep, List<LiveRef> refs) {
145        /*
146         * Look up the given endpoint and register the refs with it.
147         * The retrieved entry may get removed from the global endpoint
148         * table before EndpointEntry.registerRefs() is able to acquire
149         * its lock; in this event, it returns false, and we loop and
150         * try again.
151         */
152        EndpointEntry epEntry;
153        do {
154            epEntry = EndpointEntry.lookup(ep);
155        } while (!epEntry.registerRefs(refs));
156    }
157
158    /**
159     * Get the next sequence number to be used for a dirty or clean
160     * operation from this VM.  This method should only be called while
161     * synchronized on the EndpointEntry whose data structures the
162     * operation affects.
163     */
164    private static synchronized long getNextSequenceNum() {
165        return nextSequenceNum++;
166    }
167
168    /**
169     * Given the length of a lease and the time that it was granted,
170     * compute the absolute time at which it should be renewed, giving
171     * room for reasonable computational and communication delays.
172     */
173    private static long computeRenewTime(long grantTime, long duration) {
174        /*
175         * REMIND: This algorithm should be more sophisticated, waiting
176         * a longer fraction of the lease duration for longer leases.
177         */
178        return grantTime + (duration / 2);
179    }
180
181    /**
182     * EndpointEntry encapsulates the client-side DGC information specific
183     * to a particular Endpoint.  Of most significance is the table that
184     * maps LiveRef value to RefEntry objects and the renew/clean thread
185     * that handles asynchronous client-side DGC operations.
186     */
187    private static class EndpointEntry {
188
189        /** the endpoint that this entry is for */
190        private Endpoint endpoint;
191        /** synthesized reference to the remote server-side DGC */
192        private DGC dgc;
193
194        /** table of refs held for endpoint: maps LiveRef to RefEntry */
195        private Map<LiveRef, RefEntry> refTable = new HashMap<>(5);
196        /** set of RefEntry instances from last (failed) dirty call */
197        private Set<RefEntry> invalidRefs = new HashSet<>(5);
198
199        /** true if this entry has been removed from the global table */
200        private boolean removed = false;
201
202        /** absolute time to renew current lease to this endpoint */
203        private long renewTime = Long.MAX_VALUE;
204        /** absolute time current lease to this endpoint will expire */
205        private long expirationTime = Long.MIN_VALUE;
206        /** count of recent dirty calls that have failed */
207        private int dirtyFailures = 0;
208        /** absolute time of first recent failed dirty call */
209        private long dirtyFailureStartTime;
210        /** (average) elapsed time for recent failed dirty calls */
211        private long dirtyFailureDuration;
212
213        /** renew/clean thread for handling lease renewals and clean calls */
214        private Thread renewCleanThread;
215        /** true if renew/clean thread may be interrupted */
216        private boolean interruptible = false;
217
218        /** reference queue for phantom references */
219        private ReferenceQueue<LiveRef> refQueue = new ReferenceQueue<>();
220        /** set of clean calls that need to be made */
221        private Set<CleanRequest> pendingCleans = new HashSet<>(5);
222
223        /** global endpoint table: maps Endpoint to EndpointEntry */
224        private static Map<Endpoint,EndpointEntry> endpointTable = new HashMap<>(5);
225        /** handle for GC latency request (for future cancellation) */
226        private static GC.LatencyRequest gcLatencyRequest = null;
227
228        /**
229         * Look up the EndpointEntry for the given Endpoint.  An entry is
230         * created if one does not already exist.
231         */
232        public static EndpointEntry lookup(Endpoint ep) {
233            synchronized (endpointTable) {
234                EndpointEntry entry = endpointTable.get(ep);
235                if (entry == null) {
236                    entry = new EndpointEntry(ep);
237                    endpointTable.put(ep, entry);
238                    /*
239                     * While we are tracking live remote references registered
240                     * in this VM, request a maximum latency for inspecting the
241                     * entire heap from the local garbage collector, to place
242                     * an upper bound on the time to discover remote references
243                     * that have become unreachable (see bugid 4171278).
244                     */
245                    if (gcLatencyRequest == null) {
246                        gcLatencyRequest = GC.requestLatency(gcInterval);
247                    }
248                }
249                return entry;
250            }
251        }
252
253        private EndpointEntry(final Endpoint endpoint) {
254            this.endpoint = endpoint;
255            try {
256                LiveRef dgcRef = new LiveRef(dgcID, endpoint, false);
257                dgc = (DGC) Util.createProxy(DGCImpl.class,
258                                             new UnicastRef(dgcRef), true);
259            } catch (RemoteException e) {
260                throw new Error("internal error creating DGC stub");
261            }
262            renewCleanThread =  AccessController.doPrivileged(
263                new NewThreadAction(new RenewCleanThread(),
264                                    "RenewClean-" + endpoint, true));
265            renewCleanThread.start();
266        }
267
268        /**
269         * Register the LiveRef instances in the supplied list to participate
270         * in distributed garbage collection.
271         *
272         * This method returns false if this entry was removed from the
273         * global endpoint table (because it was empty) before these refs
274         * could be registered.  In that case, a new EndpointEntry needs
275         * to be looked up.
276         *
277         * This method must NOT be called while synchronized on this entry.
278         */
279        public boolean registerRefs(List<LiveRef> refs) {
280            assert !Thread.holdsLock(this);
281
282            Set<RefEntry> refsToDirty = null;     // entries for refs needing dirty
283            long sequenceNum;           // sequence number for dirty call
284
285            synchronized (this) {
286                if (removed) {
287                    return false;
288                }
289
290                Iterator<LiveRef> iter = refs.iterator();
291                while (iter.hasNext()) {
292                    LiveRef ref = iter.next();
293                    assert ref.getEndpoint().equals(endpoint);
294
295                    RefEntry refEntry = refTable.get(ref);
296                    if (refEntry == null) {
297                        LiveRef refClone = (LiveRef) ref.clone();
298                        refEntry = new RefEntry(refClone);
299                        refTable.put(refClone, refEntry);
300                        if (refsToDirty == null) {
301                            refsToDirty = new HashSet<>(5);
302                        }
303                        refsToDirty.add(refEntry);
304                    }
305
306                    refEntry.addInstanceToRefSet(ref);
307                }
308
309                if (refsToDirty == null) {
310                    return true;
311                }
312
313                refsToDirty.addAll(invalidRefs);
314                invalidRefs.clear();
315
316                sequenceNum = getNextSequenceNum();
317            }
318
319            makeDirtyCall(refsToDirty, sequenceNum);
320            return true;
321        }
322
323        /**
324         * Remove the given RefEntry from the ref table.  If that makes
325         * the ref table empty, remove this entry from the global endpoint
326         * table.
327         *
328         * This method must ONLY be called while synchronized on this entry.
329         */
330        private void removeRefEntry(RefEntry refEntry) {
331            assert Thread.holdsLock(this);
332            assert !removed;
333            assert refTable.containsKey(refEntry.getRef());
334
335            refTable.remove(refEntry.getRef());
336            invalidRefs.remove(refEntry);
337            if (refTable.isEmpty()) {
338                synchronized (endpointTable) {
339                    endpointTable.remove(endpoint);
340                    Transport transport = endpoint.getOutboundTransport();
341                    transport.free(endpoint);
342                    /*
343                     * If there are no longer any live remote references
344                     * registered, we are no longer concerned with the
345                     * latency of local garbage collection here.
346                     */
347                    if (endpointTable.isEmpty()) {
348                        assert gcLatencyRequest != null;
349                        gcLatencyRequest.cancel();
350                        gcLatencyRequest = null;
351                    }
352                    removed = true;
353                }
354            }
355        }
356
357        /**
358         * Make a DGC dirty call to this entry's endpoint, for the ObjIDs
359         * corresponding to the given set of refs and with the given
360         * sequence number.
361         *
362         * This method must NOT be called while synchronized on this entry.
363         */
364        private void makeDirtyCall(Set<RefEntry> refEntries, long sequenceNum) {
365            assert !Thread.holdsLock(this);
366
367            ObjID[] ids;
368            if (refEntries != null) {
369                ids = createObjIDArray(refEntries);
370            } else {
371                ids = emptyObjIDArray;
372            }
373
374            long startTime = System.currentTimeMillis();
375            try {
376                Lease lease =
377                    dgc.dirty(ids, sequenceNum, new Lease(vmid, leaseValue));
378                long duration = lease.getValue();
379
380                long newRenewTime = computeRenewTime(startTime, duration);
381                long newExpirationTime = startTime + duration;
382
383                synchronized (this) {
384                    dirtyFailures = 0;
385                    setRenewTime(newRenewTime);
386                    expirationTime = newExpirationTime;
387                }
388
389            } catch (Exception e) {
390                long endTime = System.currentTimeMillis();
391
392                synchronized (this) {
393                    dirtyFailures++;
394
395                    if (e instanceof UnmarshalException
396                            && e.getCause() instanceof InvalidClassException) {
397                        DGCImpl.dgcLog.log(Log.BRIEF, "InvalidClassException exception in DGC dirty call", e);
398                        return;             // protocol error, do not register these refs
399                    }
400
401                    if (dirtyFailures == 1) {
402                        /*
403                         * If this was the first recent failed dirty call,
404                         * reschedule another one immediately, in case there
405                         * was just a transient network problem, and remember
406                         * the start time and duration of this attempt for
407                         * future calculations of the delays between retries.
408                         */
409                        dirtyFailureStartTime = startTime;
410                        dirtyFailureDuration = endTime - startTime;
411                        setRenewTime(endTime);
412                    } else {
413                        /*
414                         * For each successive failed dirty call, wait for a
415                         * (binary) exponentially increasing delay before
416                         * retrying, to avoid network congestion.
417                         */
418                        int n = dirtyFailures - 2;
419                        if (n == 0) {
420                            /*
421                             * Calculate the initial retry delay from the
422                             * average time elapsed for each of the first
423                             * two failed dirty calls.  The result must be
424                             * at least 1000ms, to prevent a tight loop.
425                             */
426                            dirtyFailureDuration =
427                                Math.max((dirtyFailureDuration +
428                                          (endTime - startTime)) >> 1, 1000);
429                        }
430                        long newRenewTime =
431                            endTime + (dirtyFailureDuration << n);
432
433                        /*
434                         * Continue if the last known held lease has not
435                         * expired, or else at least a fixed number of times,
436                         * or at least until we've tried for a fixed amount
437                         * of time (the default lease value we request).
438                         */
439                        if (newRenewTime < expirationTime ||
440                            dirtyFailures < dirtyFailureRetries ||
441                            newRenewTime < dirtyFailureStartTime + leaseValue)
442                        {
443                            setRenewTime(newRenewTime);
444                        } else {
445                            /*
446                             * Give up: postpone lease renewals until next
447                             * ref is registered for this endpoint.
448                             */
449                            setRenewTime(Long.MAX_VALUE);
450                        }
451                    }
452
453                    if (refEntries != null) {
454                        /*
455                         * Add all of these refs to the set of refs for this
456                         * endpoint that may be invalid (this VM may not be in
457                         * the server's referenced set), so that we will
458                         * attempt to explicitly dirty them again in the
459                         * future.
460                         */
461                        invalidRefs.addAll(refEntries);
462
463                        /*
464                         * Record that a dirty call has failed for all of these
465                         * refs, so that clean calls for them in the future
466                         * will be strong.
467                         */
468                        Iterator<RefEntry> iter = refEntries.iterator();
469                        while (iter.hasNext()) {
470                            RefEntry refEntry = iter.next();
471                            refEntry.markDirtyFailed();
472                        }
473                    }
474
475                    /*
476                     * If the last known held lease will have expired before
477                     * the next renewal, all refs might be invalid.
478                     */
479                    if (renewTime >= expirationTime) {
480                        invalidRefs.addAll(refTable.values());
481                    }
482                }
483            }
484        }
485
486        /**
487         * Set the absolute time at which the lease for this entry should
488         * be renewed.
489         *
490         * This method must ONLY be called while synchronized on this entry.
491         */
492        private void setRenewTime(long newRenewTime) {
493            assert Thread.holdsLock(this);
494
495            if (newRenewTime < renewTime) {
496                renewTime = newRenewTime;
497                if (interruptible) {
498                    AccessController.doPrivileged(
499                        new PrivilegedAction<Void>() {
500                            public Void run() {
501                            renewCleanThread.interrupt();
502                            return null;
503                        }
504                    });
505                }
506            } else {
507                renewTime = newRenewTime;
508            }
509        }
510
511        /**
512         * RenewCleanThread handles the asynchronous client-side DGC activity
513         * for this entry: renewing the leases and making clean calls.
514         */
515        private class RenewCleanThread implements Runnable {
516
517            public void run() {
518                do {
519                    long timeToWait;
520                    RefEntry.PhantomLiveRef phantom = null;
521                    boolean needRenewal = false;
522                    Set<RefEntry> refsToDirty = null;
523                    long sequenceNum = Long.MIN_VALUE;
524
525                    synchronized (EndpointEntry.this) {
526                        /*
527                         * Calculate time to block (waiting for phantom
528                         * reference notifications).  It is the time until the
529                         * lease renewal should be done, bounded on the low
530                         * end by 1 ms so that the reference queue will always
531                         * get processed, and if there are pending clean
532                         * requests (remaining because some clean calls
533                         * failed), bounded on the high end by the maximum
534                         * clean call retry interval.
535                         */
536                        long timeUntilRenew =
537                            renewTime - System.currentTimeMillis();
538                        timeToWait = Math.max(timeUntilRenew, 1);
539                        if (!pendingCleans.isEmpty()) {
540                            timeToWait = Math.min(timeToWait, cleanInterval);
541                        }
542
543                        /*
544                         * Set flag indicating that it is OK to interrupt this
545                         * thread now, such as if a earlier lease renewal time
546                         * is set, because we are only going to be blocking
547                         * and can deal with interrupts.
548                         */
549                        interruptible = true;
550                    }
551
552                    try {
553                        /*
554                         * Wait for the duration calculated above for any of
555                         * our phantom references to be enqueued.
556                         */
557                        phantom = (RefEntry.PhantomLiveRef)
558                            refQueue.remove(timeToWait);
559                    } catch (InterruptedException e) {
560                    }
561
562                    synchronized (EndpointEntry.this) {
563                        /*
564                         * Set flag indicating that it is NOT OK to interrupt
565                         * this thread now, because we may be undertaking I/O
566                         * operations that should not be interrupted (and we
567                         * will not be blocking arbitrarily).
568                         */
569                        interruptible = false;
570                        Thread.interrupted();   // clear interrupted state
571
572                        /*
573                         * If there was a phantom reference enqueued, process
574                         * it and all the rest on the queue, generating
575                         * clean requests as necessary.
576                         */
577                        if (phantom != null) {
578                            processPhantomRefs(phantom);
579                        }
580
581                        /*
582                         * Check if it is time to renew this entry's lease.
583                         */
584                        long currentTime = System.currentTimeMillis();
585                        if (currentTime > renewTime) {
586                            needRenewal = true;
587                            if (!invalidRefs.isEmpty()) {
588                                refsToDirty = invalidRefs;
589                                invalidRefs = new HashSet<>(5);
590                            }
591                            sequenceNum = getNextSequenceNum();
592                        }
593                    }
594
595                    boolean needRenewal_ = needRenewal;
596                    Set<RefEntry> refsToDirty_ = refsToDirty;
597                    long sequenceNum_ = sequenceNum;
598                    AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
599                        if (needRenewal_) {
600                            makeDirtyCall(refsToDirty_, sequenceNum_);
601                        }
602
603                        if (!pendingCleans.isEmpty()) {
604                            makeCleanCalls();
605                        }
606                        return null;
607                    }, SOCKET_ACC);
608                } while (!removed || !pendingCleans.isEmpty());
609            }
610        }
611
612        /**
613         * Process the notification of the given phantom reference and any
614         * others that are on this entry's reference queue.  Each phantom
615         * reference is removed from its RefEntry's ref set.  All ref
616         * entries that have no more registered instances are collected
617         * into up to two batched clean call requests: one for refs
618         * requiring a "strong" clean call, and one for the rest.
619         *
620         * This method must ONLY be called while synchronized on this entry.
621         */
622        private void processPhantomRefs(RefEntry.PhantomLiveRef phantom) {
623            assert Thread.holdsLock(this);
624
625            Set<RefEntry> strongCleans = null;
626            Set<RefEntry> normalCleans = null;
627
628            do {
629                RefEntry refEntry = phantom.getRefEntry();
630                refEntry.removeInstanceFromRefSet(phantom);
631                if (refEntry.isRefSetEmpty()) {
632                    if (refEntry.hasDirtyFailed()) {
633                        if (strongCleans == null) {
634                            strongCleans = new HashSet<>(5);
635                        }
636                        strongCleans.add(refEntry);
637                    } else {
638                        if (normalCleans == null) {
639                            normalCleans = new HashSet<>(5);
640                        }
641                        normalCleans.add(refEntry);
642                    }
643                    removeRefEntry(refEntry);
644                }
645            } while ((phantom =
646                (RefEntry.PhantomLiveRef) refQueue.poll()) != null);
647
648            if (strongCleans != null) {
649                pendingCleans.add(
650                    new CleanRequest(createObjIDArray(strongCleans),
651                                     getNextSequenceNum(), true));
652            }
653            if (normalCleans != null) {
654                pendingCleans.add(
655                    new CleanRequest(createObjIDArray(normalCleans),
656                                     getNextSequenceNum(), false));
657            }
658        }
659
660        /**
661         * CleanRequest holds the data for the parameters of a clean call
662         * that needs to be made.
663         */
664        private static class CleanRequest {
665
666            final ObjID[] objIDs;
667            final long sequenceNum;
668            final boolean strong;
669
670            /** how many times this request has failed */
671            int failures = 0;
672
673            CleanRequest(ObjID[] objIDs, long sequenceNum, boolean strong) {
674                this.objIDs = objIDs;
675                this.sequenceNum = sequenceNum;
676                this.strong = strong;
677            }
678        }
679
680        /**
681         * Make all of the clean calls described by the clean requests in
682         * this entry's set of "pending cleans".  Clean requests for clean
683         * calls that succeed are removed from the "pending cleans" set.
684         *
685         * This method must NOT be called while synchronized on this entry.
686         */
687        private void makeCleanCalls() {
688            assert !Thread.holdsLock(this);
689
690            Iterator<CleanRequest> iter = pendingCleans.iterator();
691            while (iter.hasNext()) {
692                CleanRequest request = iter.next();
693                try {
694                    dgc.clean(request.objIDs, request.sequenceNum, vmid,
695                              request.strong);
696                    iter.remove();
697                } catch (Exception e) {
698                    /*
699                     * Many types of exceptions here could have been
700                     * caused by a transient failure, so try again a
701                     * few times, but not forever.
702                     */
703                    if (++request.failures >= cleanFailureRetries) {
704                        iter.remove();
705                    }
706                }
707            }
708        }
709
710        /**
711         * Create an array of ObjIDs (needed for the DGC remote calls)
712         * from the ids in the given set of refs.
713         */
714        private static ObjID[] createObjIDArray(Set<RefEntry> refEntries) {
715            ObjID[] ids = new ObjID[refEntries.size()];
716            Iterator<RefEntry> iter = refEntries.iterator();
717            for (int i = 0; i < ids.length; i++) {
718                ids[i] = iter.next().getRef().getObjID();
719            }
720            return ids;
721        }
722
723        /**
724         * RefEntry encapsulates the client-side DGC information specific
725         * to a particular LiveRef value.  In particular, it contains a
726         * set of phantom references to all of the instances of the LiveRef
727         * value registered in the system (but not garbage collected
728         * locally).
729         */
730        private class RefEntry {
731
732            /** LiveRef value for this entry (not a registered instance) */
733            private LiveRef ref;
734            /** set of phantom references to registered instances */
735            private Set<PhantomLiveRef> refSet = new HashSet<>(5);
736            /** true if a dirty call containing this ref has failed */
737            private boolean dirtyFailed = false;
738
739            public RefEntry(LiveRef ref) {
740                this.ref = ref;
741            }
742
743            /**
744             * Return the LiveRef value for this entry (not a registered
745             * instance).
746             */
747            public LiveRef getRef() {
748                return ref;
749            }
750
751            /**
752             * Add a LiveRef to the set of registered instances for this entry.
753             *
754             * This method must ONLY be invoked while synchronized on this
755             * RefEntry's EndpointEntry.
756             */
757            public void addInstanceToRefSet(LiveRef ref) {
758                assert Thread.holdsLock(EndpointEntry.this);
759                assert ref.equals(this.ref);
760
761                /*
762                 * Only keep a phantom reference to the registered instance,
763                 * so that it can be garbage collected normally (and we can be
764                 * notified when that happens).
765                 */
766                refSet.add(new PhantomLiveRef(ref));
767            }
768
769            /**
770             * Remove a PhantomLiveRef from the set of registered instances.
771             *
772             * This method must ONLY be invoked while synchronized on this
773             * RefEntry's EndpointEntry.
774             */
775            public void removeInstanceFromRefSet(PhantomLiveRef phantom) {
776                assert Thread.holdsLock(EndpointEntry.this);
777                assert refSet.contains(phantom);
778                refSet.remove(phantom);
779            }
780
781            /**
782             * Return true if there are no registered LiveRef instances for
783             * this entry still reachable in this VM.
784             *
785             * This method must ONLY be invoked while synchronized on this
786             * RefEntry's EndpointEntry.
787             */
788            public boolean isRefSetEmpty() {
789                assert Thread.holdsLock(EndpointEntry.this);
790                return refSet.size() == 0;
791            }
792
793            /**
794             * Record that a dirty call that explicitly contained this
795             * entry's ref has failed.
796             *
797             * This method must ONLY be invoked while synchronized on this
798             * RefEntry's EndpointEntry.
799             */
800            public void markDirtyFailed() {
801                assert Thread.holdsLock(EndpointEntry.this);
802                dirtyFailed = true;
803            }
804
805            /**
806             * Return true if a dirty call that explicitly contained this
807             * entry's ref has failed (and therefore a clean call for this
808             * ref needs to be marked "strong").
809             *
810             * This method must ONLY be invoked while synchronized on this
811             * RefEntry's EndpointEntry.
812             */
813            public boolean hasDirtyFailed() {
814                assert Thread.holdsLock(EndpointEntry.this);
815                return dirtyFailed;
816            }
817
818            /**
819             * PhantomLiveRef is a PhantomReference to a LiveRef instance,
820             * used to detect when the LiveRef becomes permanently
821             * unreachable in this VM.
822             */
823            private class PhantomLiveRef extends PhantomReference<LiveRef> {
824
825                public PhantomLiveRef(LiveRef ref) {
826                    super(ref, EndpointEntry.this.refQueue);
827                }
828
829                public RefEntry getRefEntry() {
830                    return RefEntry.this;
831                }
832            }
833        }
834    }
835}
836