Java开发网 |
注册 |
登录 |
帮助 |
搜索 |
排行榜 |
发帖统计
|
您没有登录 |
» Java开发网 » Java GUI 设计
打印话题 寄给朋友 订阅主题 |
作者 | 偶的第一篇自己翻译的E文--Controlling Class Loading |
bluecolor
发贴: 16 积分: 3 |
于 2004-12-15 23:34
JAVA拾零之内存使用 广州师范学院 bluecolor QQ:38891681 载入太多的类会使你的程序占用大量的内存空间。幸运的是,有多种技术可以帮助你减少载入的类的数目。 在本文中讨论三种用于控制类的载入的策略(tactics)。所有的这三种策略都与JAVA语言的动态映射功能(dynamic reflection features)相关。虽然使用映射来调用对象方法比平常直接调用对象方法要慢些,但在许多情况下,牺牲轻微的运行速度而使用映射还是值得的。 一、 延迟类的载入 1、问题陈述 很明显,一个类必须在它的实例创建前载入内存。然而,许多编译器(factors)会比你想象中还要早地载入类。例如,一些JIT编译器会在一个方法执行前,将该方法所引用的类载入内存。 为了更好地说明这个问题,简单地举个例子。假设有一个简单的文字处理程序。其一个主要的功能是能打开和显示一系列类型的文档。该程序定义一个Translator类的框架(framework)来处理不同类型的数据,如下图所示: 在该框架里,Translator是一个很小的接口。不过每个实现(implements)它的类就很大。实际上,每一个Translator的实现类都是一个类的集合。这就意味着,载入一个Translator实现类的代价很大。在我们的文字处理的例子中,当打开文档时,将使用工厂方法来创建适当的Translator类。方法代码code1如下: Code1: public static Translator getTranslator(String fileType) { if (fileType.equals("doc")) { return new WordTranslator(); } else if (fileType.equals("html")) { return new HTMLTranslator(); } else if (fileType.equals("txt")) { return new PlainTranslator(); } else if (fileType.equals("xml")) { return new XMLTranslator(); } else { return new DefaultTranslator(); } } 下图展示了该程序使用-verbose选项进行运行时所显示的信息片断,可见在文档打开前,所有的translators实现类都被载入,而不是只载入文档所需的特定的translators实现类。 2、解决方法:使用映射(Reflection)延迟类的载入 虽然映射(Reflection)会比直接调用(dispatch)方法要慢,但在一些情况下,这没什么大不了的。 在我们刚才讨论的文字处理程序中,使用映射(reflection)可以避免在需要一个特定类时,同时载入其他类。下面的代码code2展示了重写的工厂方法,该方法使用简单的类映射(simple class reflection)来避免以上情况发生。而且该方法功能上和在code1所展示的方法的功能是一样的。 Code2: public static Translator getTranslator(String fileType) { try { if (fileType.equals("doc")) { return (Translator)Class.forName( "WordTranslator").newInstance(); } else if (fileType.equals("html")) { return (Translator)Class.forName( "HTMLTranslator").newInstance(); } else if (fileType.equals("txt")) { return (Translator)Class.forName( "PlainTranslator").newInstance(); } else if (fileType.equals("xml")) { return (Translator)Class.forName( "XMLTranslator").newInstance(); } else { return new DefaultTranslator(); } } catch (Exception e) { return new DefaultTranslator(); } } 当然,你不必在你的程序里机械地(automatically)使用映射(reflection)。在任何一种情况下,你都需要将控制类的载入所得到的利益和增加代码的复杂性并降低计算性能的代价作比较,根据你自己的情况来决定是否使用映射(reflection)。这个策略对于一些大的或大量的不想被马上载入的类很有用。 二、 减少类的数量 1、问题陈述 为了举例说明几种减少创建类的数目的方法,我们看一下JavaBeans的监听模式。JavaBeans模型鼓励创建许多很小的类,这就导致了内存的开销增加。然而,有多种的方法去使用beans的事件机制,其中一些比另一些对内存的开销比较小。 JDK 1.1为JAVA引入了内部类,这使得程序员能轻易处理JavaBeans组件体系(component architecture)中的事件模型(event model)。下图展示了一个简单的使用Swing JButton类的应用程序运行界面。当按钮被按下时,该程序向控制台输出一串字符。在随后的章节里,会使用该程序的几个版本来举例说明如何减少程序中类的数量。 简单的UI应用程序 Code3中的类的代码使用内部类来为所有的按钮添加监听器(listeners),并为每个按钮的动作(action)创建一个相应的内部类,每个类的一个实例都与一个按钮相关联。在内部类中的actionPerformed方法调用主类中适当的方法(在该代码中,相关的方法只是输出一串字符)。 Code3: public class Listener1 extends JFrame { public Listener1() { JButton open = new JButton("Open"); JButton close = new JButton("Close"); JButton save = new JButton("Save"); getContentPane().setLayout(new FlowLayout()); getContentPane().add(open); getContentPane().add(close); getContentPane().add(save); open.addActionListener(new OpenAction()); close.addActionListener(new CloseAction()); save.addActionListener(new SaveAction()); pack(); setVisible(true); } protected void open(String file) { System.out.println("Open a file"+file); } protected void close(String file) { System.out.println("Close a file"+file); } protected void save(String file) { System.out.println("Save a file"+file); } class OpenAction implements ActionListener { public void actionPerformed(ActionEvent e) { open("c:\\logo.txt"); } } class CloseAction implements ActionListener { public void actionPerformed(ActionEvent e) { close("c:\\logo.txt"); } } class SaveAction implements ActionListener { public void actionPerformed(ActionEvent e) { save("c:\\logo.txt"); } } public static void main(String[] args) { new Listener1(); } } 很明显,创建内部类会稍微增加内存的开销,然而内部类所生产的文件只有几百个字节,而且每个类都只会占有3K左右的内存空间. 在这个例子中,每个内部类只占用大约10K的内存空间.但一个大的、复杂的程序,可能需要上百个监听器,这就会占用几百K的内存。如果你的程序属于后者的话,那可以通过减少载入的类来优化你的程序了。 2、解决方法一:更改(collapse)监听器 One way to optimize the previous example so it consumes less RAM is to collapse the listeners. The program could be written so that it functions identically with only one listener, as shown in Listing 6-4. 减少上面所用的程序范例的内存消耗的一种方法是更改(collapse)监听器。 该范例的代码需要重写,使得所有的功能都集成在一个监听器类里。代码如code4 所示: Code4: class ButtonAction implements ActionListener { public void actionPerformed(ActionEvent e) { JButton b = (JButton)e.getSource(); if ( b.getText().equals("Open") ) { open("c:\\logo.txt"); } else if (b.getText().equals("Close")) { close("c:\\logo.txt"); } else if (b.getText().equals("Save")) { save("c:\\logo.txt"); } } } 主类的修改如code5.使用该方法的好处是所有按钮的监听处理只用一个类来处理,从而减少了载入的类的数量。 Code5: ActionListener listener = new ButtonAction(); open.addActionListener(listener); close.addActionListener(listener); save.addActionListener(listener); 不过这个解决方案是有问题的。主要的问题是它维护起来比较难。如果程序里有100个行为(actions),你就需要使用100个if-then-else语句。另外一个问题是会是代码变得很脆弱(fragile)。解决方案所依赖的按钮的标题(title)可能会被改变,尤其是在程序实现本地化时。虽然这种优化方案能改善内存的使用情况,但这不不能弥补它对可维护性和稳定性所造成的负面影响。 3、解决方法二:使用映射(Reflection) Another way to reduce the number of classes that are loaded to handle events is to use reflection. In the Translator example in Section 6.1.1, reflection was used to avoid loading classes. It can also be used to avoid creating classes at all. The code in Listing 6-6 shows a generic class that can be used to dispatch an action from any ActionEvent source to any zero-argument method on the target object. 另一个减少用于处理事件(events)类的数量的方法是使用映射(reflection)。在代码code2中,映射(reflection)用于避免类的载入。它同样可以用于避免类的创建。在代码code6中展示了一个普通(generic)类,它将一个行为(action)从ActionEvent源传送到目标对象的方法中: Code6: class ReflectiveAction implements ActionListener { String methodName; //方法名 Object target; //方法所属的对象 public ReflectiveAction(Object target, String methodName) { this.target = target; this.methodName = methodName; } public void actionPerformed(ActionEvent e) { try { //参数的类型,在该例子中是字符串 Class[] argTypes =new Class[] {String.class}; //取得对象中的方法 Method method = target.getClass().getMethod(methodName, argTypes); //参数 Object[] args = {"c:\\logo.txt"}; //调用对象中的方法 method.invoke(target, args); } catch (Exception ex) { ex.printStackTrace(); } } } 为了使用这个类,你需要创建它的一个实例,并且将目标对象(target object)及该对象中的被调用的方法明传递给它。当一个事件(event)到达时,ReflectiveAction会选择对应的方法并执行。Code7展示了ReflectiveAction是如何向范例中的按钮添加监听器的。 Code7: open.addActionListener(new ReflectiveAction(this, "open")); close.addActionListener(new ReflectiveAction(this, "close")); save.addActionListener(new ReflectiveAction(this, "save")); 像ReflectiveAction这种类有时候被称作蹦床类(trampolines),因为它们在一个方法里触发(spring)另一个方法。蹦床类(trampolines)成为诱人的解决方案有两个原因: 1、 一个简单的类可以被任何一个同一类型的监听器所使用(A single class can be used for any listeners of the same type) 2、 使用蹦床类(trampolines)能使代码更加清晰(The code that results from their use is very clear) 但蹦床类(trampolines)有一些缺点。最大的问题是在编译时没有对以参数形式输入的方法名进行检测(The biggest problem is that you lose compile-time type checking lose compile-time type checking)。如果你所传入的方法名不正确的话,你的程序在编译时不会出现错误,但在运行的时候将会出现运行错误(runtime error)。由于使用映射(reflection)比直接调用(dispatch)方法要慢,所以它对程序会有轻微的影响。但是,映射(reflection)查找并调用方法所需的时间很少,用户是不会觉察(perceive)到的。 4、 解决方法三:使用代理 虽然蹦床(trampolines)类在很多情况下都很有用,但在动态环境(dynamic environments)(如builder工具)下使用起来却没那么好。这种工具需要为任何一个bean所产生的事件类型定义一个蹦床(trampolines)类。但由于JavaBeans的事件模型可扩展成无穷无尽的事件类型,所以这是不可行的。 在J2SE V1.3中引入了一个新类——java.lang.reflect.Proxy。这个类就是为了处理上面所提到的情况的。该Proxy类允许在运行过程中创建简单的蹦床风格(trampoline-style)的类。这使得创建一个能在运行时为任何类型的监听器创建代理(proxy)的代理(proxy)提供者(builder)成为可能。 注意:java.lang.reflect.Proxy不推荐使用于普通的编程中。这种策略(tactic)最好用于那些需要极大机动性(demand maximum flexibility)的系统。如:JavaBeans的汇编(assembly)工具或其他允许用户设定行为(configure actions)的工具。Proxy类对于这些应用程序是非常有用的。 Listing 6-8 shows a class that builds dynamic proxy objects. This class implements the java.lang.reflect.InvocationHandler interface, which is a companion of the Proxy class. InvocationHandler defines a single method called invoke. This method allows you to specify how a particular call is to be routed through the proxy. This example simply ignores the arguments passed to the original function-no arguments are passed to the target object. A more full-featured implementation might pass along the arguments as well. Code7展示了一个建立动态代理对象的类。这个类继承实现了java.lang. reflect.InvocationHandler接口。该接口通常和Proxy类一起使用。InvocationHandler定义了一个简单的方法invoke()。这个方法允许你决定如何通过代理来调用特定的方法。 class DynamicProxy implements InvocationHandler { Object target; String methodName; Object[] noArgs = {}; Class[] argTypes = {}; public static Object makeProxy(Object target, String methodName, Object[] args, Class[] argtypes, Class impl) { ClassLoader loader = target.getClass().getClassLoader(); DynamicProxy handler = new DynamicProxy(); handler.target = target; handler.methodName = methodName; noArgs = args; argTypes = argtypes; // create a new proxy object // the object will implement the interface passed // into the impl parameter // it will dispatch all method calls to the proxy // to the target object via the invoke method below return Proxy.newProxyInstance(loader , new Class[]{impl}, handler); } public Object invoke(Object proxy, Method method, Object[] args) { try { Method targetMethod = target.getClass(). getMethod(methodName, argTypes); return targetMethod.invoke(target, noArgs); } catch (Exception e ) { e.printStackTrace(); return null; } } } DynamicProxy提供了一个用于创新新的代理的静态方法。你需要提供方法所属对象(也就是代理对象(the target for the proxy)),你想要调用的方法名,该方法的参数数组,该方法参数类型数组及代理要实现的接口。 当你使用了代理后,你的代码会相应地变得有些复杂。当你第一次创建用于实现指定地接口的代理时,一个新的类被创建。例如:如果Proxy实现了ActionListener,一个命名为$ProxyActionListener的类就会被创建。但你从来不直接和这个类进行交互(interact)。每次你通过代理来调用一个方法(method)时,代理就会调用InvocationHandler中的invoke方法(method)。 在DynamicProxy类的范例中,所以的方法调用都被重定向到指定类对象的特定方法中。Code8展示如何使用DynamicProxy向按钮添加监听器。 Code8: open.addActionListener( (ActionListener)DynamicProxy.makeProxy(this, "open", New String[]{"c:\\logo.txt"}, new Class[] {String.class}, ActionListener.class)); close.addActionListener( (ActionListener)DynamicProxy.makeProxy(this, "close", New String[]{"c:\\logo.txt"}, new Class[] {String.class}, ActionListener.class)); save.addActionListener( (ActionListener)DynamicProxy.makeProxy(this, "save", New String[]{"c:\\logo.txt"}, new Class[] {String.class}, ActionListener.class)); 这种策略与蹦床(trampolines)策略有什么相同和不同点呢?相同之处在于它们的运行的消耗相似。其区别是当第一个ActionListener代理被创建后,以后地ActionListener代理就不会产生新的类。取而代之的是,你将会有先前(previously)所创建的类的实例。使用代理的好处是你不必每次都为你所使用的新的监听器接口创建一个蹦床(trampolines)类,它会自动为你创建。使用代理的缺点是它使用起来要比蹦床(trampolines)复杂。 三、总结:谁真正需要使用这些策略呢? 虽然这些用于减少载入类的策略中的一部分看上去很不可思议,但相似的技术已经成功地应用到今天的应用开发中,不仅仅在JavaBeans监听器类的使用方面起作用,在其他的一些情况下同样能起到作用。为了延迟类的载入,几个Swing类使用了蹦床(trampolines)策略。Swing的早期版本会为程序载入许多不需要的类。为了解决这个问题,与本文所提及的类似的技术被用于延迟一些类的载入,直到它们真的要被使用。 原文: http://java.sun.com/docs/books/performance/1st_edition/html/JPClassLoading.fm.html JAVA拾零之内存使用.doc (87.5k) Windows环境下的tomcat + apache配置(绝对实践操作版) |
话题树型展开 |
人气 | 标题 | 作者 | 字数 | 发贴时间 |
2651 | 偶的第一篇自己翻译的E文--Controlling Class Loading | bluecolor | 12252 | 2004-12-15 23:34 |
已读帖子 新的帖子 被删除的帖子 |
Powered by Jute Powerful Forum® Version Jute 1.5.6 Ent Copyright © 2002-2021 Cjsdn Team. All Righits Reserved. 闽ICP备05005120号-1 客服电话 18559299278 客服信箱 714923@qq.com 客服QQ 714923 |