Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8257500

Drawing MultiResolutionImage with ImageObserver "leaks" memory



    • Subcomponent:
    • Resolved In Build:
    • CPU:
    • OS:


      java version "15.0.1" 2020-10-20
      Java(TM) SE Runtime Environment (build 15.0.1+9-18)
      Java HotSpot(TM) 64-Bit Server VM (build 15.0.1+9-18, mixed mode, sharing)

      It appears sun.awt.image.MultiResolutionToolkitImage.getResolutionVariantObserver() leaks memory into sun.awt.image.MultiResolutionToolkitImage.ObserverCache.INSTANCE which is a sun.awt.SoftCache. The keys in the cache are hard references to ImageObservers and the values are soft references to lambdas which however capture a reference to the key (thus preventing the soft values to be ever cleared). MultiResolutionToolkitImage.getResolutionVariantObserver() appears called by the Java 2D rendering pipeline:

          Graphics2D g = ...;
          ImageObserver observer = ...;
          MultiResolutionImage mrImage = ...;
          g.drawImage(mrImage, x, y, observer);

      Any component which happens to paint an ImageIcon (JLabel, JButton, etc.) using a multi-resolution image gets leaked into the observer cache. Moreover the components usually have hard references to their parents, thus whole panel structures including top-level windows/dialogs get leaked.

      An easy fix would be for MultiResolutionToolkitImage.getResolutionVariantObserver() to produce a lambda which captures a WeakReference to the original observer, instead. The lambda also needs to capture a WeakReference to the original image, also. The image may be cached separately and unless it gets garbage collected it will prevent the observer from being released.

      Draw a MultiResolutionImage with a (non-null) ImageObsever.

      EXPECTED -
      Once the application holds no hard references to the used ImageObsever it should be garbage-collected, eventually.
      ACTUAL -
      The ImageObserver used during a paint operation with a MultiResolutionImage remains in memory and cannot be garbage collected.

      ---------- BEGIN SOURCE ----------
      package net.example.swing;

      import java.awt.BasicStroke;
      import java.awt.BorderLayout;
      import java.awt.Color;
      import java.awt.Component;
      import java.awt.Container;
      import java.awt.Font;
      import java.awt.FontMetrics;
      import java.awt.Graphics2D;
      import java.awt.Image;
      import java.awt.LayoutManager;
      import java.awt.RenderingHints;
      import java.awt.event.ActionEvent;
      import java.awt.event.KeyEvent;
      import java.awt.image.BufferedImage;
      import java.awt.image.ImageObserver;
      import java.lang.ref.Reference;
      import java.lang.ref.ReferenceQueue;
      import java.lang.ref.WeakReference;
      import java.lang.reflect.InvocationTargetException;
      import java.util.concurrent.Executors;
      import java.util.concurrent.ScheduledExecutorService;
      import java.util.concurrent.TimeUnit;

      import javax.swing.AbstractAction;
      import javax.swing.Box;
      import javax.swing.BoxLayout;
      import javax.swing.DefaultListModel;
      import javax.swing.Icon;
      import javax.swing.ImageIcon;
      import javax.swing.JCheckBox;
      import javax.swing.JComponent;
      import javax.swing.JFrame;
      import javax.swing.JLabel;
      import javax.swing.JList;
      import javax.swing.JPanel;
      import javax.swing.JScrollPane;
      import javax.swing.JSplitPane;
      import javax.swing.JTabbedPane;
      import javax.swing.JToolBar;
      import javax.swing.KeyStroke;
      import javax.swing.SwingUtilities;
      import javax.swing.UIManager;
      import javax.swing.UnsupportedLookAndFeelException;
      import javax.swing.plaf.metal.DefaultMetalTheme;
      import javax.swing.plaf.metal.MetalLookAndFeel;

      public class MrImageObserverLeakTest extends JFrame {

          DefaultListModel<Reference<Object>> liveObjects;
          ReferenceQueue<Object> queue = new ReferenceQueue<>();
          JTabbedPane tabs;
          JCheckBox workaroundDefault;
          JCheckBox workaroundDisabled;

          AbstractAction openTab = new AbstractAction("+Tab") {
                  super.putValue(SHORT_DESCRIPTION, "Open a New Tab (Ctrl+T)");
              @Override public void actionPerformed(ActionEvent evt) {
                  TabContent content = new TabContent(workaroundDefault.isSelected(),
                  tabs.addTab(content.getName(), null, content, "Ctrl+W to Close");
                  liveObjects.addElement(new DebugReference(content, queue));

          AbstractAction closeTab = new AbstractAction() {
              public void actionPerformed(ActionEvent evt) {

          public MrImageObserverLeakTest() {
              super("Multi-Resolution ImageObserver Leak Test");

              Container contentPane = super.getContentPane();
              contentPane.add(initToolBar(), BorderLayout.PAGE_START);
              contentPane.add(initMainPanel(), BorderLayout.CENTER);

          private Component initToolBar() {
              JToolBar toolBar = new JToolBar();

              toolBar.add(new AbstractAction("GC") {
                      super.putValue(SHORT_DESCRIPTION, "Run the Garbage Collector");
                  @Override public void actionPerformed(ActionEvent evt) {


              toolBar.add(new JLabel("Icon workaround:")).setEnabled(false);
              workaroundDefault = (JCheckBox) toolBar.add(new JCheckBox("\"Default\"", true));
              workaroundDisabled = (JCheckBox) toolBar.add(new JCheckBox("\"Disabled\"", true));

              return toolBar;

          private Component initMainPanel() {
              tabs = new JTabbedPane();
              liveObjects = new DefaultListModel<>();
              JList<?> objectList = new JList<Reference<Object>>(liveObjects) {
                  @Override public boolean getScrollableTracksViewportWidth() { return true; }

              tabs.getActionMap().put("OpenNewTab", openTab);
              tabs.getActionMap().put("CloseCurrentTab", closeTab);
              tabs.getActionMap().put("CloseAllTabs", new AbstractAction() {
                  @Override public void actionPerformed(ActionEvent e) { tabs.removeAll(); }
              KeyStroke ctrlT = KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK);
              KeyStroke ctrlW = KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_DOWN_MASK);
              KeyStroke ctrlShiftW = KeyStroke.getKeyStroke(KeyEvent.VK_W,
                      KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK);
              tabs.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlT, "OpenNewTab");
              tabs.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlW, "CloseCurrentTab");
              tabs.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlShiftW, "CloseAllTabs");

              JScrollPane listScroll = new JScrollPane(objectList);
              Box listPane = Box.createVerticalBox();
              JLabel listLabel = (JLabel) listPane.add(new JLabel("Live objects:"));

              JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabs, listPane);
              SwingUtilities.invokeLater(() -> split.setDividerLocation(0.4));
              return split;

          void startPolling() {
              ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
              Runnable pollQueue = () -> {
                  Reference<?> ref;
                  while ((ref = queue.poll()) != null) {
                      try {
                          final Reference<?> refCapture = ref;
                          SwingUtilities.invokeAndWait(() -> {
                              int index = liveObjects.indexOf(refCapture);
                              assert (index >= 0);
                      } catch (InvocationTargetException e) {
                      } catch (InterruptedException e) {
              service.scheduleAtFixedRate(pollQueue, 5, 1, TimeUnit.SECONDS);

          public static void main(String[] args) throws Exception {
              SwingUtilities.invokeLater(() -> {

                  MrImageObserverLeakTest frame = new MrImageObserverLeakTest();
                  frame.setLocation(50, 50);

          static void setLookAndFeel() {
              MetalLookAndFeel.setCurrentTheme(new DefaultMetalTheme());
              try {
                  UIManager.setLookAndFeel(new MetalLookAndFeel());
              } catch (UnsupportedLookAndFeelException e) {

          static class TabContent extends JPanel {

              private static int count = 0;

              private final boolean workaroundDefault;
              private final boolean workaroundDisabled;

              TabContent(boolean workaroundDefault, boolean workaroundDisabled) {
                  super((LayoutManager) null);
                  super.setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
                  super.setName("#" + (count += 1));
                  this.workaroundDefault = workaroundDefault;
                  this.workaroundDisabled = workaroundDisabled;


                  ImageIcon defaultIcon = TestIcon.createMrIcon();
                  if (workaroundDefault) {
                  super.add(new LeakedComponent(defaultIcon));

                  JLabel disabled = (JLabel) super.add(new LeakedComponent(defaultIcon));
                  Icon disabledIcon = disabled.getDisabledIcon();
                  if (workaroundDisabled) {

              protected String paramString() {
                  return getName() + ", workaround: default=" + workaroundDefault + ", disabled=" + workaroundDisabled;

          static class DebugReference extends WeakReference<Object> {

              DebugReference(Object referent, ReferenceQueue<? super Object> q) {
                  super(referent, q);

              public String toString() {
                  Object t = get();
                  if (t == null) {
                      return "null";
                  return t.toString();

          static class DummyObserver implements ImageObserver {
              static final ImageObserver INSTANCE = new DummyObserver();
              public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
                  return false;

          static class TestIcon {

              static ImageIcon createMrIcon() {
                  final int userWidth = 48;
                  final int userHeight = 48;
                  // Using Java-internal class just for test/demo purpose.
                  Image mrImage = new sun.awt.image.MultiResolutionCachedImage(userWidth, userHeight,
                          (deviceWidth, deviceHeight) -> {
                      BufferedImage variant = new BufferedImage(deviceWidth, deviceHeight, BufferedImage.TYPE_INT_ARGB);
                      Graphics2D g = variant.createGraphics();
                      g.scale((double) deviceWidth / userWidth, (double) deviceHeight / userHeight);

                      final int padding = 2;
                      g.fillOval(padding, padding, userWidth - 2 * padding, userHeight - 2 * padding);
                      g.setStroke(new BasicStroke(2));
                      g.drawOval(padding, padding, userWidth - 2 * padding, userHeight - 2 * padding);

                      g.setFont(new Font(Font.SERIF, Font.BOLD, 40));
                      FontMetrics fm = g.getFontMetrics();
                      int stringWidth = fm.stringWidth("i");
                      int stringHeight = 27; // magic
                      g.drawString("i", (userWidth - (float) stringWidth) / 2,
                                        (userHeight + (float) stringHeight) / 2);

                      return variant;
                  return new ImageIcon(mrImage);

              static <T extends Icon> T avoidMrLeak(T icon) {
                  if (icon instanceof ImageIcon) {
                      ((ImageIcon) icon).setImageObserver(DummyObserver.INSTANCE);
                  return icon;


          static class LeakedComponent extends JLabel {
              LeakedComponent(Icon icon) {


      ---------- END SOURCE ----------

      Non-perfect, as it disables handling of animated images.

      In ImageIcon subclasses under user control, one could override ImageIcon.paintIcon() to always paint with a null observer.

      For vanilla ImageIcon instances one could explicitly setImageObserver() with a shared dummy observer that would be the only instance leaked. This is not very feasible to implement as icons may "pop up" from everwhere. One should also take care to perform the same post-processing on disabled ImageIcons produced by LookAndFeel.getDisabledIcon(), like:

          JButton button = new JButton(icon);
          Icon icon = button.getDisabledIcon();
          if (icon instanceof ImageIcon) {
              ((ImageIcon) icon).setImageObserver(DUMMY_OBSERVER);

      the later has the further disadvantage of forcing a disabled icon construction, which might not be really necessary. Note, Metal / Ocean (vs. Metal / Steel) doesn't support producing multi-resolution aware icons from LookAndFeel.getDisabledIcon() so these icons doesn't cause leak.

      FREQUENCY : always


          Issue Links



              serb Sergey Bylokhov
              webbuggrp Webbug Group
              0 Vote for this issue
              5 Start watching this issue