/* * Copyright 2006-2012 ICEsoft Technologies Inc. * * Licensed 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.icepdf.core.util; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * The <code>MemoryManager</code> class is a utility to help manage the amount of memory * available to the application. When memory intensive operations are about * occur, the <code>MemoryManager</code> is asked if it can allocate the needed amount of memory. * If there is not enough memory available, the <code>MemoryManager</code> will purge the * cache to try and free the requested amount of memory. */ public class MemoryManager { private static final Logger logger = Logger.getLogger(MemoryManager.class.toString()); // internal reference to MemeoryMangaer, used for singleton pattern. private static MemoryManager instance; /** * Runtime object responsible for returning VM memory use information */ protected final Runtime runtime = Runtime.getRuntime(); /** * The minimum amount of free memory at which the memory manager will force * a purge of the cached pageTree. This value can be set by the system * property org.icepdf.core.minMemory */ // Old default was 300000, but PageTree would set it to 3000000, to help // ensure that parsing font glyphs will not result in a memory exception protected long minMemory = 300000; /** * The maximum amount of memory allocated to the JVM. */ protected long maxMemory = runtime.maxMemory(); /** * When we decide to reduce our memory footprint, this is how many items we * purge at once. */ protected int purgeSize; /** * If a memory-based ceiling, like maxMemory, is not sufficient, then you * can use maxSize to specify the maximum number of items that may * be opened before purging commences. A value of 0 (zero) means it * will not be used */ protected int maxSize; protected WeakHashMap<Object, HashSet<MemoryManageable>> locked; protected ArrayList<MemoryManageable> leastRecentlyUsed; protected long cumulativeDurationManagingMemory; protected long cumulativeDurationNotManagingMemory; protected long previousTimestampManagedMemory; protected int percentageDurationManagingMemory; protected ArrayList<MemoryManagerDelegate> delegates; /** * Get an instance of the <code>MemoryManager</code>. If there is not a <code>MemoryManager</code> * initiated, a new <code>MemoryManager</code> is is created and returned. * * @return the current <code>MemoryManager</code> object org.icepdf.core.minMemory */ public static MemoryManager getInstance() { if (instance == null) { instance = new MemoryManager(); } return instance; } /** * Creates a new instance of a <code>MemoryManager</code>. */ protected MemoryManager() { // get min system memory try { int t = parse("org.icepdf.core.minMemory"); if (t > 0) { minMemory = t; } } catch (Throwable e) { logger.log(Level.FINE, "Error setting org.icepdf.core.minMemory"); } purgeSize = Defs.sysPropertyInt("org.icepdf.core.purgeSize", 5); maxSize = Defs.sysPropertyInt("org.icepdf.core.maxSize", 0); locked = new WeakHashMap<Object, HashSet<MemoryManageable>>(); leastRecentlyUsed = new ArrayList<MemoryManageable>(256); delegates = new ArrayList<MemoryManagerDelegate>(64); } public synchronized void lock(Object user, MemoryManageable mm) { if (user == null || mm == null) return; //System.out.println("+-+ MM.lock() user: " + user + ", mm: " + mm); HashSet<MemoryManageable> inUse = locked.get(user); if (inUse == null) { inUse = new HashSet<MemoryManageable>(256); locked.put(user, inUse); } inUse.add(mm); leastRecentlyUsed.remove(mm); leastRecentlyUsed.add(mm); if (maxSize > 0) { int numUsed = leastRecentlyUsed.size(); int numUsedMoreThanShould = numUsed - maxSize; if (numUsedMoreThanShould > 0) { //System.out.println("+-+ MM.lock() numUsedMoreThanShould: " + numUsedMoreThanShould + ", maxSize: " + maxSize + ", numUsed: " + numUsed); int numToDo = Math.max(purgeSize, numUsedMoreThanShould); reduceMemory(numToDo); } } } public synchronized void release(Object user, MemoryManageable mm) { if (user == null || mm == null) return; //System.out.println("+-+ MM.release() user: " + user + ", mm: " + mm); HashSet inUse = locked.get(user); if (inUse != null) { boolean removed = inUse.remove(mm); // remove locked reference if it no longer holds any mm objects. if (inUse.size() == 0) { locked.remove(user); } //if( removed ) System.out.println("+-+ MM.release() mm was removed"); } } public synchronized void registerMemoryManagerDelegate(MemoryManagerDelegate delegate) { if (!delegates.contains(delegate)) delegates.add(delegate); } public synchronized void releaseAllByLibrary(Library library) { //System.out.println("+-+ MM.releaseAllByLibrary() library: " + library); if (library == null) return; // Remove every MemoryManageable whose Library is library // Go in reverse order, so removals won't affect indexing for (int i = leastRecentlyUsed.size() - 1; i >= 0; i--) { MemoryManageable mm = leastRecentlyUsed.get(i); //System.out.println("+-+ MM.releaseAllByLibrary() LRU " + i + " of " + leastRecentlyUsed.size() + " mm: " + mm); Library lib = mm.getLibrary(); if (lib == null) { //System.out.println("*** MM.releaseAllByLibrary() mm.getLibrary() was null"); continue; } //System.out.println("+-+ MM.releaseAllByLibrary() lib: " + lib + ", lib == library (remove mm): " + lib.equals(library)); if (lib.equals(library)) { leastRecentlyUsed.remove(i); } } // Go through every user, and looks at the MemoryManageable(s) // that it's locking ArrayList<Object> usersToRemove = new ArrayList<Object>(); Set entries = locked.entrySet(); for (Object entry1 : entries) { Map.Entry entry = (Map.Entry) entry1; Object user = entry.getKey(); //System.out.println("+-+ MM.releaseAllByLibrary() user: " + user); HashSet inUse = (HashSet) entry.getValue(); if (inUse != null) { // Remove every MemoryManageable whose Library is library ArrayList<MemoryManageable> mmsToRemove = new ArrayList<MemoryManageable>(); for (Object anInUse : inUse) { MemoryManageable mm = (MemoryManageable) anInUse; //System.out.println("+-+ MM.releaseAllByLibrary() mm: " + mm); if (mm != null) { Library lib = mm.getLibrary(); if (lib == null) { //System.out.println("*** MM.releaseAllByLibrary() mm.getLibrary() was null"); continue; } //System.out.println("+-+ MM.releaseAllByLibrary() lib: " + lib + ", lib == library (remove mm): " + lib.equals(library)); if (lib.equals(library)) { mmsToRemove.add(mm); } } } for (MemoryManageable aMmsToRemove : mmsToRemove) { inUse.remove(aMmsToRemove); } mmsToRemove.clear(); // If the user has no more MemoryManageable(s) // locked, then remove it as well. // We don't immediately remove it, since the // iterators are fail-fast. Instead, mark them // for removal, and do it after iterating if (inUse.size() == 0) { //System.out.println("+-+ MM.releaseAllByLibrary() remove user: " + user); usersToRemove.add(user); } } } for (Object anUsersToRemove : usersToRemove) { locked.remove(anUsersToRemove); } usersToRemove.clear(); for (int i = delegates.size() - 1; i >= 0; i--) { MemoryManagerDelegate mmd = delegates.get(i); boolean shouldRemove = false; if (mmd == null) shouldRemove = true; else { Library lib = mmd.getLibrary(); if (lib == null) shouldRemove = true; else { if (lib.equals(library)) shouldRemove = true; } } if (shouldRemove) delegates.remove(i); } } /** * @return If potentially reduced some memory */ protected synchronized boolean reduceMemory() { int numToDo = purgeSize; int aggressive = 0; int lruSize = leastRecentlyUsed.size(); if (percentageDurationManagingMemory > 15 || lruSize > 100) aggressive = lruSize * 60 / 100; else if (lruSize > 50) aggressive = lruSize * 50 / 100; else if (lruSize > 20) aggressive = lruSize * 40 / 100; if (aggressive > numToDo) numToDo = aggressive; int numDone = reduceMemory(numToDo); boolean delegatesReduced = false; if (numDone == 0) delegatesReduced = reduceMemoryWithDelegates(true); else if (numDone < numToDo) delegatesReduced = reduceMemoryWithDelegates(false); return ((numDone > 0) || delegatesReduced); } protected int reduceMemory(int numToDo) { //System.out.println("+-+ MM.reduceMemory() numToDo: " + numToDo + ", LRU size: " + leastRecentlyUsed.size()); int numDone = 0; try { int leastRecentlyUsedIndex = 0; while (numDone < numToDo && leastRecentlyUsedIndex < leastRecentlyUsed.size()) { //System.out.println("+-+ MM.reduceMemory() index: " + leastRecentlyUsedIndex + ", size: " + leastRecentlyUsed.size()); MemoryManageable mm = leastRecentlyUsed.get(leastRecentlyUsedIndex); //System.out.println("+-+ MM.reduceMemory() isLocked: " + isLocked(mm) + ", mm: " + mm); if (!isLocked(mm)) { mm.reduceMemory(); numDone++; leastRecentlyUsed.remove(leastRecentlyUsedIndex); } else leastRecentlyUsedIndex++; } } catch (Exception e) { logger.log(Level.FINE, "Problem while reducing memory", e); } //System.out.println("+-+ MM.reduceMemory() managing: " + cumulativeDurationManagingMemory + ", not: " + cumulativeDurationNotManagingMemory + " managing: " + percentageDurationManagingMemory + "%"); return numDone; } protected synchronized boolean isLocked(MemoryManageable mm) { Set entries = locked.entrySet(); // this can get pretty inefficient if locked is large for (Object entry1 : entries) { Map.Entry entry = (Map.Entry) entry1; HashSet inUse = (HashSet) entry.getValue(); if (inUse != null && inUse.contains(mm)) return true; } return false; } protected synchronized boolean reduceMemoryWithDelegates(boolean aggressively) { int reductionPolicy = aggressively ? MemoryManagerDelegate.REDUCE_AGGRESSIVELY : MemoryManagerDelegate.REDUCE_SOMEWHAT; boolean anyReduced = false; for (MemoryManagerDelegate mmd : delegates) { if (mmd == null) continue; boolean reduced = mmd.reduceMemory(reductionPolicy); anyReduced |= reduced; } return anyReduced; } /** * Utility method to parse memory values specified by the k, K, m or M and * return the corresponding number of bytes. * * @param memoryValue memory value to parse * @return the number of bytes */ private static int parse(String memoryValue) { String s = Defs.sysProperty(memoryValue); if (s == null) { return -1; } int mult = 1; char c = s.charAt(s.length() - 1); if (c == 'k' || c == 'K') { mult = 1024; s = s.substring(0, s.length() - 1); } if (c == 'm' || c == 'M') { mult = 1024 * 1024; s = s.substring(0, s.length() - 1); } return mult * Integer.parseInt(s); } /** * Set the minimum amount of memory. Basically, if the amount * of free heap is under this value, the core will go into * the memory recovery mode. * * @param m minimum amount of memory that should be kept free on the heap */ public void setMinMemory(long m) { minMemory = m; } /** * Get the minimum amount of memory * * @return minimum amount of memory that should be kept free on the heap. */ public long getMinMemory() { return minMemory; } /** * Get runtime free memory. * * @return free memory in bytes. */ public long getFreeMemory() { return runtime.freeMemory(); } /** * Will return true if the system is not in a low memory * condition after allocation of specified number of bytes. */ private boolean canAllocate(int bytes, boolean doGC) { long mem = runtime.freeMemory(); // Enough memory? if ((mem - bytes) > minMemory) { return true; } // Do we allow the heap to grow? long total = runtime.totalMemory(); if (maxMemory > total) { mem += maxMemory - total; if ((mem - bytes) > minMemory) { return true; } } // This is so that checkMemory() can try to clear out // some cached pages _before_ running the garbage collector, // which saves a lot of CPU if (!doGC) return false; // lets purge some pages reduceMemory(); // Nope, try GC System.gc(); mem = runtime.freeMemory(); // Enough memory? if ((mem - bytes) > minMemory) { return true; } // Nope, try heavy GC System.runFinalization(); System.gc(); mem = runtime.freeMemory(); // Enough memory? if ((mem - bytes) > minMemory) { return true; } // We are low on memory! //System.out.println("Failed -not enough Available Memory " + mem); return false; } /** * Check whether the runtime is low on memory. Page initialization and other * large memory allocation operations should call this * method before (during) attempts to allocate resources. * * @return If this method returns true, such a component should stop its * operation and free up the memory it has allocated. */ public boolean isLowMemory() { return !canAllocate(0, true); } /** * @param memoryNeeded memory looking to allocate * @return true if it's sure we can allocate memoryNeeded number of bytes */ public boolean checkMemory(int memoryNeeded) { long beginTime = System.currentTimeMillis(); int count = 0; // try and allocate memory, but quit after ten tries. while (!canAllocate(memoryNeeded, count > 0)) { // cache files on memory check boolean reducedSomething = reduceMemory(); if (!reducedSomething && count > 0) { finishedMemoryProcessing(beginTime); return false; } count++; if (count > 10) { finishedMemoryProcessing(beginTime); return false; } } finishedMemoryProcessing(beginTime); return true; } private void finishedMemoryProcessing(long beginTime) { long endTime = System.currentTimeMillis(); long duration = endTime - beginTime; if (duration > 0) cumulativeDurationManagingMemory += duration; if (previousTimestampManagedMemory != 0) { duration = beginTime - previousTimestampManagedMemory; if (duration > 0) cumulativeDurationNotManagingMemory += duration; } previousTimestampManagedMemory = endTime; long totalDuration = cumulativeDurationManagingMemory + cumulativeDurationNotManagingMemory; if (totalDuration > 0) { percentageDurationManagingMemory = (int) (cumulativeDurationManagingMemory * 100 / totalDuration); } } }