Skip to content

Instantly share code, notes, and snippets.

@stanio
Last active June 21, 2021 06:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stanio/065c7051a835a2d76cd805f60685162b to your computer and use it in GitHub Desktop.
Save stanio/065c7051a835a2d76cd805f60685162b to your computer and use it in GitHub Desktop.
MultiResolutionToolkitImage.ObserverCache memory leak
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.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.ArrayList;
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.LookAndFeel;
import javax.swing.SwingUtilities;
/**
* <em>Not an actual leak!</em> Soft references tend to stick around for much
* longer than weak references, for example. Try using VM arguments like:</p>
* <pre>
* -XX:SoftRefLRUPolicyMSPerMB=1 -Xmx64M</pre>
* <p>
* to cause soft references to be cleared faster.</p>
*
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-8257500">JDK-8257500 : Drawing MultiResolutionImage with ImageObserver leaks memory</a>
* @see <a href="https://bugs.openjdk.java.net/browse/JDK-8022449">JDK-8022449 : Can we get rid of sun.misc.SoftCache?</a>
*/
@SuppressWarnings("serial")
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(),
workaroundDisabled.isSelected());
tabs.addTab(content.getName(), null, content, "Ctrl+W to Close");
liveObjects.addElement(new DebugReference(content, queue));
if ((evt.getModifiers() & ActionEvent.SHIFT_MASK) == 0) {
tabs.setSelectedIndex(tabs.getTabCount() - 1);
}
}
};
AbstractAction closeTab = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent evt) {
if ((evt.getModifiers() & ActionEvent.SHIFT_MASK) == 0) {
int index = tabs.getSelectedIndex();
if (index >= 0) tabs.removeTabAt(index);
} else {
tabs.removeAll();
}
}
};
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.setName("Tools");
toolBar.setFloatable(false);
toolBar.setRollover(true);
toolBar.add(openTab);
toolBar.add(new AbstractAction("GC") {
{
super.putValue(SHORT_DESCRIPTION, "Run the Garbage Collector (Ctrl+Click for Aggressive)");
}
@Override public void actionPerformed(ActionEvent evt) {
if ((evt.getModifiers() & ActionEvent.CTRL_MASK) != 0) {
try {
// java.util.ArrayList.MAX_ARRAY_SIZE
// jdk.internal.util.ArraysSupport.MAX_ARRAY_LENGTH
final int maxArraySize = Integer.MAX_VALUE - 8;
final ArrayList<Object> temp = new ArrayList<>();
long freeBytes;
while ((freeBytes = Runtime.getRuntime().freeMemory()) > 0) {
int size = (int) Math.min(freeBytes, maxArraySize);
temp.add(new byte[size]);
}
} catch (OutOfMemoryError e) {
// -XX:SoftRefLRUPolicyMSPerMB=... Soft references are
// more likely to be cleared sooner at this point.
}
}
System.gc();
}
});
toolBar.addSeparator();
toolBar.add(new JLabel("Icon workaround:")).setEnabled(false);
workaroundDefault = (JCheckBox) toolBar.add(new JCheckBox("\"Default\"", true));
workaroundDefault.setOpaque(false);
workaroundDisabled = (JCheckBox) toolBar.add(new JCheckBox("\"Disabled\"", true));
workaroundDisabled.setOpaque(false);
return toolBar;
}
private Component initMainPanel() {
tabs = new JTabbedPane();
tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
liveObjects = new DefaultListModel<>();
JList<?> objectList = new JList<Reference<Object>>(liveObjects) {
@Override public boolean getScrollableTracksViewportWidth() { return true; }
};
tabs.getActionMap().put("OpenTab", openTab);
tabs.getActionMap().put("CloseTab", closeTab);
LookAndFeel.loadKeyBindings(tabs.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW),
new Object[] { "control T", "OpenTab",
"control shift T", "OpenTab",
"control W", "CloseTab",
"control shift W", "CloseTab" });
JScrollPane listScroll = new JScrollPane(objectList);
Box listPane = Box.createVerticalBox();
JLabel listLabel = (JLabel) listPane.add(new JLabel("Live objects:"));
listLabel.setLabelFor(listScroll);
listScroll.setAlignmentX(LEFT_ALIGNMENT);
listPane.add(listScroll);
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabs, listPane);
split.setResizeWeight(0.4);
SwingUtilities.invokeLater(() -> split.setDividerLocation(0.4));
return split;
}
void startPolling() {
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
Runnable pollQueue = () -> {
try {
Reference<?> ref;
while ((ref = queue.poll()) != null) {
final Reference<?> refCapture = ref;
SwingUtilities.invokeAndWait(() -> {
int index = liveObjects.indexOf(refCapture);
assert (index >= 0);
liveObjects.removeElementAt(index);
});
}
} catch (InvocationTargetException e) {
System.err.println(e);
} catch (InterruptedException e) {
System.err.println(e);
Thread.currentThread().interrupt();
} catch (OutOfMemoryError e) {
// Try again next time
}
};
service.scheduleAtFixedRate(pollQueue, 5, 1, TimeUnit.SECONDS);
}
public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(() -> {
// swing.defaultlaf, swing.metalTheme
if (!System.getProperties().containsKey("swing.metalTheme")) {
// The default Ocean doesn't have MultiResolutionImage
// support with LookAndFeel.getDisabledIcon()
System.setProperty("swing.metalTheme", "steel");
}
MrImageObserverLeakTest frame = new MrImageObserverLeakTest();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocation(50, 50);
frame.setVisible(true);
frame.startPolling();
});
}
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;
super.add(Box.createHorizontalGlue());
ImageIcon defaultIcon = TestIcon.createMrIcon();
if (workaroundDefault) {
TestIcon.avoidMrLeak(defaultIcon);
}
super.add(new LeakedComponent(defaultIcon));
super.add(Box.createHorizontalStrut(5));
JLabel disabled = (JLabel) super.add(new LeakedComponent(defaultIcon));
Icon disabledIcon = disabled.getDisabledIcon();
if (workaroundDisabled) {
TestIcon.avoidMrLeak(disabledIcon);
}
disabled.setEnabled(false);
super.add(Box.createHorizontalGlue());
}
@Override
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);
}
@Override
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();
@Override
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);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
final int padding = 2;
g.setColor(Color.blue);
g.fillOval(padding, padding, userWidth - 2 * padding, userHeight - 2 * padding);
g.setColor(Color.white);
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);
g.dispose();
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) {
super(icon);
}
}
}
@stanio
Copy link
Author

stanio commented Jun 21, 2021

            // Using Java-internal class just for test/demo purpose.
            Image mrImage = new sun.awt.image.MultiResolutionCachedImage(userWidth, userHeight,

Use --add-opens java.desktop/sun.awt.image=ALL-UNNAMED JVM option to prevent:

Exception in thread "AWT-EventQueue-0" java.lang.IllegalAccessError: class net.example.swing.MrImageObserverLeakTest$TestIcon (in unnamed module @0x130fefce) cannot access class sun.awt.image.MultiResolutionCachedImage (in module java.desktop) because module java.desktop does not export sun.awt.image to unnamed module @0x130fefce
	at net.example.swing.MrImageObserverLeakTest$TestIcon.createMrIcon(MrImageObserverLeakTest.java:287)
	at net.example.swing.MrImageObserverLeakTest$TabContent.<init>(MrImageObserverLeakTest.java:232)
	at net.example.swing.MrImageObserverLeakTest$1.actionPerformed(MrImageObserverLeakTest.java:70)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment