Env:
JDK: 1.8u112 oracle
JRE: 10.0.2
JVM max heap size: ~2GB.
OS: Windows 10
IDE: Netbeans 8.1
RAM: DDR4 8GB
Processor: 6700hq i7 intel
Context
A simple GUI that opens an image file (jpg/png) and magnifies it via user input.
Desc
A class extends JFrame. The frame's contentPane has a JButton,a JLabel & a JScrollPane. Clicking the button shows a JFileChooser. The label is inside the scrollpane. Selecting a file opens it in the label(open image files only for the purposes of this question-jpg/png tested upon). The label has a mouse wheel listener that causes zooming of image via Image.getScaledInstance
. At each zoom, magnifiaction (ratio of new image width(or height) to corresponding original's) and Runtime.totalMemory
is printed.
Problem
- Upon zooming into the image, too much memory seems to being consumed by the code. The task manager shows 1708 MB memory usage at 11.8 times magnification for a 7.23KB png image. Expected should be around the order of 11.8*11.8*7.23KB
- Upon zooming out, the memory consumption doesn't reduce
- Why is the heap expanding so much(at around ~17 times mag, it reaches 2GB) in the first place? Are discarded
ImageIcon
objects(see code) not being gced? - How to make code viable for mag where mag * mag * originalImageSize(in bytes)<50% JVM max heap size?
Code
import java.awt.Dimension;
import java.awt.Image;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class gui extends javax.swing.JFrame {
Image image;
Dimension size;
private double mag = 1;
Runtime runtime = Runtime.getRuntime();
public gui() {
initComponents();
}
private void zoom() {
int[] newSize = {(int) (size.width * mag), (int) (size.height * mag)};
if (newSize[0] > 0 && newSize[1] > 0) {
label.setIcon(new ImageIcon(image.getScaledInstance(newSize[0], newSize[1], Image.SCALE_DEFAULT)));
}
System.out.println("mag:" + (int) (mag * 100) + "% mem:" + runtime.totalMemory() / 1024 / 1024 + "MB");
}
private void loadImage(File imgFile) throws IOException {
String path = imgFile.getPath().toLowerCase();
if (path.endsWith("gif")) {
ImageIcon icon = new ImageIcon(path);
image = icon.getImage();
label.setIcon(icon);
} else {
image = ImageIO.read(imgFile);
ImageIcon icon = new ImageIcon(image);
label.setIcon(icon);
}
size = new Dimension(image.getWidth(null), image.getHeight(null));
}
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
dialog = new javax.swing.JFileChooser();
jScrollPane1 = new javax.swing.JScrollPane();
label = new javax.swing.JLabel();
button = new javax.swing.JButton();
dialog.setCurrentDirectory(new java.io.File("D:\\"));
dialog.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
dialogActionPerformed(evt);
}
});
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("Image Viewer");
label.setHorizontalAlignment(javax.swing.SwingConstants.LEFT);
label.setVerticalAlignment(javax.swing.SwingConstants.TOP);
label.addMouseWheelListener(new java.awt.event.MouseWheelListener() {
public void mouseWheelMoved(java.awt.event.MouseWheelEvent evt) {
labelMouseWheelMoved(evt);
}
});
jScrollPane1.setViewportView(label);
button.setText("open");
button.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
buttonActionPerformed(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 689, Short.MAX_VALUE)
.addGroup(layout.createSequentialGroup()
.addComponent(button)
.addGap(0, 0, Short.MAX_VALUE)))
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addContainerGap()
.addComponent(button)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 430, Short.MAX_VALUE)
.addContainerGap())
);
pack();
}// </editor-fold>
private void dialogActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
if (evt.getActionCommand().equals(JFileChooser.APPROVE_SELECTION)) {
try {
File file = dialog.getSelectedFile();
loadImage(file);
setTitle(file.getPath());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
private void labelMouseWheelMoved(java.awt.event.MouseWheelEvent evt) {
if (image != null) {
int amt = -evt.getWheelRotation();
double newMag = mag + amt * 0.1;
if (newMag > 0) {
mag = newMag;
zoom();
}
}
}
private void buttonActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
dialog.showOpenDialog(this);
}
public static void main(String args[]) throws Exception {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new gui().setVisible(true);
}
});
}
// Variables declaration - do not modify
private javax.swing.JButton button;
private javax.swing.JFileChooser dialog;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JLabel label;
// End of variables declaration
}
Test file
Any jpg or png image should do besides test file.
Output from the test file
mag:110% mem:123MB mag:120% mem:123MB mag:130% mem:123MB mag:140% mem:123MB mag:150% mem:123MB mag:160% mem:123MB mag:150% mem:123MB mag:160% mem:123MB mag:170% mem:123MB mag:180% mem:155MB mag:190% mem:155MB mag:200% mem:155MB mag:210% mem:155MB mag:220% mem:155MB mag:230% mem:157MB mag:240% mem:157MB mag:250% mem:157MB mag:260% mem:157MB mag:270% mem:253MB mag:280% mem:253MB mag:290% mem:253MB mag:300% mem:253MB mag:310% mem:253MB mag:320% mem:256MB mag:330% mem:256MB mag:340% mem:256MB mag:350% mem:256MB mag:360% mem:256MB mag:370% mem:393MB mag:380% mem:393MB mag:390% mem:393MB mag:400% mem:393MB mag:410% mem:393MB mag:420% mem:393MB mag:430% mem:466MB mag:440% mem:466MB mag:450% mem:466MB mag:460% mem:466MB mag:470% mem:466MB mag:480% mem:466MB mag:489% mem:541MB mag:499% mem:541MB mag:509% mem:541MB mag:519% mem:541MB mag:529% mem:541MB mag:539% mem:641MB mag:549% mem:641MB mag:559% mem:641MB mag:569% mem:641MB mag:579% mem:641MB mag:589% mem:825MB mag:599% mem:825MB mag:609% mem:825MB mag:619% mem:825MB mag:609% mem:825MB mag:619% mem:825MB mag:629% mem:892MB mag:639% mem:892MB mag:649% mem:892MB mag:659% mem:892MB mag:669% mem:892MB mag:679% mem:892MB mag:689% mem:881MB mag:699% mem:881MB mag:709% mem:881MB mag:719% mem:881MB mag:729% mem:1029MB mag:739% mem:1029MB mag:749% mem:1029MB mag:759% mem:1029MB mag:769% mem:1104MB mag:779% mem:1104MB mag:789% mem:1104MB mag:799% mem:1104MB mag:809% mem:1075MB mag:819% mem:1075MB mag:829% mem:1075MB mag:839% mem:1182MB mag:849% mem:1182MB mag:859% mem:1182MB mag:869% mem:1289MB mag:879% mem:1289MB mag:889% mem:1542MB mag:899% mem:1542MB mag:909% mem:1542MB mag:919% mem:1569MB mag:929% mem:1569MB mag:939% mem:1569MB mag:949% mem:1480MB mag:959% mem:1480MB mag:969% mem:1548MB mag:979% mem:1548MB mag:989% mem:1655MB mag:999% mem:1655MB mag:1009% mem:1707MB mag:1019% mem:1707MB mag:1029% mem:1802MB mag:1039% mem:1850MB mag:1049% mem:1850MB mag:1059% mem:1871MB mag:1069% mem:1871MB mag:1079% mem:1801MB mag:1089% mem:1862MB mag:1099% mem:1862MB mag:1109% mem:1815MB mag:1119% mem:1822MB mag:1129% mem:1758MB mag:1139% mem:1774MB mag:1149% mem:1711MB mag:1159% mem:1734MB mag:1169% mem:1676MB mag:1179% mem:1708MB mag:1189% mem:1654MB
How they could be GC'ed without the GC running?
Why should the GC run when the memory suffices?
That's it. The GC runs when needed and there isn't much to win by keeping the memory usage lower than necessary.
For efficiency reasons, the GC is actually a "survivor collector": It only deals with surviving objects and what's left behind is free memory. Therefore it makes sense to run it ALAP as most objects die young.
No, a Java process is free to use all the memory you gave it.
Yes, as there's no need for the GC to run.
The images at all intermediate sizes are unreachable, but not yet collected.
You can't. When the memory will be needed by the Java process, then it gets reclaimed.
I was lying a bit. You can call
System.gc
manually and it'll probably help. But don't do it. While this answers the last question, it solves no real problem. If you want to keep the memory usage low, then give Java less memory using-Xmx1000M
or alike.