diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
index 9ffc903f06eab51891ede050d0e640f2e6dbdd62..c15dbbf6f026810d1d6211db2d71d9b83d774150 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java
@@ -131,6 +131,7 @@ public class PluginUpdateChecker extends UpdateHandler {
             boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
             String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
             int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
+            _baos.reset();
             try {
                 _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _xpi2pURL, TrustedUpdate.HEADER_BYTES);
                 _get.addStatusListener(PluginUpdateCheckerRunner.this);
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index c9cca505c4ee8f231ff963dbe8dbbcbc72c83251..dfbb5c718a557eb86fc2e0d27a2e061428b84bcd 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -1078,6 +1078,7 @@ public class DataHelper {
         ReusableGZIPInputStream in = ReusableGZIPInputStream.acquire();
         in.initialize(new ByteArrayInputStream(orig, offset, length));
         
+        // don't make this a static field, or else I2PAppContext gets initialized too early
         ByteCache cache = ByteCache.getInstance(8, MAX_UNCOMPRESSED);
         ByteArray outBuf = cache.acquire();
         int written = 0;
diff --git a/core/java/src/net/i2p/util/ByteCache.java b/core/java/src/net/i2p/util/ByteCache.java
index 992d4e5e67caf7eb49407e7af51afc4664621a36..38df7a03982007fb01449e92255bcc6feeffe78d 100644
--- a/core/java/src/net/i2p/util/ByteCache.java
+++ b/core/java/src/net/i2p/util/ByteCache.java
@@ -13,27 +13,98 @@ import net.i2p.data.ByteArray;
  * Cache the objects frequently used to reduce memory churn.  The ByteArray 
  * should be held onto as long as the  data referenced in it is needed.
  *
+ * Heap size control - survey of usage (April 2010) :
+ *
+ *  </pre>
+	Size	Max	MaxMem	From
+
+	16	16	256	CryptixAESEngine
+	16	32	512	BloomFilterIVValidator
+	16	64	1K	UDP PacketBuilder
+	16	128	2K	tunnel HopProcessor
+	16	128	2K	tunnel TrivialPreprocessor
+	16	128	2K	tunnel InboundEndpointProcessor
+	16	128	2K	tunnel OutboundGatewayProcessor
+
+	32	64	2K	UDP PacketBuilder
+	32	128	4K	tunnel TrivialPreprocessor
+
+	1K	32	32K	tunnel TrivialPreprocessor
+	1K	512	512K	tunnel FragmentHandler
+	1K	512	512K	I2NP TunnelDataMessage
+	1K	512	512K	tunnel FragmentedMessage
+
+	1730	128	216K	streaming MessageOutputStream
+
+	2K	64	128K	UDP IMS
+
+	4K	32	128K	I2PTunnelRunner
+
+	8K	8	64K	I2PTunnel HTTPResponseOutputStream
+
+	32K	4	128K	SAM StreamSession
+	32K	10	320K	SAM v2StreamSession
+	32K	64	2M	UDP OMS
+	32K	128	4M	streaming MessageInputStream
+
+	36K	64	2.25M	streaming PacketQueue
+
+	40K	8	320K	DataHelper decompress
+
+	64K	64	4M	UDP MessageReceiver - disabled in 0.7.14
+ *  </pre>
+ *
  */
 public final class ByteCache {
-    private final static Map _caches = new HashMap(16);
+
+    private static final Map<Integer, ByteCache> _caches = new HashMap(16);
+
+    /**
+     *  max size in bytes of each cache
+     *  Set to max memory / 128, with a min of 128KB and a max of 4MB
+     *
+     *  @since 0.7.14
+     */
+    private static final int MAX_CACHE;
+    static {
+        long maxMemory = Runtime.getRuntime().maxMemory();
+        MAX_CACHE = (int) Math.min(4*1024*1024l, Math.max(128*1024l, maxMemory / 128));
+    }
+
     /**
      * Get a cache responsible for objects of the given size
      *
      * @param cacheSize how large we want the cache to grow before using on 
      *                  demand allocation
+     *                  Since 0.7.14, a limit of 1MB / size is enforced
+     *                  for the typical 128MB max memory JVM
      * @param size how large should the objects cached be?
      */
     public static ByteCache getInstance(int cacheSize, int size) {
+        if (cacheSize * size > MAX_CACHE)
+            cacheSize = MAX_CACHE / size;
         Integer sz = Integer.valueOf(size);
         ByteCache cache = null;
         synchronized (_caches) {
             if (!_caches.containsKey(sz))
                 _caches.put(sz, new ByteCache(cacheSize, size));
-            cache = (ByteCache)_caches.get(sz);
+            cache = _caches.get(sz);
         }
         cache.resize(cacheSize);
+        //I2PAppContext.getGlobalContext().logManager().getLog(ByteCache.class).error("ByteCache size: " + size + " max: " + cacheSize, new Exception("from"));
         return cache;
     }
+
+    /**
+     *  Clear everything (memory pressure)
+     *  @since 0.7.14
+     */
+    public static void clearAll() {
+        for (ByteCache bc : _caches.values())
+            bc.clear();
+        I2PAppContext.getGlobalContext().logManager().getLog(ByteCache.class).error("WARNING: Low memory, clearing byte caches");
+    }
+
     private Log _log;
     /** list of available and available entries */
     private Queue<ByteArray> _available;
@@ -57,13 +128,14 @@ public final class ByteCache {
         _lastOverflow = -1;
         SimpleScheduler.getInstance().addPeriodicEvent(new Cleanup(), CLEANUP_FREQUENCY);
         _log = I2PAppContext.getGlobalContext().logManager().getLog(ByteCache.class);
+        I2PAppContext.getGlobalContext().statManager().createRateStat("byteCache.memory." + entrySize, "Memory usage (B)", "Router", new long[] { 60*1000 });
     }
     
     private void resize(int maxCachedEntries) {
         if (_maxCached >= maxCachedEntries) return;
         _maxCached = maxCachedEntries;
         // make a bigger one, move the cached items over
-        Queue newLBQ = new LinkedBlockingQueue(maxCachedEntries);
+        Queue<ByteArray> newLBQ = new LinkedBlockingQueue(maxCachedEntries);
         ByteArray ba;
         while ((ba = _available.poll()) != null)
             newLBQ.offer(ba);
@@ -109,8 +181,17 @@ public final class ByteCache {
         }
     }
     
+    /**
+     *  Clear everything (memory pressure)
+     *  @since 0.7.14
+     */
+    private void clear() {
+        _available.clear();
+    }
+
     private class Cleanup implements SimpleTimer.TimedEvent {
         public void timeReached() {
+            I2PAppContext.getGlobalContext().statManager().addRateData("byteCache.memory." + _entrySize, _entrySize * _available.size(), 0);
             if (System.currentTimeMillis() - _lastOverflow > EXPIRE_PERIOD) {
                 // we haven't exceeded the cache size in a few minutes, so lets
                 // shrink the cache 
diff --git a/core/java/src/net/i2p/util/ResettableGZIPInputStream.java b/core/java/src/net/i2p/util/ResettableGZIPInputStream.java
index 11432bd3e0779b37ce47ff228c40c333ffa3d1ea..19c5a903ffbbb1b5cd23bbbff43534577f7fcc8a 100644
--- a/core/java/src/net/i2p/util/ResettableGZIPInputStream.java
+++ b/core/java/src/net/i2p/util/ResettableGZIPInputStream.java
@@ -211,6 +211,7 @@ public class ResettableGZIPInputStream extends InflaterInputStream {
         }
     }
     
+/******
     public static void main(String args[]) {
         for (int i = 129; i < 64*1024; i++) {
             if (!test(i)) return;
@@ -279,4 +280,5 @@ public class ResettableGZIPInputStream extends InflaterInputStream {
             return false;
         }
     }
+******/
 }
diff --git a/core/java/src/net/i2p/util/ResettableGZIPOutputStream.java b/core/java/src/net/i2p/util/ResettableGZIPOutputStream.java
index a9aa9cc3ce22b66c3a74d913534bac1a95020b81..06d1506d4a4f34f5f279473d170c35089eab9fa6 100644
--- a/core/java/src/net/i2p/util/ResettableGZIPOutputStream.java
+++ b/core/java/src/net/i2p/util/ResettableGZIPOutputStream.java
@@ -122,6 +122,7 @@ public class ResettableGZIPOutputStream extends DeflaterOutputStream {
         super.write(buf, off, len);
     }
     
+/******
     public static void main(String args[]) {
         for (int i = 0; i < 2; i++)
             test();
@@ -165,12 +166,13 @@ public class ResettableGZIPOutputStream extends DeflaterOutputStream {
         } catch (Exception e) { e.printStackTrace(); }
     }
     
-    /** just for testing/verification, expose the CRC32 values */
+    // just for testing/verification, expose the CRC32 values
     private static final class SnoopGZIPOutputStream extends GZIPOutputStream {
         public SnoopGZIPOutputStream(OutputStream o) throws IOException {
             super(o);
         }
         public CRC32 getCRC() { return crc; }
     }
+******/
 }
 
diff --git a/core/java/src/net/i2p/util/ReusableGZIPInputStream.java b/core/java/src/net/i2p/util/ReusableGZIPInputStream.java
index 229814de9edaab69da18b04f6adc5e623fed2a91..4db3b4c2016a8b5a8d5c7ca13ab8a7c17c221efc 100644
--- a/core/java/src/net/i2p/util/ReusableGZIPInputStream.java
+++ b/core/java/src/net/i2p/util/ReusableGZIPInputStream.java
@@ -48,6 +48,7 @@ public class ReusableGZIPInputStream extends ResettableGZIPInputStream {
     
     private ReusableGZIPInputStream() { super(); }
     
+/*******
     public static void main(String args[]) {
         for (int i = 0; i < 2; i++)
             test();
@@ -127,5 +128,6 @@ public class ReusableGZIPInputStream extends ResettableGZIPInputStream {
             return false;
         }
     }
+******/
 }
 
diff --git a/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java b/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java
index 97680ecafbf6e0abab5fc5695796a16c8d0f40ef..bbc1c334a41a60c56818a48e35f2302cf9438631 100644
--- a/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java
+++ b/core/java/src/net/i2p/util/ReusableGZIPOutputStream.java
@@ -65,6 +65,7 @@ public class ReusableGZIPOutputStream extends ResettableGZIPOutputStream {
     /** pull the contents of the stream written */
     public byte[] getData() { return _buffer.toByteArray(); }
     
+/******
     public static void main(String args[]) {
         try {
             for (int i = 0; i < 2; i++)
@@ -129,5 +130,6 @@ public class ReusableGZIPOutputStream extends ResettableGZIPOutputStream {
             return false;
         }
     }
+*****/
 }
 
diff --git a/history.txt b/history.txt
index 082e117aa7dda4321b225cffb947185491b48175..8f29bb18c2f2a9e95cdeb8743b22094e82e938d8 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,12 @@
+2010-05-02 zzz
+    * ByteCache:
+      - Add a per-cache stat
+      - Limit each cache based on max memory
+      - Disable in UDP MessageReceiver
+      - Add clearAll() method to be called when under
+        severe memory pressure; call from Router
+    * Plugins: Fix version checker bug
+
 2010-04-27 zzz
     * i2psnark: Serve downloaded files from the servlet rather
       than with a file: link
diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java
index b730d6d4a83967ff57ede5feb8f52216bf9b9b68..ee54f6974c9d6d67b294190dc405dd173c605062 100644
--- a/router/java/src/net/i2p/router/Router.java
+++ b/router/java/src/net/i2p/router/Router.java
@@ -41,6 +41,7 @@ import net.i2p.router.transport.udp.UDPTransport;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
 import net.i2p.stat.StatManager;
+import net.i2p.util.ByteCache;
 import net.i2p.util.FileUtil;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.I2PThread;
@@ -224,6 +225,7 @@ public class Router {
         _killVMOnEnd = true;
         _oomListener = new I2PThread.OOMEventListener() { 
             public void outOfMemory(OutOfMemoryError oom) { 
+                ByteCache.clearAll();
                 _log.log(Log.CRIT, "Thread ran out of memory", oom);
                 for (int i = 0; i < 5; i++) { // try this 5 times, in case it OOMs
                     try { 
@@ -252,6 +254,8 @@ public class Router {
      *
      */
     public void setKillVMOnEnd(boolean shouldDie) { _killVMOnEnd = shouldDie; }
+
+    /** @deprecated unused */
     public boolean getKillVMOnEnd() { return _killVMOnEnd; }
     
     public String getConfigFilename() { return _configFilename; }
@@ -923,7 +927,7 @@ public class Router {
     private static final boolean ALLOW_DYNAMIC_KEYS = false;
 
     private void finalShutdown(int exitCode) {
-        _log.log(Log.CRIT, "Shutdown(" + exitCode + ") complete", new Exception("Shutdown"));
+        _log.log(Log.CRIT, "Shutdown(" + exitCode + ") complete"  /* , new Exception("Shutdown") */ );
         try { _context.logManager().shutdown(); } catch (Throwable t) { }
         if (ALLOW_DYNAMIC_KEYS) {
             if (Boolean.valueOf(_context.getProperty(PROP_DYNAMIC_KEYS)).booleanValue())
@@ -1357,12 +1361,16 @@ public class Router {
     
 /* following classes are now private static inner classes, didn't bother to reindent */
 
+private static final long LOW_MEMORY_THRESHOLD = 5 * 1024 * 1024;
+
 /**
  * coalesce the stats framework every minute
  *
  */
 private static class CoalesceStatsEvent implements SimpleTimer.TimedEvent {
     private RouterContext _ctx;
+    private long _maxMemory;
+
     public CoalesceStatsEvent(RouterContext ctx) { 
         _ctx = ctx; 
         ctx.statManager().createRateStat("bw.receiveBps", "How fast we receive data (in KBps)", "Bandwidth", new long[] { 60*1000, 5*60*1000, 60*60*1000 });
@@ -1373,8 +1381,8 @@ private static class CoalesceStatsEvent implements SimpleTimer.TimedEvent {
         ctx.statManager().createRateStat("router.activeSendPeers", "How many peers we've sent to this minute", "Throttle", new long[] { 60*1000, 5*60*1000, 60*60*1000 });
         ctx.statManager().createRateStat("router.highCapacityPeers", "How many high capacity peers we know", "Throttle", new long[] { 5*60*1000, 60*60*1000 });
         ctx.statManager().createRateStat("router.fastPeers", "How many fast peers we know", "Throttle", new long[] { 5*60*1000, 60*60*1000 });
-        long max = Runtime.getRuntime().maxMemory() / (1024*1024);
-        ctx.statManager().createRateStat("router.memoryUsed", "(Bytes) Max is " + max + "MB", "Router", new long[] { 60*1000 });
+        _maxMemory = Runtime.getRuntime().maxMemory();
+        ctx.statManager().createRateStat("router.memoryUsed", "(Bytes) Max is " + (_maxMemory / (1024*1024)) + "MB", "Router", new long[] { 60*1000 });
     }
     private RouterContext getContext() { return _ctx; }
     public void timeReached() {
@@ -1395,6 +1403,8 @@ private static class CoalesceStatsEvent implements SimpleTimer.TimedEvent {
         
         long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
         getContext().statManager().addRateData("router.memoryUsed", used, 0);
+        if (_maxMemory - used < LOW_MEMORY_THRESHOLD)
+            ByteCache.clearAll();
 
         getContext().tunnelDispatcher().updateParticipatingStats(COALESCE_TIME);
 
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 87e5bffca0684477c0ba8daee48da05fbea99f44..282c18b42238909e2faa8bba95d82c15667d116c 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 1;
+    public final static long BUILD = 2;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
index 39b4163e794e93f543d907e0b3d0fda5f5d5c1ef..1a95c227fe6d42a29cfc3bf61a6f04e150692980 100644
--- a/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
+++ b/router/java/src/net/i2p/router/transport/udp/MessageReceiver.java
@@ -10,7 +10,7 @@ import net.i2p.data.i2np.I2NPMessageException;
 import net.i2p.data.i2np.I2NPMessageHandler;
 import net.i2p.data.i2np.I2NPMessageImpl;
 import net.i2p.router.RouterContext;
-import net.i2p.util.ByteCache;
+//import net.i2p.util.ByteCache;
 import net.i2p.util.I2PThread;
 import net.i2p.util.Log;
 
@@ -26,7 +26,7 @@ public class MessageReceiver {
     /** list of messages (InboundMessageState) fully received but not interpreted yet */
     private final BlockingQueue<InboundMessageState> _completeMessages;
     private boolean _alive;
-    private ByteCache _cache;
+    //private ByteCache _cache;
     private static final int THREADS = 5;
     private static final long POISON_IMS = -99999999999l;
     
@@ -35,7 +35,8 @@ public class MessageReceiver {
         _log = ctx.logManager().getLog(MessageReceiver.class);
         _transport = transport;
         _completeMessages = new LinkedBlockingQueue();
-        _cache = ByteCache.getInstance(64, I2NPMessage.MAX_SIZE);
+        // the runners run forever, no need to have a cache
+        //_cache = ByteCache.getInstance(64, I2NPMessage.MAX_SIZE);
         _context.statManager().createRateStat("udp.inboundExpired", "How many messages were expired before reception?", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.inboundRemaining", "How many messages were remaining when a message is pulled off the complete queue?", "udp", UDPTransport.RATES);
         _context.statManager().createRateStat("udp.inboundReady", "How many messages were ready when a message is added to the complete queue?", "udp", UDPTransport.RATES);
@@ -91,7 +92,8 @@ public class MessageReceiver {
     
     public void loop(I2NPMessageHandler handler) {
         InboundMessageState message = null;
-        ByteArray buf = _cache.acquire();
+        //ByteArray buf = _cache.acquire();
+        ByteArray buf = new ByteArray(new byte[I2NPMessage.MAX_SIZE]);
         while (_alive) {
             int expired = 0;
             long expiredLifetime = 0;
@@ -142,7 +144,7 @@ public class MessageReceiver {
         }
         
         // no need to zero it out, as these buffers are only used with an explicit getCompleteSize
-        _cache.release(buf, false); 
+        //_cache.release(buf, false); 
     }
     
     private I2NPMessage readMessage(ByteArray buf, InboundMessageState state, I2NPMessageHandler handler) {