一、问题的提出
曾经使用JAVA开发大型系统的网友恐怕都有过这种体会:系统运行一段时间后就抛出out of memory 异常,然后是艰难的内存泄漏问题排查。在一个大型的JAVA系统中,内存泄露问题是最难查找和解决的问题之一。因为从异常信息无从得知到底是系统中那一部份出现了内存泄漏,只有通过对代码进行走查,使用jprofiler等调试工具察看运行时内存分配情况等手段进行分析和查找。通过这种方法解决内存泄漏问题往往需要耗费大量的人力,而且并不能保证能够找到所有导致内存泄漏的代码。
二、解决思路
1、JAVA内存回收机制
让我们先了解一下JAVA的内存回收机制,再看看如何解决JAVA内存泄漏问题。Java规范中并没有规定如何进行内存回收,但主要的内存回收机制有两种,Reference counting collector和Tracing collector。
Reference counting collector是java早期常用的内存回收机制,原理是为每一个对象设置一个引用计数器,对象创建时计数器设置为1,当一个新的引用指向该对象时,计数器加1,当该对象的一个引用超过作用范围或者被赋于新值时计数器减1,计数器为0时该对象就会被垃圾回收。这种方式的优点是垃圾回收是一点一点进行的,不会导致程序长时间停顿,对于实时性要求较高的程序比较有利。缺点是不能检测到对象间的交叉引用,如果被回收对象间存在交叉引用,对象的引用计数器不为0,对象就不能被回收。
Tracing collector的原理是从每一个根对象(根对象一般是线程堆栈中的本地变量所引用的对象,这些对象可以被线程入口点直接访问)出发,画出一个对象引用图。在对象引用图中不存在的对象就不能再被程序访问到,对程序的执行不再有影响,因此应该被垃圾回收。这种算法可以正确地回收存在交叉引用的对象,但会造成程序执行有一定的停顿。
2、如何才能让对象被垃圾回收
从JAVA的内存回收机制可以看出,对象的垃圾回收是有要求的:当一个对象不存在到根对象的路径时,该对象才能被回收。因此需要我们在编写程序时保证当一个对象需要被释放时,该对象中不存在任何会直接或者间接连接到根对象的引用。
对于服务器端程序,这个要求比较容易做到;但对于基于Swing的客户端程序,要做到这个就比较难了,Swing组件通常由多个子组件构成的,各个子组件间经常互相引用;而且由于Swing采用了MVC的思想,组件中注册了许多的监听器,而大多数监听器都是内部类,在这些内部类中常常又使用了组件中的方法和变量,这种情况导致JAVA Swing程序的对象间存在大量交叉引用,形成了复杂的网状结构。在这种情况下,如果想要释放一个对象内存,要从该对象的所有引用关系中找到并打断所有会连接到根对象的引用是比较困难的。
图1是通过工具在一个运行的JAVA程序中截取的Swing组件对象的引用关系图,通过该图可以看出这些对象间的引用关系呈复杂的网状结构。从图中可以看到,要从该图中找到一个对象的所有到根对象的引用关系,基本上是难以完成的。
图1界面Swing组件对象间复杂的交叉引用关系
3、方案一
最简单的解决方案是当一个Swing对象不再被使用时,通过编码显示地打断该对象和所有其它对象间的引用关系,使该对象孤立,以便JAVA内存回收机制回收。我在工作过程中最先使用这种方法来解决内存泄漏问题,但很快我发现使用这种方法需要大量额外的代码来保证对象被回收,让界面编码工作工作变得很繁琐,界面代码冗长,可读性变差。而且由于这种方法依赖人来判断哪些引用关系需要打断,难免出现遗漏,对内存泄漏问题的解决不是很完善。该方案参见下面的代码段,其中蓝色的部分是为了释放内存而多出的代码。
例1:通过编码显示打断待回收对象和其它对象的引用关系
/**
* 释放资源
*/
public void free()
{
if (dirty)
{
if (ShowMsgPane.showConfirmDialog(this, I18nUtil.getLabelValue("IsSave")) == ShowMsgPane.OK_OPTION)
{
saveMaintenancesInfo();
}
}
alarmData = null;
user = null;
viewType = 0;
txtCustomMainte.getDocument().removeDocumentListener(documentListener);
txtCustomMainte = null;
documentListener = null;
detailPanel.freeResource();
detailPanel = null;
alarmView.getAlarmModel().removeTableModelListener(tableModelListener);
alarmView = null;
tableModelListener = null;
clearAlarmBtn.removeActionListener(clearAlarmAction);
clearAlarmAction = null;
clearAlarmBtn = null;
confirmAlarmBtn.removeActionListener(confirmAlarmAction);
confirmAlarmAction = null;
confirmAlarmBtn = null;
reconfirmAlarmBtn.removeActionListener(reconfirmAlarmAction);
reconfirmAlarmAction = null;
reconfirmAlarmBtn = null;
manualReturnAlarmBtn.removeActionListener(manualReturnAction);
manualReturnAction = null;
manualReturnAlarmBtn = null;
maintenancesInfoBtn.removeActionListener(maintenancesInfoAction);
maintenancesInfoAction = null;
maintenancesInfoBtn = null;
closeBtn.unregisterKeyboardAction(ESCAPE_STROKE);
closeBtn.removeActionListener(closeAction);
closeAction = null;
closeBtn = null;
this.removeWindowListener(windowListener);
windowListener = null;
this.removeAll();
this.dispose();
}
4、方案二
既然难以判断究竟哪些引用需要显示打断,为何不干脆将所有引用关系都打断呢?宁可错杀一千,不可漏网一个 ^_^!
要将所有引用关系都打断,有两个方面,一是将待回收对象向外的所有引用打断,即将所有类变量置为null;二是需要将所有其他对象对待回收对象的引用打断,一般是注销该对象在其他对象上注册的回调接口,如各种监听器。通过这两个步骤可以保证待回收对象成为孤立对象,被JAVA内存回收机制回收。
由于标准的Swing监听器是有限的,可以通过判断Swing组件的类型来释放该组件向其它对象注册的标准Swing监听器;至于将类变量置为null,可以通过JAVA反射机制来进行。因此完全可以采用一个工具方法来代替例1中释放内存的代码,在这个工具方法中,递归处理被释放组件对象的每一个子组件,注销组件的各种标准的Swing监听器,将子组件从父组件中移出,并通过反射机制将对象的类变量置为null。
该方法的代码参见四、工具类代码
采用这种方法后,例1的代码修改如下
例2:采用公共方法来回收内存
/**
* 释放资源
*/
public void free()
{
if (dirty)
{
if (ShowMsgPane.showConfirmDialog(this, I18nUtil.getLabelValue("IsSave")) == ShowMsgPane.OK_OPTION)
{
saveMaintenancesInfo();
}
}
UIReleaseUtil.freeSwingObject(this);
}
对比例1,可见采用方案二的代码精简了许多。
4、其它注意事项
前面的方法可以保证大部分情况下对象被垃圾回收,但还有一些注意事项。
JDialog内存释放:JDialog默认的关闭动作是隐藏对话框,而不是释放对话框,因此如果需要在关闭对话框时释放对话框,需要设置对话框的默认关闭动作为DISPOSE_ON_CLOSE,另外在关闭对话框时需要调用UIReleaseUtil.freeSwingObject()方法释放资源。具体参见UIReleaseUtil.java的public static void createFreeableDlg(JDialog dlg)方法。
对于自己实现的非标准Swing监听器,UIReleaseUtil.freeSwingObject()方法不能注销,需要在调用UIReleaseUtil.freeSwingObject()前显示注销。
由于在调用UIReleaseUtil.freeSwingObject()对Swing对象释放后,类变量都将被置为null,因此如果需要在调用UIReleaseUtil.freeSwingObject()后使用该对象的类变量,需要在调用UIReleaseUtil.freeSwingObject()方法前先将该变量保存在别处。这种情况通常出现在使用对话框返回数据时。
一言以蔽之,好的方法可以提高工作效率,但只有深入了解了JAVA内存回收机制的工作原理,才能够写出没有内存泄漏的高质量的JAVA程序。
三、效果评价
采用方案二的方法,只需要很少的代码就可以解决JAVA Swing程序的内存泄露问题,将编码人员从繁琐的内存泄露代码编写中解脱出来,可以更好地关注业务逻辑。具有较好的实际效果。
本方法适用于所有采用JAVA进行开发的软件项目,只需要系统人员参照UIReleaseUtil.java定义项目的内存释放工具类,然后对负责界面编码的人员进行少量培训即可。
四、工具类代码
/**
* <p>类型名称: UIReleaseUtil </p>
* <p>类型描述: 释放资源的工具类。</p>
*/
public class UIReleaseUtil
{
private UIReleaseUtil()
{
}
/**
* 释放资源
* @param cmp
*/
public static void freeSwingObject(Component cmp)
{
freeSwingObjectImpl(cmp);
freeObject(cmp);
}
/**
* 使一个对话框关闭时内存可以被释放
* @param dlg
*/
public static void createFreeableDlg(JDialog dlg)
{
dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
dlg.addWindowListener(new WindowCloseAdapter());
}
private static void freeSwingObjectImpl(Component cmp)
{
if (cmp == null)
{
return;
}
freeComponent(cmp);
freeContainer(cmp);
freeJComponent(cmp);
freeButton(cmp);
freeText(cmp);
freeWindow(cmp);
}
private static void freeComponent(Component cmp)
{
//注销并释放监听器资源
EventListener[] listeners = cmp.getComponentListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeComponentListener((ComponentListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getFocusListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeFocusListener((FocusListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getHierarchyListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeHierarchyListener((HierarchyListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getHierarchyBoundsListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeHierarchyBoundsListener((HierarchyBoundsListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getInputMethodListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeInputMethodListener((InputMethodListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getKeyListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeKeyListener((KeyListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getMouseListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeMouseListener((MouseListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeMouseMotionListener((MouseMotionListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getMouseWheelListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removeMouseWheelListener((MouseWheelListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = cmp.getPropertyChangeListeners();
for (int i = 0; i < listeners.length; i++)
{
cmp.removePropertyChangeListener((PropertyChangeListener) listeners[i]);
}
closeUIFreeable(listeners);
}
private static void freeContainer(Component cmp)
{
if (!(cmp instanceof Container))
{
return;
}
Container container = (Container) cmp;
Component[] cmps = container.getComponents();
for (int i = 0; i < cmps.length; i++)
{
freeSwingObjectImpl(cmps[i]);
}
container.removeAll();
}
private static void freeJComponent(Component cmp)
{
if (!(cmp instanceof JComponent))
{
return;
}
JComponent jcmp = (JComponent) cmp;
//注销并释放监听器资源
EventListener[] listeners = jcmp.getAncestorListeners();
for (int i = 0; i < listeners.length; i++)
{
jcmp.removeAncestorListener((AncestorListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = jcmp.getContainerListeners();
for (int i = 0; i < listeners.length; i++)
{
jcmp.removeContainerListener((ContainerListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = jcmp.getVetoableChangeListeners();
for (int i = 0; i < listeners.length; i++)
{
jcmp.removeVetoableChangeListener((VetoableChangeListener) listeners[i]);
}
closeUIFreeable(listeners);
//释放keystroke
KeyStroke[] keystrokes = jcmp.getRegisteredKeyStrokes();
for (int i = 0; i < keystrokes.length; i++)
{
jcmp.unregisterKeyboardAction(keystrokes[i]);
}
closeUIFreeable(keystrokes);
//其他
ActionMap actionMap = jcmp.getActionMap();
if (actionMap != null)
{
actionMap.clear();
}
jcmp.setActionMap(null);
}
private static void freeWindow(Component cmp)
{
if (!(cmp instanceof Window))
{
return;
}
Window window = (Window) cmp;
//注销并释放监听器资源
EventListener[] listeners = window.getWindowFocusListeners();
for (int i = 0; i < listeners.length; i++)
{
window.removeWindowFocusListener((WindowFocusListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = window.getWindowListeners();
for (int i = 0; i < listeners.length; i++)
{
window.removeWindowListener((WindowListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = window.getWindowStateListeners();
for (int i = 0; i < listeners.length; i++)
{
window.removeWindowStateListener((WindowStateListener) listeners[i]);
}
closeUIFreeable(listeners);
window.dispose();
}
private static void freeButton(Component cmp)
{
if (!(cmp instanceof AbstractButton))
{
return;
}
AbstractButton btn = (AbstractButton) cmp;
//注销并释放监听器资源
EventListener[] listeners = btn.getActionListeners();
for (int i = 0; i < listeners.length; i++)
{
btn.removeActionListener((ActionListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = btn.getChangeListeners();
for (int i = 0; i < listeners.length; i++)
{
btn.removeChangeListener((ChangeListener) listeners[i]);
}
closeUIFreeable(listeners);
listeners = btn.getItemListeners();
for (int i = 0; i < listeners.length; i++)
{
btn.removeItemListener((ItemListener) listeners[i]);
}
closeUIFreeable(listeners);
closeUIFreeable(btn.getAction());
btn.setAction(null);
}
private static void freeText(Component cmp)
{
if (!(cmp instanceof JTextComponent))
{
return;
}
JTextComponent text = (JTextComponent) cmp;
EventListener[] listeners = text.getCaretListeners();
for (int i = 0; i < listeners.length; i++)
{
text.removeCaretListener((CaretListener) listeners[i]);
}
closeUIFreeable(listeners);
}
static void freeObject(final Object obj)
{
Field[] fields = obj.getClass().getDeclaredFields();
for (int i = 0; i < fields.length; i++)
{
if (Modifier.isFinal(fields[i].getModifiers()))
{
continue;
}
if (Modifier.isStatic(fields[i].getModifiers()))
{
continue;
}
//基本类型或者String不释放
if (fields[i].getType().isPrimitive() || fields[i].getType().getName().equals("java.lang.String"))
{
continue;
}
try
{
fields[i].setAccessible(true);
fields[i].set(obj, null);
}
catch (Exception ignore)
{
dMsg.warn(ignore);
}
}
}
private static void closeUIFreeable(Object[] freeables)
{
for (int i = 0; i < freeables.length; i++)
{
closeUIFreeable(freeables[i]);
}
}
private static void closeUIFreeable(Object freeable)
{
if (freeable instanceof UIFreeable)
{
((UIFreeable) (freeable)).freeResource();
}
}
private static DebugPrn dMsg = new DebugPrn(GuiUtil.class.getName());
}
/**
* <p>文件名称: WindowCloseAdapter.java </p>
* <p>文件描述: 窗口关闭器。
* <p>用于关闭对话框,并回收其资源。</p>
*/
class WindowCloseAdapter extends WindowAdapter
{
/**
* 默认构造方法。
*/
public WindowCloseAdapter()
{
super();
}
/**
* 带是否释放资源参数的构造方法。
* @param dispose boolean
*/
public WindowCloseAdapter(boolean dispose)
{
super();
this.dispose = dispose;
}
/**
* @see java.awt.event.WindowListener#windowClosing(WindowEvent)
* @param e
*/
public void windowClosing(WindowEvent e)
{
Window source = e.getWindow();
if (source != null)
{
if(!dispose)
{
source.hide();
return;
}
if (source instanceof UIFreeable)
{
((UIFreeable) source).freeResource();
}
source.dispose();
GuiUtil.freeSwingObject(source);
}
}
//关闭的时候是否释放资源
private boolean dispose = true;
}
参考资料
1、 Bill Venners Java's garbage-collected heap
http://www.javaworld.com/javaworld/jw-08-1996/jw-08-gc.html