Index: trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 19074)
+++ trunk/src/org/openstreetmap/josm/gui/layer/imagery/ReprojectionTile.java	(revision 19075)
@@ -66,5 +66,5 @@
     /**
      * Get the scale that was used for reprojecting the tile.
-     *
+     * <p>
      * This is not necessarily the mapview scale, but may be
      * adjusted to avoid excessively large cache image.
@@ -78,5 +78,5 @@
      * Check if it is necessary to refresh the cache to match the current mapview
      * scale and get optimized image quality.
-     *
+     * <p>
      * When the maximum zoom is exceeded, this method will generally return false.
      * @param currentScale the current mapview scale
@@ -161,13 +161,13 @@
         ProjectionBounds pbTargetAligned = pbMarginAndAlign(pbTarget, scale, margin);
 
-        ImageWarp.PointTransform pointTransform = pt -> {
-            EastNorth target = new EastNorth(pbTargetAligned.minEast + pt.getX() * scale,
-                    pbTargetAligned.maxNorth - pt.getY() * scale);
+        ImageWarp.PointTransform pointTransform = (x, y) -> {
+            EastNorth target = new EastNorth(pbTargetAligned.minEast + x * scale,
+                    pbTargetAligned.maxNorth - y * scale);
             EastNorth sourceEN = projServer.latlon2eastNorth(projCurrent.eastNorth2latlon(target));
-            double x = source.getTileSize() *
+            double x2 = source.getTileSize() *
                     (sourceEN.east() - pbServer.minEast) / (pbServer.maxEast - pbServer.minEast);
-            double y = source.getTileSize() *
+            double y2 = source.getTileSize() *
                     (pbServer.maxNorth - sourceEN.north()) / (pbServer.maxNorth - pbServer.minNorth);
-            return new Point2D.Double(x, y);
+            return new Point2D.Double(x2, y2);
         };
 
@@ -225,5 +225,5 @@
     /**
      * Make sure, the image is not scaled up too much.
-     *
+     * <p>
      * This would not give any significant improvement in image quality and may
      * exceed the user's memory. The correction factor is a power of 2.
Index: trunk/src/org/openstreetmap/josm/tools/ImageWarp.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 19074)
+++ trunk/src/org/openstreetmap/josm/tools/ImageWarp.java	(revision 19075)
@@ -6,4 +6,5 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -14,5 +15,5 @@
 /**
  * Image warping algorithm.
- *
+ * <p>
  * Deforms an image geometrically according to a given transformation formula.
  * @since 11858
@@ -27,16 +28,18 @@
      * Transformation that translates the pixel coordinates.
      */
+    @FunctionalInterface
     public interface PointTransform {
         /**
          * Translates pixel coordinates.
-         * @param pt pixel coordinates
+         * @param x The x coordinate
+         * @param y The y coordinate
          * @return transformed pixel coordinates
          */
-        Point2D transform(Point2D pt);
+        Point2D transform(double x, double y);
     }
 
     /**
      * Wrapper that optimizes a given {@link ImageWarp.PointTransform}.
-     *
+     * <p>
      * It does so by spanning a grid with certain step size. It will invoke the
      * potentially expensive master transform only at those grid points and use
@@ -75,9 +78,9 @@
 
         @Override
-        public Point2D transform(Point2D pt) {
-            int xIdx = (int) Math.floor(pt.getX() / stride);
-            int yIdx = (int) Math.floor(pt.getY() / stride);
-            double dx = pt.getX() / stride - xIdx;
-            double dy = pt.getY() / stride - yIdx;
+        public Point2D transform(double x, double y) {
+            int xIdx = (int) Math.floor(x / stride);
+            int yIdx = (int) Math.floor(y / stride);
+            double dx = x / stride - xIdx;
+            double dy = y / stride - yIdx;
             Point2D value00 = getValue(xIdx, yIdx);
             Point2D value01 = getValue(xIdx, yIdx + 1);
@@ -92,5 +95,14 @@
 
         private Point2D getValue(int xIdx, int yIdx) {
-            return getRow(yIdx).computeIfAbsent(xIdx, k -> trfm.transform(new Point2D.Double(xIdx * stride, yIdx * stride)));
+            final Map<Integer, Point2D> rowMap = getRow(yIdx);
+            // This *was* computeIfAbsent. Unfortunately, it appears that it generated a ton of memory allocations.
+            // As in, this was ~50 GB memory allocations in a test, and converting to a non-lambda form made it 1.3GB.
+            // The primary culprit was LambdaForm#linkToTargetMethod
+            Point2D current = rowMap.get(xIdx);
+            if (current == null) {
+                current = trfm.transform(xIdx * stride, yIdx * stride);
+                rowMap.put(xIdx, current);
+            }
+            return current;
         }
 
@@ -98,6 +110,7 @@
             cleanUp(yIdx - 3);
             Map<Integer, Point2D> row = cache.get(yIdx);
+            // Note: using computeIfAbsent will drastically increase memory allocations
             if (row == null) {
-                row = new HashMap<>();
+                row = new HashMap<>(256);
                 cache.put(yIdx, row);
                 if (consistencyTest) {
@@ -128,5 +141,5 @@
         /**
          * Nearest neighbor.
-         *
+         * <p>
          * Simplest possible method. Faster, but not very good quality.
          */
@@ -135,5 +148,5 @@
         /**
          * Bilinear.
-         *
+         * <p>
          * Decent quality.
          */
@@ -153,12 +166,17 @@
         BufferedImage imgTarget = new BufferedImage(targetDim.width, targetDim.height, BufferedImage.TYPE_INT_ARGB);
         Rectangle2D srcRect = new Rectangle2D.Double(0, 0, srcImg.getWidth(), srcImg.getHeight());
+        // These arrays reduce the amount of memory allocations (getRGB and setRGB are
+        // collectively 40% of the memory cost, 78% if LambdaForm#linkToTargetMethod is
+        // ignored). We mostly want to decrease GC pauses here.
+        final int[] pixel = new int[1]; // Yes, this really does decrease memory allocations with TYPE_INT_ARGB.
+        final Object sharedArray = getSharedArray(srcImg);
         for (int j = 0; j < imgTarget.getHeight(); j++) {
             for (int i = 0; i < imgTarget.getWidth(); i++) {
-                Point2D srcCoord = invTransform.transform(new Point2D.Double(i, j));
+                Point2D srcCoord = invTransform.transform(i, j);
                 if (srcRect.contains(srcCoord)) {
                     int rgba;
                     switch (interpolation) {
                         case NEAREST_NEIGHBOR:
-                            rgba = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg);
+                            rgba = getColor((int) Math.round(srcCoord.getX()), (int) Math.round(srcCoord.getY()), srcImg, sharedArray);
                             break;
                         case BILINEAR:
@@ -167,8 +185,8 @@
                             int y0 = (int) Math.floor(srcCoord.getY());
                             double dy = srcCoord.getY() - y0;
-                            int c00 = getColor(x0, y0, srcImg);
-                            int c01 = getColor(x0, y0 + 1, srcImg);
-                            int c10 = getColor(x0 + 1, y0, srcImg);
-                            int c11 = getColor(x0 + 1, y0 + 1, srcImg);
+                            int c00 = getColor(x0, y0, srcImg, sharedArray);
+                            int c01 = getColor(x0, y0 + 1, srcImg, sharedArray);
+                            int c10 = getColor(x0 + 1, y0, srcImg, sharedArray);
+                            int c11 = getColor(x0 + 1, y0 + 1, srcImg, sharedArray);
                             rgba = 0;
                             // loop over color components: blue, green, red, alpha
@@ -184,5 +202,5 @@
                             throw new AssertionError(Objects.toString(interpolation));
                     }
-                    imgTarget.setRGB(i, j, rgba);
+                    imgTarget.getRaster().setDataElements(i, j, imgTarget.getColorModel().getDataElements(rgba, pixel));
                 }
             }
@@ -191,9 +209,25 @@
     }
 
-    private static int getColor(int x, int y, BufferedImage img) {
+    private static Object getSharedArray(BufferedImage srcImg) {
+        final int numBands = srcImg.getRaster().getNumBands();
+        // Add data types as needed (shown via profiling, look for getRGB).
+        switch (srcImg.getRaster().getDataBuffer().getDataType()) {
+            case DataBuffer.TYPE_BYTE:
+                return new byte[numBands];
+            case DataBuffer.TYPE_INT:
+                return new int[numBands];
+            default:
+                return null;
+        }
+    }
+
+    private static int getColor(int x, int y, BufferedImage img, Object sharedArray) {
         // border strategy: continue with the color of the outermost pixel,
-        return img.getRGB(
-                Utils.clamp(x, 0, img.getWidth() - 1),
-                Utils.clamp(y, 0, img.getHeight() - 1));
+        final int rx = Utils.clamp(x, 0, img.getWidth() - 1);
+        final int ry = Utils.clamp(y, 0, img.getHeight() - 1);
+        if (sharedArray == null) {
+            return img.getRGB(rx, ry);
+        }
+        return img.getColorModel().getRGB(img.getRaster().getDataElements(rx, ry, sharedArray));
     }
 }
