View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.internal.storage.file;
13  
14  import java.io.IOException;
15  import java.lang.ref.ReferenceQueue;
16  import java.lang.ref.SoftReference;
17  import java.util.Collections;
18  import java.util.Map;
19  import java.util.Random;
20  import java.util.concurrent.ConcurrentHashMap;
21  import java.util.concurrent.ConcurrentLinkedQueue;
22  import java.util.concurrent.atomic.AtomicLong;
23  import java.util.concurrent.atomic.AtomicReferenceArray;
24  import java.util.concurrent.atomic.LongAdder;
25  import java.util.concurrent.locks.ReentrantLock;
26  import java.util.stream.Collectors;
27  
28  import org.eclipse.jgit.annotations.NonNull;
29  import org.eclipse.jgit.internal.JGitText;
30  import org.eclipse.jgit.storage.file.WindowCacheConfig;
31  import org.eclipse.jgit.storage.file.WindowCacheStats;
32  import org.eclipse.jgit.util.Monitoring;
33  
34  /**
35   * Caches slices of a {@link org.eclipse.jgit.internal.storage.file.PackFile} in
36   * memory for faster read access.
37   * <p>
38   * The WindowCache serves as a Java based "buffer cache", loading segments of a
39   * PackFile into the JVM heap prior to use. As JGit often wants to do reads of
40   * only tiny slices of a file, the WindowCache tries to smooth out these tiny
41   * reads into larger block-sized IO operations.
42   * <p>
43   * Whenever a cache miss occurs, {@link #load(PackFile, long)} is invoked by
44   * exactly one thread for the given <code>(PackFile,position)</code> key tuple.
45   * This is ensured by an array of locks, with the tuple hashed to a lock
46   * instance.
47   * <p>
48   * During a miss, older entries are evicted from the cache so long as
49   * {@link #isFull()} returns true.
50   * <p>
51   * Its too expensive during object access to be 100% accurate with a least
52   * recently used (LRU) algorithm. Strictly ordering every read is a lot of
53   * overhead that typically doesn't yield a corresponding benefit to the
54   * application.
55   * <p>
56   * This cache implements a loose LRU policy by randomly picking a window
57   * comprised of roughly 10% of the cache, and evicting the oldest accessed entry
58   * within that window.
59   * <p>
60   * Entities created by the cache are held under SoftReferences if option
61   * {@code core.packedGitUseStrongRefs} is set to {@code false} in the git config
62   * (this is the default) or by calling
63   * {@link WindowCacheConfig#setPackedGitUseStrongRefs(boolean)}, permitting the
64   * Java runtime's garbage collector to evict entries when heap memory gets low.
65   * Most JREs implement a loose least recently used algorithm for this eviction.
66   * When this option is set to {@code true} strong references are used which
67   * means that Java gc cannot evict the WindowCache to reclaim memory. On the
68   * other hand this provides more predictable performance since the cache isn't
69   * flushed when used heap comes close to the maximum heap size.
70   * <p>
71   * The internal hash table does not expand at runtime, instead it is fixed in
72   * size at cache creation time. The internal lock table used to gate load
73   * invocations is also fixed in size.
74   * <p>
75   * The key tuple is passed through to methods as a pair of parameters rather
76   * than as a single Object, thus reducing the transient memory allocations of
77   * callers. It is more efficient to avoid the allocation, as we can't be 100%
78   * sure that a JIT would be able to stack-allocate a key tuple.
79   * <p>
80   * This cache has an implementation rule such that:
81   * <ul>
82   * <li>{@link #load(PackFile, long)} is invoked by at most one thread at a time
83   * for a given <code>(PackFile,position)</code> tuple.</li>
84   * <li>For every <code>load()</code> invocation there is exactly one
85   * {@link #createRef(PackFile, long, ByteWindow)} invocation to wrap a
86   * SoftReference or a StrongReference around the cached entity.</li>
87   * <li>For every Reference created by <code>createRef()</code> there will be
88   * exactly one call to {@link #clear(PageRef)} to cleanup any resources associated
89   * with the (now expired) cached entity.</li>
90   * </ul>
91   * <p>
92   * Therefore, it is safe to perform resource accounting increments during the
93   * {@link #load(PackFile, long)} or
94   * {@link #createRef(PackFile, long, ByteWindow)} methods, and matching
95   * decrements during {@link #clear(PageRef)}. Implementors may need to override
96   * {@link #createRef(PackFile, long, ByteWindow)} in order to embed additional
97   * accounting information into an implementation specific
98   * {@link org.eclipse.jgit.internal.storage.file.WindowCache.PageRef} subclass, as
99   * the cached entity may have already been evicted by the JRE's garbage
100  * collector.
101  * <p>
102  * To maintain higher concurrency workloads, during eviction only one thread
103  * performs the eviction work, while other threads can continue to insert new
104  * objects in parallel. This means that the cache can be temporarily over limit,
105  * especially if the nominated eviction thread is being starved relative to the
106  * other threads.
107  */
108 public class WindowCache {
109 
110 	/**
111 	 * Record statistics for a cache
112 	 */
113 	static interface StatsRecorder {
114 		/**
115 		 * Record cache hits. Called when cache returns a cached entry.
116 		 *
117 		 * @param count
118 		 *            number of cache hits to record
119 		 */
120 		void recordHits(int count);
121 
122 		/**
123 		 * Record cache misses. Called when the cache returns an entry which had
124 		 * to be loaded.
125 		 *
126 		 * @param count
127 		 *            number of cache misses to record
128 		 */
129 		void recordMisses(int count);
130 
131 		/**
132 		 * Record a successful load of a cache entry
133 		 *
134 		 * @param loadTimeNanos
135 		 *            time to load a cache entry
136 		 */
137 		void recordLoadSuccess(long loadTimeNanos);
138 
139 		/**
140 		 * Record a failed load of a cache entry
141 		 *
142 		 * @param loadTimeNanos
143 		 *            time used trying to load a cache entry
144 		 */
145 		void recordLoadFailure(long loadTimeNanos);
146 
147 		/**
148 		 * Record cache evictions due to the cache evictions strategy
149 		 *
150 		 * @param count
151 		 *            number of evictions to record
152 		 */
153 		void recordEvictions(int count);
154 
155 		/**
156 		 * Record files opened by cache
157 		 *
158 		 * @param delta
159 		 *            delta of number of files opened by cache
160 		 */
161 		void recordOpenFiles(int delta);
162 
163 		/**
164 		 * Record cached bytes
165 		 *
166 		 * @param pack
167 		 *            pack file the bytes are read from
168 		 *
169 		 * @param delta
170 		 *            delta of cached bytes
171 		 */
172 		void recordOpenBytes(PackFile pack, int delta);
173 
174 		/**
175 		 * Returns a snapshot of this recorder's stats. Note that this may be an
176 		 * inconsistent view, as it may be interleaved with update operations.
177 		 *
178 		 * @return a snapshot of this recorder's stats
179 		 */
180 		@NonNull
181 		WindowCacheStats getStats();
182 	}
183 
184 	static class StatsRecorderImpl
185 			implements StatsRecorder, WindowCacheStats {
186 		private final LongAdder hitCount;
187 		private final LongAdder missCount;
188 		private final LongAdder loadSuccessCount;
189 		private final LongAdder loadFailureCount;
190 		private final LongAdder totalLoadTime;
191 		private final LongAdder evictionCount;
192 		private final LongAdder openFileCount;
193 		private final LongAdder openByteCount;
194 		private final Map<String, LongAdder> openByteCountPerRepository;
195 
196 		/**
197 		 * Constructs an instance with all counts initialized to zero.
198 		 */
199 		public StatsRecorderImpl() {
200 			hitCount = new LongAdder();
201 			missCount = new LongAdder();
202 			loadSuccessCount = new LongAdder();
203 			loadFailureCount = new LongAdder();
204 			totalLoadTime = new LongAdder();
205 			evictionCount = new LongAdder();
206 			openFileCount = new LongAdder();
207 			openByteCount = new LongAdder();
208 			openByteCountPerRepository = new ConcurrentHashMap<>();
209 		}
210 
211 		@Override
212 		public void recordHits(int count) {
213 			hitCount.add(count);
214 		}
215 
216 		@Override
217 		public void recordMisses(int count) {
218 			missCount.add(count);
219 		}
220 
221 		@Override
222 		public void recordLoadSuccess(long loadTimeNanos) {
223 			loadSuccessCount.increment();
224 			totalLoadTime.add(loadTimeNanos);
225 		}
226 
227 		@Override
228 		public void recordLoadFailure(long loadTimeNanos) {
229 			loadFailureCount.increment();
230 			totalLoadTime.add(loadTimeNanos);
231 		}
232 
233 		@Override
234 		public void recordEvictions(int count) {
235 			evictionCount.add(count);
236 		}
237 
238 		@Override
239 		public void recordOpenFiles(int delta) {
240 			openFileCount.add(delta);
241 		}
242 
243 		@Override
244 		public void recordOpenBytes(PackFile pack, int delta) {
245 			openByteCount.add(delta);
246 			String repositoryId = repositoryId(pack);
247 			LongAdder la = openByteCountPerRepository
248 					.computeIfAbsent(repositoryId, k -> new LongAdder());
249 			la.add(delta);
250 			if (delta < 0) {
251 				openByteCountPerRepository.computeIfPresent(repositoryId,
252 						(k, v) -> v.longValue() == 0 ? null : v);
253 			}
254 		}
255 
256 		private static String repositoryId(PackFile pack) {
257 			// use repository's gitdir since packfile doesn't know its
258 			// repository
259 			return pack.getPackFile().getParentFile().getParentFile()
260 					.getParent();
261 		}
262 
263 		@Override
264 		public WindowCacheStats getStats() {
265 			return this;
266 		}
267 
268 		@Override
269 		public long getHitCount() {
270 			return hitCount.sum();
271 		}
272 
273 		@Override
274 		public long getMissCount() {
275 			return missCount.sum();
276 		}
277 
278 		@Override
279 		public long getLoadSuccessCount() {
280 			return loadSuccessCount.sum();
281 		}
282 
283 		@Override
284 		public long getLoadFailureCount() {
285 			return loadFailureCount.sum();
286 		}
287 
288 		@Override
289 		public long getEvictionCount() {
290 			return evictionCount.sum();
291 		}
292 
293 		@Override
294 		public long getTotalLoadTime() {
295 			return totalLoadTime.sum();
296 		}
297 
298 		@Override
299 		public long getOpenFileCount() {
300 			return openFileCount.sum();
301 		}
302 
303 		@Override
304 		public long getOpenByteCount() {
305 			return openByteCount.sum();
306 		}
307 
308 		@Override
309 		public void resetCounters() {
310 			hitCount.reset();
311 			missCount.reset();
312 			loadSuccessCount.reset();
313 			loadFailureCount.reset();
314 			totalLoadTime.reset();
315 			evictionCount.reset();
316 		}
317 
318 		@Override
319 		public Map<String, Long> getOpenByteCountPerRepository() {
320 			return Collections.unmodifiableMap(
321 					openByteCountPerRepository.entrySet().stream()
322 							.collect(Collectors.toMap(Map.Entry::getKey,
323 									e -> Long.valueOf(e.getValue().sum()),
324 									(u, v) -> v)));
325 		}
326 	}
327 
328 	private static final int bits(int newSize) {
329 		if (newSize < 4096)
330 			throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
331 		if (Integer.bitCount(newSize) != 1)
332 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBePowerOf2);
333 		return Integer.numberOfTrailingZeros(newSize);
334 	}
335 
336 	private static final Random rng = new Random();
337 
338 	private static volatile WindowCache cache;
339 
340 	private static volatile int streamFileThreshold;
341 
342 	static {
343 		reconfigure(new WindowCacheConfig());
344 	}
345 
346 	/**
347 	 * Modify the configuration of the window cache.
348 	 * <p>
349 	 * The new configuration is applied immediately. If the new limits are
350 	 * smaller than what is currently cached, older entries will be purged
351 	 * as soon as possible to allow the cache to meet the new limit.
352 	 *
353 	 * @deprecated use {@code cfg.install()} to avoid internal reference.
354 	 * @param cfg
355 	 *            the new window cache configuration.
356 	 * @throws java.lang.IllegalArgumentException
357 	 *             the cache configuration contains one or more invalid
358 	 *             settings, usually too low of a limit.
359 	 */
360 	@Deprecated
361 	public static void reconfigure(WindowCacheConfig cfg) {
362 		final WindowCacheal/storage/file/WindowCache.html#WindowCache">WindowCache nc = new WindowCache(cfg);
363 		final WindowCache oc = cache;
364 		if (oc != null)
365 			oc.removeAll();
366 		cache = nc;
367 		streamFileThreshold = cfg.getStreamFileThreshold();
368 		DeltaBaseCache.reconfigure(cfg);
369 	}
370 
371 	static int getStreamFileThreshold() {
372 		return streamFileThreshold;
373 	}
374 
375 	/**
376 	 * @return the cached instance.
377 	 */
378 	public static WindowCache getInstance() {
379 		return cache;
380 	}
381 
382 	static final ByteWindow get(PackFile pack, long offset)
383 			throws IOException {
384 		final WindowCache c = cache;
385 		final ByteWindow r = c.getOrLoad(pack, c.toStart(offset));
386 		if (c != cache) {
387 			// The cache was reconfigured while we were using the old one
388 			// to load this window. The window is still valid, but our
389 			// cache may think its still live. Ensure the window is removed
390 			// from the old cache so resources can be released.
391 			//
392 			c.removeAll();
393 		}
394 		return r;
395 	}
396 
397 	static final void purge(PackFile pack) {
398 		cache.removeAll(pack);
399 	}
400 
401 	/** cleanup released and/or garbage collected windows. */
402 	private final CleanupQueue queue;
403 
404 	/** Number of entries in {@link #table}. */
405 	private final int tableSize;
406 
407 	/** Access clock for loose LRU. */
408 	private final AtomicLong clock;
409 
410 	/** Hash bucket directory; entries are chained below. */
411 	private final AtomicReferenceArray<Entry> table;
412 
413 	/** Locks to prevent concurrent loads for same (PackFile,position). */
414 	private final Lock[] locks;
415 
416 	/** Lock to elect the eviction thread after a load occurs. */
417 	private final ReentrantLock evictLock;
418 
419 	/** Number of {@link #table} buckets to scan for an eviction window. */
420 	private final int evictBatch;
421 
422 	private final int maxFiles;
423 
424 	private final long maxBytes;
425 
426 	private final boolean mmap;
427 
428 	private final int windowSizeShift;
429 
430 	private final int windowSize;
431 
432 	private final StatsRecorder statsRecorder;
433 
434 	private final StatsRecorderImpl mbean;
435 
436 	private boolean useStrongRefs;
437 
438 	private WindowCache(WindowCacheConfig cfg) {
439 		tableSize = tableSize(cfg);
440 		final int lockCount = lockCount(cfg);
441 		if (tableSize < 1)
442 			throw new IllegalArgumentException(JGitText.get().tSizeMustBeGreaterOrEqual1);
443 		if (lockCount < 1)
444 			throw new IllegalArgumentException(JGitText.get().lockCountMustBeGreaterOrEqual1);
445 
446 		clock = new AtomicLong(1);
447 		table = new AtomicReferenceArray<>(tableSize);
448 		locks = new Lock[lockCount];
449 		for (int i = 0; i < locks.length; i++)
450 			locks[i] = new Lock();
451 		evictLock = new ReentrantLock();
452 
453 		int eb = (int) (tableSize * .1);
454 		if (64 < eb)
455 			eb = 64;
456 		else if (eb < 4)
457 			eb = 4;
458 		if (tableSize < eb)
459 			eb = tableSize;
460 		evictBatch = eb;
461 
462 		maxFiles = cfg.getPackedGitOpenFiles();
463 		maxBytes = cfg.getPackedGitLimit();
464 		mmap = cfg.isPackedGitMMAP();
465 		windowSizeShift = bits(cfg.getPackedGitWindowSize());
466 		windowSize = 1 << windowSizeShift;
467 		useStrongRefs = cfg.isPackedGitUseStrongRefs();
468 		queue = useStrongRefs ? new StrongCleanupQueue(this)
469 				: new SoftCleanupQueue(this);
470 
471 		mbean = new StatsRecorderImpl();
472 		statsRecorder = mbean;
473 		if (cfg.getExposeStatsViaJmx()) {
474 			Monitoring.registerMBean(mbean, "block_cache"); //$NON-NLS-1$
475 		}
476 
477 		if (maxFiles < 1)
478 			throw new IllegalArgumentException(JGitText.get().openFilesMustBeAtLeast1);
479 		if (maxBytes < windowSize)
480 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
481 	}
482 
483 	/**
484 	 * @return cache statistics for the WindowCache
485 	 */
486 	public WindowCacheStats getStats() {
487 		return statsRecorder.getStats();
488 	}
489 
490 	/**
491 	 * Reset stats. Does not reset open bytes and open files stats.
492 	 */
493 	public void resetStats() {
494 		mbean.resetCounters();
495 	}
496 
497 	private int hash(int packHash, long off) {
498 		return packHash + (int) (off >>> windowSizeShift);
499 	}
500 
501 	private ByteWindow load(PackFile pack, long offset) throws IOException {
502 		long startTime = System.nanoTime();
503 		if (pack.beginWindowCache())
504 			statsRecorder.recordOpenFiles(1);
505 		try {
506 			if (mmap)
507 				return pack.mmap(offset, windowSize);
508 			ByteArrayWindow w = pack.read(offset, windowSize);
509 			statsRecorder.recordLoadSuccess(System.nanoTime() - startTime);
510 			return w;
511 		} catch (IOException | RuntimeException | Error e) {
512 			close(pack);
513 			statsRecorder.recordLoadFailure(System.nanoTime() - startTime);
514 			throw e;
515 		} finally {
516 			statsRecorder.recordMisses(1);
517 		}
518 	}
519 
520 	private PageRef<ByteWindow> createRef(PackFile p, long o, ByteWindow v) {
521 		final PageRef<ByteWindow> ref = useStrongRefs
522 				? new StrongRef(p, o, v, queue)
523 				: new SoftRef(p, o, v, (SoftCleanupQueue) queue);
524 		statsRecorder.recordOpenBytes(ref.getPack(), ref.getSize());
525 		return ref;
526 	}
527 
528 	private void clear(PageRef<ByteWindow> ref) {
529 		statsRecorder.recordOpenBytes(ref.getPack(), -ref.getSize());
530 		statsRecorder.recordEvictions(1);
531 		close(ref.getPack());
532 	}
533 
534 	private void close(PackFile pack) {
535 		if (pack.endWindowCache()) {
536 			statsRecorder.recordOpenFiles(-1);
537 		}
538 	}
539 
540 	private boolean isFull() {
541 		return maxFiles < mbean.getOpenFileCount()
542 				|| maxBytes < mbean.getOpenByteCount();
543 	}
544 
545 	private long toStart(long offset) {
546 		return (offset >>> windowSizeShift) << windowSizeShift;
547 	}
548 
549 	private static int tableSize(WindowCacheConfig cfg) {
550 		final int wsz = cfg.getPackedGitWindowSize();
551 		final long limit = cfg.getPackedGitLimit();
552 		if (wsz <= 0)
553 			throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
554 		if (limit < wsz)
555 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
556 		return (int) Math.min(5 * (limit / wsz) / 2, 2000000000);
557 	}
558 
559 	private static int lockCount(WindowCacheConfig cfg) {
560 		return Math.max(cfg.getPackedGitOpenFiles(), 32);
561 	}
562 
563 	/**
564 	 * Lookup a cached object, creating and loading it if it doesn't exist.
565 	 *
566 	 * @param pack
567 	 *            the pack that "contains" the cached object.
568 	 * @param position
569 	 *            offset within <code>pack</code> of the object.
570 	 * @return the object reference.
571 	 * @throws IOException
572 	 *             the object reference was not in the cache and could not be
573 	 *             obtained by {@link #load(PackFile, long)}.
574 	 */
575 	private ByteWindow getOrLoad(PackFile pack, long position)
576 			throws IOException {
577 		final int slot = slot(pack, position);
578 		final Entry e1 = table.get(slot);
579 		ByteWindow v = scan(e1, pack, position);
580 		if (v != null) {
581 			statsRecorder.recordHits(1);
582 			return v;
583 		}
584 
585 		synchronized (lock(pack, position)) {
586 			Entry e2 = table.get(slot);
587 			if (e2 != e1) {
588 				v = scan(e2, pack, position);
589 				if (v != null) {
590 					statsRecorder.recordHits(1);
591 					return v;
592 				}
593 			}
594 
595 			v = load(pack, position);
596 			final PageRef<ByteWindow> ref = createRef(pack, position, v);
597 			hit(ref);
598 			for (;;) {
599 				final Entry n = new Entry(clean(e2), ref);
600 				if (table.compareAndSet(slot, e2, n))
601 					break;
602 				e2 = table.get(slot);
603 			}
604 		}
605 
606 		if (evictLock.tryLock()) {
607 			try {
608 				gc();
609 				evict();
610 			} finally {
611 				evictLock.unlock();
612 			}
613 		}
614 
615 		return v;
616 	}
617 
618 	private ByteWindow scan(Entry n, PackFile pack, long position) {
619 		for (; n != null; n = n.next) {
620 			final PageRef<ByteWindow> r = n.ref;
621 			if (r.getPack() == pack && r.getPosition() == position) {
622 				final ByteWindow v = r.get();
623 				if (v != null) {
624 					hit(r);
625 					return v;
626 				}
627 				n.kill();
628 				break;
629 			}
630 		}
631 		return null;
632 	}
633 
634 	private void hit(PageRef r) {
635 		// We don't need to be 100% accurate here. Its sufficient that at least
636 		// one thread performs the increment. Any other concurrent access at
637 		// exactly the same time can simply use the same clock value.
638 		//
639 		// Consequently we attempt the set, but we don't try to recover should
640 		// it fail. This is why we don't use getAndIncrement() here.
641 		//
642 		final long c = clock.get();
643 		clock.compareAndSet(c, c + 1);
644 		r.setLastAccess(c);
645 	}
646 
647 	private void evict() {
648 		while (isFull()) {
649 			int ptr = rng.nextInt(tableSize);
650 			Entry old = null;
651 			int slot = 0;
652 			for (int b = evictBatch - 1; b >= 0; b--, ptr++) {
653 				if (tableSize <= ptr)
654 					ptr = 0;
655 				for (Entry e = table.get(ptr); e != null; e = e.next) {
656 					if (e.dead)
657 						continue;
658 					if (old == null || e.ref.getLastAccess() < old.ref
659 							.getLastAccess()) {
660 						old = e;
661 						slot = ptr;
662 					}
663 				}
664 			}
665 			if (old != null) {
666 				old.kill();
667 				gc();
668 				final Entry e1 = table.get(slot);
669 				table.compareAndSet(slot, e1, clean(e1));
670 			}
671 		}
672 	}
673 
674 	/**
675 	 * Clear every entry from the cache.
676 	 * <p>
677 	 * This is a last-ditch effort to clear out the cache, such as before it
678 	 * gets replaced by another cache that is configured differently. This
679 	 * method tries to force every cached entry through {@link #clear(PageRef)} to
680 	 * ensure that resources are correctly accounted for and cleaned up by the
681 	 * subclass. A concurrent reader loading entries while this method is
682 	 * running may cause resource accounting failures.
683 	 */
684 	private void removeAll() {
685 		for (int s = 0; s < tableSize; s++) {
686 			Entry e1;
687 			do {
688 				e1 = table.get(s);
689 				for (Entry e = e1; e != null; e = e.next)
690 					e.kill();
691 			} while (!table.compareAndSet(s, e1, null));
692 		}
693 		gc();
694 	}
695 
696 	/**
697 	 * Clear all entries related to a single file.
698 	 * <p>
699 	 * Typically this method is invoked during {@link PackFile#close()}, when we
700 	 * know the pack is never going to be useful to us again (for example, it no
701 	 * longer exists on disk). A concurrent reader loading an entry from this
702 	 * same pack may cause the pack to become stuck in the cache anyway.
703 	 *
704 	 * @param pack
705 	 *            the file to purge all entries of.
706 	 */
707 	private void removeAll(PackFile pack) {
708 		for (int s = 0; s < tableSize; s++) {
709 			final Entry e1 = table.get(s);
710 			boolean hasDead = false;
711 			for (Entry e = e1; e != null; e = e.next) {
712 				if (e.ref.getPack() == pack) {
713 					e.kill();
714 					hasDead = true;
715 				} else if (e.dead)
716 					hasDead = true;
717 			}
718 			if (hasDead)
719 				table.compareAndSet(s, e1, clean(e1));
720 		}
721 		gc();
722 	}
723 
724 	private void gc() {
725 		queue.gc();
726 	}
727 
728 	private int slot(PackFile pack, long position) {
729 		return (hash(pack.hash, position) >>> 1) % tableSize;
730 	}
731 
732 	private Lock lock(PackFile pack, long position) {
733 		return locks[(hash(pack.hash, position) >>> 1) % locks.length];
734 	}
735 
736 	private static Entry clean(Entry top) {
737 		while (top != null && top.dead) {
738 			top.ref.kill();
739 			top = top.next;
740 		}
741 		if (top == null)
742 			return null;
743 		final Entry n = clean(top.next);
744 		return n == top.next ? top : new Entry(n, top.ref);
745 	}
746 
747 	private static class Entry {
748 		/** Next entry in the hash table's chain list. */
749 		final Entry next;
750 
751 		/** The referenced object. */
752 		final PageRef<ByteWindow> ref;
753 
754 		/**
755 		 * Marked true when ref.get() returns null and the ref is dead.
756 		 * <p>
757 		 * A true here indicates that the ref is no longer accessible, and that
758 		 * we therefore need to eventually purge this Entry object out of the
759 		 * bucket's chain.
760 		 */
761 		volatile boolean dead;
762 
763 		Entry(Entry n, PageRef<ByteWindow> r) {
764 			next = n;
765 			ref = r;
766 		}
767 
768 		final void kill() {
769 			dead = true;
770 			ref.kill();
771 		}
772 	}
773 
774 	private static interface PageRef<T> {
775 		/**
776 		 * Returns this reference object's referent. If this reference object
777 		 * has been cleared, either by the program or by the garbage collector,
778 		 * then this method returns <code>null</code>.
779 		 *
780 		 * @return The object to which this reference refers, or
781 		 *         <code>null</code> if this reference object has been cleared
782 		 */
783 		T get();
784 
785 	    /**
786 		 * Kill this ref
787 		 *
788 		 * @return <code>true</code> if this reference object was successfully
789 		 *         killed; <code>false</code> if it was already killed
790 		 */
791 		boolean kill();
792 
793 		/**
794 		 * Get the packfile the referenced cache page is allocated for
795 		 *
796 		 * @return the packfile the referenced cache page is allocated for
797 		 */
798 		PackFile getPack();
799 
800 		/**
801 		 * Get the position of the referenced cache page in the packfile
802 		 *
803 		 * @return the position of the referenced cache page in the packfile
804 		 */
805 		long getPosition();
806 
807 		/**
808 		 * Get size of cache page
809 		 *
810 		 * @return size of cache page
811 		 */
812 		int getSize();
813 
814 		/**
815 		 * Get pseudo time of last access to this cache page
816 		 *
817 		 * @return pseudo time of last access to this cache page
818 		 */
819 		long getLastAccess();
820 
821 		/**
822 		 * Set pseudo time of last access to this cache page
823 		 *
824 		 * @param time
825 		 *            pseudo time of last access to this cache page
826 		 */
827 		void setLastAccess(long time);
828 
829 		/**
830 		 * Whether this is a strong reference.
831 		 * @return {@code true} if this is a strong reference
832 		 */
833 		boolean isStrongRef();
834 	}
835 
836 	/** A soft reference wrapped around a cached object. */
837 	private static class SoftRef extends SoftReference<ByteWindow>
838 			implements PageRef<ByteWindow> {
839 		private final PackFile pack;
840 
841 		private final long position;
842 
843 		private final int size;
844 
845 		private long lastAccess;
846 
847 		protected SoftRef(final PackFile pack, final long position,
848 				final ByteWindow v, final SoftCleanupQueue queue) {
849 			super(v, queue);
850 			this.pack = pack;
851 			this.position = position;
852 			this.size = v.size();
853 		}
854 
855 		@Override
856 		public PackFile getPack() {
857 			return pack;
858 		}
859 
860 		@Override
861 		public long getPosition() {
862 			return position;
863 		}
864 
865 		@Override
866 		public int getSize() {
867 			return size;
868 		}
869 
870 		@Override
871 		public long getLastAccess() {
872 			return lastAccess;
873 		}
874 
875 		@Override
876 		public void setLastAccess(long time) {
877 			this.lastAccess = time;
878 		}
879 
880 		@Override
881 		public boolean kill() {
882 			return enqueue();
883 		}
884 
885 		@Override
886 		public boolean isStrongRef() {
887 			return false;
888 		}
889 	}
890 
891 	/** A strong reference wrapped around a cached object. */
892 	private static class StrongRef implements PageRef<ByteWindow> {
893 		private ByteWindow referent;
894 
895 		private final PackFile pack;
896 
897 		private final long position;
898 
899 		private final int size;
900 
901 		private long lastAccess;
902 
903 		private CleanupQueue queue;
904 
905 		protected StrongRef(final PackFile pack, final long position,
906 				final ByteWindow v, final CleanupQueue queue) {
907 			this.pack = pack;
908 			this.position = position;
909 			this.referent = v;
910 			this.size = v.size();
911 			this.queue = queue;
912 		}
913 
914 		@Override
915 		public PackFile getPack() {
916 			return pack;
917 		}
918 
919 		@Override
920 		public long getPosition() {
921 			return position;
922 		}
923 
924 		@Override
925 		public int getSize() {
926 			return size;
927 		}
928 
929 		@Override
930 		public long getLastAccess() {
931 			return lastAccess;
932 		}
933 
934 		@Override
935 		public void setLastAccess(long time) {
936 			this.lastAccess = time;
937 		}
938 
939 		@Override
940 		public ByteWindow get() {
941 			return referent;
942 		}
943 
944 		@Override
945 		public boolean kill() {
946 			if (referent == null) {
947 				return false;
948 			}
949 			referent = null;
950 			return queue.enqueue(this);
951 		}
952 
953 		@Override
954 		public boolean isStrongRef() {
955 			return true;
956 		}
957 	}
958 
959 	private static interface CleanupQueue {
960 		boolean enqueue(PageRef<ByteWindow> r);
961 		void gc();
962 	}
963 
964 	private static class SoftCleanupQueue extends ReferenceQueue<ByteWindow>
965 			implements CleanupQueue {
966 		private final WindowCache wc;
967 
968 		SoftCleanupQueue(WindowCache cache) {
969 			this.wc = cache;
970 		}
971 
972 		@Override
973 		public boolean enqueue(PageRef<ByteWindow> r) {
974 			// no need to explicitly add soft references which are enqueued by
975 			// the JVM
976 			return false;
977 		}
978 
979 		@Override
980 		public void gc() {
981 			SoftRef r;
982 			while ((r = (SoftRef) poll()) != null) {
983 				wc.clear(r);
984 
985 				final int s = wc.slot(r.getPack(), r.getPosition());
986 				final Entry e1 = wc.table.get(s);
987 				for (Entry n = e1; n != null; n = n.next) {
988 					if (n.ref == r) {
989 						n.dead = true;
990 						wc.table.compareAndSet(s, e1, clean(e1));
991 						break;
992 					}
993 				}
994 			}
995 		}
996 	}
997 
998 	private static class StrongCleanupQueue implements CleanupQueue {
999 		private final WindowCache wc;
1000 
1001 		private final ConcurrentLinkedQueue<PageRef<ByteWindow>> queue = new ConcurrentLinkedQueue<>();
1002 
1003 		StrongCleanupQueue(WindowCache wc) {
1004 			this.wc = wc;
1005 		}
1006 
1007 		@Override
1008 		public boolean enqueue(PageRef<ByteWindow> r) {
1009 			if (queue.contains(r)) {
1010 				return false;
1011 			}
1012 			return queue.add(r);
1013 		}
1014 
1015 		@Override
1016 		public void gc() {
1017 			PageRef<ByteWindow> r;
1018 			while ((r = queue.poll()) != null) {
1019 				wc.clear(r);
1020 
1021 				final int s = wc.slot(r.getPack(), r.getPosition());
1022 				final Entry e1 = wc.table.get(s);
1023 				for (Entry n = e1; n != null; n = n.next) {
1024 					if (n.ref == r) {
1025 						n.dead = true;
1026 						wc.table.compareAndSet(s, e1, clean(e1));
1027 						break;
1028 					}
1029 				}
1030 			}
1031 		}
1032 	}
1033 
1034 	private static final class Lock {
1035 		// Used only for its implicit monitor.
1036 	}
1037 }