| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.hadoop.hbase.regionserver; |
| |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.BlockingQueue; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.concurrent.atomic.LongAdder; |
| |
| import org.apache.yetus.audience.InterfaceAudience; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.apache.hadoop.hbase.regionserver.HeapMemoryManager.HeapMemoryTuneObserver; |
| import org.apache.hadoop.hbase.util.Bytes; |
| import org.apache.hadoop.util.StringUtils; |
| |
| import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; |
| import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; |
| |
| /** |
| * Does the management of memstoreLAB chunk creations. A monotonically incrementing id is associated |
| * with every chunk |
| */ |
| @InterfaceAudience.Private |
| public class ChunkCreator { |
| private static final Logger LOG = LoggerFactory.getLogger(ChunkCreator.class); |
| // monotonically increasing chunkid. Starts at 1. |
| private AtomicInteger chunkID = new AtomicInteger(1); |
| // maps the chunk against the monotonically increasing chunk id. We need to preserve the |
| // natural ordering of the key |
| // CellChunkMap creation should convert the weak ref to hard reference |
| |
| // chunk id of each chunk is the first integer written on each chunk, |
| // the header size need to be changed in case chunk id size is changed |
| public static final int SIZEOF_CHUNK_HEADER = Bytes.SIZEOF_INT; |
| |
| /** |
| * Types of chunks, based on their sizes |
| */ |
| public enum ChunkType { |
| // An index chunk is a small chunk, allocated from the index chunks pool. |
| // Its size is fixed and is 10% of the size of a data chunk. |
| INDEX_CHUNK, |
| // A data chunk is a regular chunk, allocated from the data chunks pool. |
| // Its size is fixed and given as input to the ChunkCreator c'tor. |
| DATA_CHUNK, |
| // A jumbo chunk isn't allocated from pool. Its size is bigger than the size of a |
| // data chunk, and is determined per chunk (meaning, there is no fixed jumbo size). |
| JUMBO_CHUNK |
| } |
| |
| // mapping from chunk IDs to chunks |
| private Map<Integer, Chunk> chunkIdMap = new ConcurrentHashMap<Integer, Chunk>(); |
| |
| private final boolean offheap; |
| @VisibleForTesting |
| static ChunkCreator instance; |
| @VisibleForTesting |
| static boolean chunkPoolDisabled = false; |
| private MemStoreChunkPool dataChunksPool; |
| private final int chunkSize; |
| private MemStoreChunkPool indexChunksPool; |
| |
| @VisibleForTesting |
| ChunkCreator(int chunkSize, boolean offheap, long globalMemStoreSize, float poolSizePercentage, |
| float initialCountPercentage, HeapMemoryManager heapMemoryManager, |
| float indexChunkSizePercentage) { |
| this.offheap = offheap; |
| this.chunkSize = chunkSize; // in case pools are not allocated |
| initializePools(chunkSize, globalMemStoreSize, poolSizePercentage, indexChunkSizePercentage, |
| initialCountPercentage, heapMemoryManager); |
| } |
| |
| @VisibleForTesting |
| private void initializePools(int chunkSize, long globalMemStoreSize, |
| float poolSizePercentage, float indexChunkSizePercentage, |
| float initialCountPercentage, |
| HeapMemoryManager heapMemoryManager) { |
| this.dataChunksPool = initializePool("data", globalMemStoreSize, |
| (1 - indexChunkSizePercentage) * poolSizePercentage, |
| initialCountPercentage, chunkSize, heapMemoryManager); |
| // The index chunks pool is needed only when the index type is CCM. |
| // Since the pools are not created at all when the index type isn't CCM, |
| // we don't need to check it here. |
| this.indexChunksPool = initializePool("index", globalMemStoreSize, |
| indexChunkSizePercentage * poolSizePercentage, |
| initialCountPercentage, (int) (indexChunkSizePercentage * chunkSize), |
| heapMemoryManager); |
| } |
| |
| /** |
| * Initializes the instance of ChunkCreator |
| * @param chunkSize the chunkSize |
| * @param offheap indicates if the chunk is to be created offheap or not |
| * @param globalMemStoreSize the global memstore size |
| * @param poolSizePercentage pool size percentage |
| * @param initialCountPercentage the initial count of the chunk pool if any |
| * @param heapMemoryManager the heapmemory manager |
| * @return singleton MSLABChunkCreator |
| */ |
| @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "LI_LAZY_INIT_STATIC", |
| justification = "Method is called by single thread at the starting of RS") |
| @VisibleForTesting |
| public static ChunkCreator initialize(int chunkSize, boolean offheap, long globalMemStoreSize, |
| float poolSizePercentage, float initialCountPercentage, |
| HeapMemoryManager heapMemoryManager, |
| float indexChunkSizePercent) { |
| if (instance != null) { |
| return instance; |
| } |
| instance = new ChunkCreator(chunkSize, offheap, globalMemStoreSize, poolSizePercentage, |
| initialCountPercentage, heapMemoryManager, indexChunkSizePercent); |
| return instance; |
| } |
| |
| @VisibleForTesting |
| public static ChunkCreator getInstance() { |
| return instance; |
| } |
| |
| /** |
| * Creates and inits a chunk. The default implementation for a specific chunk size. |
| * @return the chunk that was initialized |
| */ |
| Chunk getChunk(ChunkType chunkType) { |
| return getChunk(CompactingMemStore.IndexType.ARRAY_MAP, chunkType); |
| } |
| |
| /** |
| * Creates and inits a chunk. The default implementation. |
| * @return the chunk that was initialized |
| */ |
| Chunk getChunk() { |
| return getChunk(CompactingMemStore.IndexType.ARRAY_MAP, ChunkType.DATA_CHUNK); |
| } |
| |
| /** |
| * Creates and inits a chunk. The default implementation for a specific index type. |
| * @return the chunk that was initialized |
| */ |
| Chunk getChunk(CompactingMemStore.IndexType chunkIndexType) { |
| return getChunk(chunkIndexType, ChunkType.DATA_CHUNK); |
| } |
| |
| /** |
| * Creates and inits a chunk with specific index type and type. |
| * @return the chunk that was initialized |
| */ |
| Chunk getChunk(CompactingMemStore.IndexType chunkIndexType, ChunkType chunkType) { |
| switch (chunkType) { |
| case INDEX_CHUNK: |
| if (indexChunksPool != null) { |
| return getChunk(chunkIndexType, indexChunksPool.getChunkSize()); |
| } |
| case DATA_CHUNK: |
| if (dataChunksPool == null) { |
| return getChunk(chunkIndexType, chunkSize); |
| } else { |
| return getChunk(chunkIndexType, dataChunksPool.getChunkSize()); |
| } |
| default: |
| throw new IllegalArgumentException( |
| "chunkType must either be INDEX_CHUNK or DATA_CHUNK"); |
| } |
| } |
| |
| /** |
| * Creates and inits a chunk. |
| * @return the chunk that was initialized |
| * @param chunkIndexType whether the requested chunk is going to be used with CellChunkMap index |
| * @param size the size of the chunk to be allocated, in bytes |
| */ |
| Chunk getChunk(CompactingMemStore.IndexType chunkIndexType, int size) { |
| Chunk chunk = null; |
| MemStoreChunkPool pool = null; |
| |
| // if the size is suitable for one of the pools |
| if (dataChunksPool != null && size == dataChunksPool.getChunkSize()) { |
| pool = dataChunksPool; |
| } else if (indexChunksPool != null && size == indexChunksPool.getChunkSize()) { |
| pool = indexChunksPool; |
| } |
| |
| // if we have a pool |
| if (pool != null) { |
| // the pool creates the chunk internally. The chunk#init() call happens here |
| chunk = pool.getChunk(); |
| // the pool has run out of maxCount |
| if (chunk == null) { |
| if (LOG.isTraceEnabled()) { |
| LOG.trace("The chunk pool is full. Reached maxCount= " + pool.getMaxCount() |
| + ". Creating chunk onheap."); |
| } |
| } |
| } |
| |
| if (chunk == null) { |
| // the second parameter explains whether CellChunkMap index is requested, |
| // in that case, put allocated on demand chunk mapping into chunkIdMap |
| chunk = createChunk(false, chunkIndexType, size); |
| } |
| |
| // now we need to actually do the expensive memory allocation step in case of a new chunk, |
| // else only the offset is set to the beginning of the chunk to accept allocations |
| chunk.init(); |
| return chunk; |
| } |
| |
| /** |
| * Creates and inits a chunk of a special size, bigger than a regular chunk size. |
| * Such a chunk will never come from pool and will always be on demand allocated. |
| * @return the chunk that was initialized |
| * @param jumboSize the special size to be used |
| */ |
| Chunk getJumboChunk(int jumboSize) { |
| int allocSize = jumboSize + SIZEOF_CHUNK_HEADER; |
| if (allocSize <= dataChunksPool.getChunkSize()) { |
| LOG.warn("Jumbo chunk size " + jumboSize + " must be more than regular chunk size " |
| + dataChunksPool.getChunkSize() + ". Converting to regular chunk."); |
| return getChunk(CompactingMemStore.IndexType.CHUNK_MAP); |
| } |
| // the new chunk is going to hold the jumbo cell data and needs to be referenced by |
| // a strong map. Therefore the CCM index type |
| return getChunk(CompactingMemStore.IndexType.CHUNK_MAP, allocSize); |
| } |
| |
| /** |
| * Creates the chunk either onheap or offheap |
| * @param pool indicates if the chunks have to be created which will be used by the Pool |
| * @param chunkIndexType whether the requested chunk is going to be used with CellChunkMap index |
| * @param size the size of the chunk to be allocated, in bytes |
| * @return the chunk |
| */ |
| private Chunk createChunk(boolean pool, CompactingMemStore.IndexType chunkIndexType, int size) { |
| Chunk chunk = null; |
| int id = chunkID.getAndIncrement(); |
| assert id > 0; |
| // do not create offheap chunk on demand |
| if (pool && this.offheap) { |
| chunk = new OffheapChunk(size, id, pool); |
| } else { |
| chunk = new OnheapChunk(size, id, pool); |
| } |
| if (pool || (chunkIndexType == CompactingMemStore.IndexType.CHUNK_MAP)) { |
| // put the pool chunk into the chunkIdMap so it is not GC-ed |
| this.chunkIdMap.put(chunk.getId(), chunk); |
| } |
| return chunk; |
| } |
| |
| // Chunks from pool are created covered with strong references anyway |
| // TODO: change to CHUNK_MAP if it is generally defined |
| private Chunk createChunkForPool(CompactingMemStore.IndexType chunkIndexType, int chunkSize) { |
| if (chunkSize != dataChunksPool.getChunkSize() && |
| chunkSize != indexChunksPool.getChunkSize()) { |
| return null; |
| } |
| return createChunk(true, chunkIndexType, chunkSize); |
| } |
| |
| @VisibleForTesting |
| // Used to translate the ChunkID into a chunk ref |
| Chunk getChunk(int id) { |
| // can return null if chunk was never mapped |
| return chunkIdMap.get(id); |
| } |
| |
| boolean isOffheap() { |
| return this.offheap; |
| } |
| |
| private void removeChunks(Set<Integer> chunkIDs) { |
| this.chunkIdMap.keySet().removeAll(chunkIDs); |
| } |
| |
| Chunk removeChunk(int chunkId) { |
| return this.chunkIdMap.remove(chunkId); |
| } |
| |
| @VisibleForTesting |
| // the chunks in the chunkIdMap may already be released so we shouldn't relay |
| // on this counting for strong correctness. This method is used only in testing. |
| int numberOfMappedChunks() { |
| return this.chunkIdMap.size(); |
| } |
| |
| @VisibleForTesting |
| void clearChunkIds() { |
| this.chunkIdMap.clear(); |
| } |
| |
| /** |
| * A pool of {@link Chunk} instances. |
| * |
| * MemStoreChunkPool caches a number of retired chunks for reusing, it could |
| * decrease allocating bytes when writing, thereby optimizing the garbage |
| * collection on JVM. |
| */ |
| private class MemStoreChunkPool implements HeapMemoryTuneObserver { |
| private final int chunkSize; |
| private int maxCount; |
| |
| // A queue of reclaimed chunks |
| private final BlockingQueue<Chunk> reclaimedChunks; |
| private final float poolSizePercentage; |
| |
| /** Statistics thread schedule pool */ |
| private final ScheduledExecutorService scheduleThreadPool; |
| /** Statistics thread */ |
| private static final int statThreadPeriod = 60 * 5; |
| private final AtomicLong chunkCount = new AtomicLong(); |
| private final LongAdder reusedChunkCount = new LongAdder(); |
| private final String label; |
| |
| MemStoreChunkPool(String label, int chunkSize, int maxCount, int initialCount, |
| float poolSizePercentage) { |
| this.label = label; |
| this.chunkSize = chunkSize; |
| this.maxCount = maxCount; |
| this.poolSizePercentage = poolSizePercentage; |
| this.reclaimedChunks = new LinkedBlockingQueue<>(); |
| for (int i = 0; i < initialCount; i++) { |
| Chunk chunk = createChunk(true, CompactingMemStore.IndexType.ARRAY_MAP, chunkSize); |
| chunk.init(); |
| reclaimedChunks.add(chunk); |
| } |
| chunkCount.set(initialCount); |
| final String n = Thread.currentThread().getName(); |
| scheduleThreadPool = Executors.newScheduledThreadPool(1, new ThreadFactoryBuilder() |
| .setNameFormat(n + "-MemStoreChunkPool Statistics").setDaemon(true).build()); |
| this.scheduleThreadPool.scheduleAtFixedRate(new StatisticsThread(), statThreadPeriod, |
| statThreadPeriod, TimeUnit.SECONDS); |
| } |
| |
| /** |
| * Poll a chunk from the pool, reset it if not null, else create a new chunk to return if we have |
| * not yet created max allowed chunks count. When we have already created max allowed chunks and |
| * no free chunks as of now, return null. It is the responsibility of the caller to make a chunk |
| * then. |
| * Note: Chunks returned by this pool must be put back to the pool after its use. |
| * @return a chunk |
| * @see #putbackChunks(Chunk) |
| */ |
| Chunk getChunk() { |
| return getChunk(CompactingMemStore.IndexType.ARRAY_MAP); |
| } |
| |
| Chunk getChunk(CompactingMemStore.IndexType chunkIndexType) { |
| Chunk chunk = reclaimedChunks.poll(); |
| if (chunk != null) { |
| chunk.reset(); |
| reusedChunkCount.increment(); |
| } else { |
| // Make a chunk iff we have not yet created the maxCount chunks |
| while (true) { |
| long created = this.chunkCount.get(); |
| if (created < this.maxCount) { |
| if (this.chunkCount.compareAndSet(created, created + 1)) { |
| chunk = createChunkForPool(chunkIndexType, chunkSize); |
| break; |
| } |
| } else { |
| break; |
| } |
| } |
| } |
| return chunk; |
| } |
| |
| int getChunkSize() { |
| return chunkSize; |
| } |
| |
| /** |
| * Add the chunks to the pool, when the pool achieves the max size, it will skip the remaining |
| * chunks |
| * @param c |
| */ |
| private void putbackChunks(Chunk c) { |
| int toAdd = this.maxCount - reclaimedChunks.size(); |
| if (c.isFromPool() && c.size == chunkSize && toAdd > 0) { |
| reclaimedChunks.add(c); |
| } else { |
| // remove the chunk (that is not going to pool) |
| // though it is initially from the pool or not |
| ChunkCreator.this.removeChunk(c.getId()); |
| } |
| } |
| |
| private class StatisticsThread extends Thread { |
| StatisticsThread() { |
| super("MemStoreChunkPool.StatisticsThread"); |
| setDaemon(true); |
| } |
| |
| @Override |
| public void run() { |
| logStats(); |
| } |
| |
| private void logStats() { |
| if (!LOG.isDebugEnabled()) return; |
| long created = chunkCount.get(); |
| long reused = reusedChunkCount.sum(); |
| long total = created + reused; |
| LOG.debug("{} stats (chunk size={}): current pool size={}, created chunk count={}, " + |
| "reused chunk count={}, reuseRatio={}", label, chunkSize, reclaimedChunks.size(), |
| created, reused, |
| (total == 0? "0": StringUtils.formatPercent((float)reused/(float)total,2))); |
| } |
| } |
| |
| private int getMaxCount() { |
| return this.maxCount; |
| } |
| |
| @Override |
| public void onHeapMemoryTune(long newMemstoreSize, long newBlockCacheSize) { |
| // don't do any tuning in case of offheap memstore |
| if (isOffheap()) { |
| LOG.warn("{} not tuning the chunk pool as it is offheap", label); |
| return; |
| } |
| int newMaxCount = |
| (int) (newMemstoreSize * poolSizePercentage / getChunkSize()); |
| if (newMaxCount != this.maxCount) { |
| // We need an adjustment in the chunks numbers |
| if (newMaxCount > this.maxCount) { |
| // Max chunks getting increased. Just change the variable. Later calls to getChunk() would |
| // create and add them to Q |
| LOG.info("{} max count for chunks increased from {} to {}", this.label, this.maxCount, |
| newMaxCount); |
| this.maxCount = newMaxCount; |
| } else { |
| // Max chunks getting decreased. We may need to clear off some of the pooled chunks now |
| // itself. If the extra chunks are serving already, do not pool those when we get them back |
| LOG.info("{} max count for chunks decreased from {} to {}", this.label, this.maxCount, |
| newMaxCount); |
| this.maxCount = newMaxCount; |
| if (this.reclaimedChunks.size() > newMaxCount) { |
| synchronized (this) { |
| while (this.reclaimedChunks.size() > newMaxCount) { |
| this.reclaimedChunks.poll(); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| static void clearDisableFlag() { |
| chunkPoolDisabled = false; |
| } |
| |
| private MemStoreChunkPool initializePool(String label, long globalMemStoreSize, |
| float poolSizePercentage, float initialCountPercentage, int chunkSize, |
| HeapMemoryManager heapMemoryManager) { |
| if (poolSizePercentage <= 0) { |
| LOG.info("{} poolSizePercentage is less than 0. So not using pool", label); |
| return null; |
| } |
| if (chunkPoolDisabled) { |
| return null; |
| } |
| if (poolSizePercentage > 1.0) { |
| throw new IllegalArgumentException( |
| MemStoreLAB.CHUNK_POOL_MAXSIZE_KEY + " must be between 0.0 and 1.0"); |
| } |
| int maxCount = (int) (globalMemStoreSize * poolSizePercentage / chunkSize); |
| if (initialCountPercentage > 1.0 || initialCountPercentage < 0) { |
| throw new IllegalArgumentException(label + " " + MemStoreLAB.CHUNK_POOL_INITIALSIZE_KEY + |
| " must be between 0.0 and 1.0"); |
| } |
| int initialCount = (int) (initialCountPercentage * maxCount); |
| LOG.info("Allocating {} MemStoreChunkPool with chunk size {}, max count {}, initial count {}", |
| label, StringUtils.byteDesc(chunkSize), maxCount, initialCount); |
| MemStoreChunkPool memStoreChunkPool = new MemStoreChunkPool(label, chunkSize, maxCount, |
| initialCount, poolSizePercentage); |
| if (heapMemoryManager != null && memStoreChunkPool != null) { |
| // Register with Heap Memory manager |
| heapMemoryManager.registerTuneObserver(memStoreChunkPool); |
| } |
| return memStoreChunkPool; |
| } |
| |
| @VisibleForTesting |
| int getMaxCount() { |
| return getMaxCount(ChunkType.DATA_CHUNK); |
| } |
| |
| @VisibleForTesting |
| int getMaxCount(ChunkType chunkType) { |
| switch (chunkType) { |
| case INDEX_CHUNK: |
| if (indexChunksPool != null) { |
| return indexChunksPool.getMaxCount(); |
| } |
| break; |
| case DATA_CHUNK: |
| if (dataChunksPool != null) { |
| return dataChunksPool.getMaxCount(); |
| } |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "chunkType must either be INDEX_CHUNK or DATA_CHUNK"); |
| } |
| |
| return 0; |
| } |
| |
| @VisibleForTesting |
| int getPoolSize() { |
| return getPoolSize(ChunkType.DATA_CHUNK); |
| } |
| |
| @VisibleForTesting |
| int getPoolSize(ChunkType chunkType) { |
| switch (chunkType) { |
| case INDEX_CHUNK: |
| if (indexChunksPool != null) { |
| return indexChunksPool.reclaimedChunks.size(); |
| } |
| break; |
| case DATA_CHUNK: |
| if (dataChunksPool != null) { |
| return dataChunksPool.reclaimedChunks.size(); |
| } |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "chunkType must either be INDEX_CHUNK or DATA_CHUNK"); |
| } |
| return 0; |
| } |
| |
| @VisibleForTesting |
| boolean isChunkInPool(int chunkId) { |
| Chunk c = getChunk(chunkId); |
| if (c==null) { |
| return false; |
| } |
| |
| // chunks that are from pool will return true chunk reference not null |
| if (dataChunksPool != null && dataChunksPool.reclaimedChunks.contains(c)) { |
| return true; |
| } else if (indexChunksPool != null && indexChunksPool.reclaimedChunks.contains(c)) { |
| return true; |
| } |
| return false; |
| } |
| |
| /* |
| * Only used in testing |
| */ |
| @VisibleForTesting |
| void clearChunksInPool() { |
| if (dataChunksPool != null) { |
| dataChunksPool.reclaimedChunks.clear(); |
| } |
| if (indexChunksPool != null) { |
| indexChunksPool.reclaimedChunks.clear(); |
| } |
| } |
| |
| int getChunkSize() { |
| return getChunkSize(ChunkType.DATA_CHUNK); |
| } |
| |
| int getChunkSize(ChunkType chunkType) { |
| switch (chunkType) { |
| case INDEX_CHUNK: |
| if (indexChunksPool != null) { |
| return indexChunksPool.getChunkSize(); |
| } |
| case DATA_CHUNK: |
| if (dataChunksPool != null) { |
| return dataChunksPool.getChunkSize(); |
| } else { // When pools are empty |
| return chunkSize; |
| } |
| default: |
| throw new IllegalArgumentException( |
| "chunkType must either be INDEX_CHUNK or DATA_CHUNK"); |
| } |
| } |
| |
| synchronized void putbackChunks(Set<Integer> chunks) { |
| // if there is no pool just try to clear the chunkIdMap in case there is something |
| if (dataChunksPool == null && indexChunksPool == null) { |
| this.removeChunks(chunks); |
| return; |
| } |
| |
| // if there is a pool, go over all chunk IDs that came back, the chunks may be from pool or not |
| for (int chunkID : chunks) { |
| // translate chunk ID to chunk, if chunk initially wasn't in pool |
| // this translation will (most likely) return null |
| Chunk chunk = ChunkCreator.this.getChunk(chunkID); |
| if (chunk != null) { |
| if (chunk.isFromPool() && chunk.isIndexChunk()) { |
| indexChunksPool.putbackChunks(chunk); |
| } else if (chunk.isFromPool() && chunk.size == dataChunksPool.getChunkSize()) { |
| dataChunksPool.putbackChunks(chunk); |
| } else { |
| // chunks which are not from one of the pools |
| // should be released without going to the pools. |
| // Removing them from chunkIdMap will cause their removal by the GC. |
| this.removeChunk(chunkID); |
| } |
| } |
| // if chunk is null, it was never covered by the chunkIdMap (and so wasn't in pool also), |
| // so we have nothing to do on its release |
| } |
| return; |
| } |
| |
| } |
| |