﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>BlogJava-蒙古狼-文章分类-深入jvm</title><link>http://www.blogjava.net/landy/category/10464.html</link><description>像狼一样凶狠</description><language>zh-cn</language><lastBuildDate>Tue, 27 Feb 2007 12:58:04 GMT</lastBuildDate><pubDate>Tue, 27 Feb 2007 12:58:04 GMT</pubDate><ttl>60</ttl><item><title>通过ClassLoader管理组件依赖</title><link>http://www.blogjava.net/landy/articles/42943.html</link><dc:creator>独孤过客</dc:creator><author>独孤过客</author><pubDate>Tue, 25 Apr 2006 01:44:00 GMT</pubDate><guid>http://www.blogjava.net/landy/articles/42943.html</guid><wfw:comment>http://www.blogjava.net/landy/comments/42943.html</wfw:comment><comments>http://www.blogjava.net/landy/articles/42943.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/landy/comments/commentRss/42943.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/landy/services/trackbacks/42943.html</trackback:ping><description><![CDATA[
		<div>Java的类加载机制是非常强大的。你可以利用外部第三方的组件而不需要头文件或静态连接。你只需要简单的把组件的JAR文件放到classpath下的目录中。运行时引用完全是动态处理的。但如果这些第三方组件有自己的依赖关系时会怎么样呢？通常这需要开发人员自己解决所有需要的相应版本的组件集，并且确认他们被加到classpath中。<br />　　<br />　　<b>JAR清单文件</b><br />　　<br />　　实际上你不需要这样做，Java的类加载机制可以更优雅地解决这个问题。一种方案是需要每一个组件的作者在JAR清单中定义内部组件的依赖关系。这里清单是指一个被包含在JAR中的定义文件元数据的文本文件（META-INF/MANIFEST.MF）。最常用的属性是Main-Class，定义了通过java –jar方式定位哪个类会被调用。然而，还有一个不那么有名的属性Class-Path可以用来定义他所依赖的其他JAR。Java缺省的ClassLoader会检查这些属性并且自动附加这些特定的依赖到classpath中。<br />　　<br />　　让我们来看一个例子。考虑一个实现交通模拟的Java应用，他由三个JAR组成：<br />　　<br />　　·simulator-ui.jar：基于Swing的视图来显示模拟的过程。<br />　　·simulator.jar:用来表示模拟状态的数据对象和实现模拟的控制类。<br />　　·rule-engine.jar:常用的第三方规则引擎被用来建立模拟规则的模型。<br />　　simulator-ui.jar依赖simulator.jar，而simulator.jar依赖rule-engine.jar。<br />　　<br />　　而通常执行这个应用的方法如下：<br />　　$ java -classpath<br />　　simulator-ui.jar:simulator.jar:rule-engine.jar<br />　　com.oreilly.simulator.ui.Main<br />　　<br />　　编者注：上面的命令行应该在同一行键入；只是由于网页布局的限制看起来好像是多行。<br />　　<br />　　但我们也可以在JAR的清单文件中定义这些信息，simulator-ui的MANIFEST.MF如下：<br />　　<br />　　Main-Class: com.oreilly.simulator.ui.Main<br />　　Class-Path: simulator.jar<br />　　<br />　　而simulator的MANIFEST.MF包含：<br />　　Class-Path: rule-engine.jar<br />　　<br />　　rule-engine.jar或者没有清单文件，或者清单文件为空。<br />　　<br />　　现在我们可以这样做：<br />　　$ java -jar simulator-ui.jar<br />　　<br />　　Java会自动解析清单的入口来取得主类及修改classpath，甚至可以确定simulator-ui.jar的路径和解释所有与这个路径相关的Class-Path属性，所以我们可以简单按照下面的方式之一来做：<br />　　$ java -jar ../simulator-ui.jar<br />　　$ java -jar /home/don/build/simulator-ui.jar<br />　　<br />　　<b>依赖冲突</b><br />　　<br />　　Java的Class-Path属性的实现相对于手工定义整个classpath是一个大的改善。然而，两种方式都有自己的限制。一个重要的限制就是你只能加载组件的一个特定版本。这看起来是很显然的因为许多编程环境都有这个限制。但是在大的包含多个第三方依赖的多JAR项目中依赖冲突是很常见的。<br />　　<br />　　例如，你正在开发一个通过查询多个搜索引擎并比较他们的结果的搜索引擎。Google和Amazon的Alexa都支持使用SOAP作为通讯机制的网络服务API，也都提供了相应的Java类库方便访问这些API。让我们假设你的JAR- metasearch.jar，依赖于google.jar和amazon.jar，而他们都依赖于公共的soap.jar。<br />　　<br />　　现在是没有问题，但如果将来SOAP协议或API发生改变时会怎么样呢？很可能这两个搜索引擎不会选择同时升级。可能在某一天你访问Amazon时需要SOAP1.x版本而访问Google时需要SOAP2.x版本，而这两个版本的SOAP并不能在同一个进程空间中共存。在这里，我们可能包含下面的JAR依赖：<br />　　<br />　　$ cat metasearch/META-INF/MANIFEST.MF<br />　　Main-Class: com.onjava.metasearch.Main<br />　　Class-Path: google.jar amazon.jar<br />　　<br />　　$ cat amazon/META-INF/MANIFEST.MF<br />　　Class-Path: soap-v1.jar<br />　　<br />　　$ cat google/META-INF/MANIFEST.MF<br />　　Class-Path: soap-v2.jar<br />　　<br />　　上面正确地描述了依赖关系，但这里并没有包含什么魔法--这样设置并不会像我们期望地那样工作。如果soap-v1.jar和soap-v2.jar定义了许多相同的类，我们肯定这是会出问题的。<br />　　$ java -jar metasearch.jar<br />　　SOAP v1: remotely invoking searchAmazon<br />　　SOAP v1: remotely invoking searchGoogle<br />　　<br />　　你可以看到，soap-v1.jar被首先加在classpath中，因此实际上也只有他会被使用。上面的例子等价于：<br />　　$ java -classpath<br />　　metasearch.jar:amazon.jar:google.jar:soap-v1.jar:soap-v2.jar<br />　　# WRONG!<br />　　<br />　　编者注：上面的命令行应该在同一行键入；只是由于网页布局的限制看起来好像是多行。<br />　　<br />　　有趣的是如果Yahoo也发布了一个网络服务API，而他看起来并没有依赖于现有的SOAP/XML-RPC类库。在较小的项目中，组件依赖冲突常被用来作为在你只要手工包装方案或者只需要一两个类时而不使用让你不使用全量组件（如集合类库）的原因之一。手工包装方案有他的用处，但使用已有的组件是更普遍的方式。而且复制其他组件的类到你的代码库永远不是一个好主意。实际上你已经与组件的开发产生分岐而且没有机会在有问题修复或安全升级时合并他。<br />　　<br />　　许多大的项目，如主要的商业组件，已经采用将他们使用的整个组件构建到他们的JAR内部。为了这么做，他们改变了包名使其唯一（如com/acme/foobar/org/freeware/utility），而且直接在他们的JAR中包含类。这样做的好处是可以防止在这些组件中多个版本的冲突，但这也是有代价的。这么做对开发人员来说完全隐藏了对第三方的依赖。但如果这种方式大规模的应用，将会导致效率的降低（包括JAR文件的大小和加载多个JAR版本到进程中的效率降低）。这种方式的问题在于如果两个组件依赖于同一个版本的第三方组件时，就没有协调机制来确定共享的组件只被加载一次。这个问题我们会在下一节进行研究。除了效率的降低外，很可能你这种绑定第三方软件的方式会与那些软件的许可协议冲突。<br />　　<br />　　另一种解决这个问题的方式是每一个组件的开发人员显式的在他的包名中编码一个版本号。Sun的javac代码就采用这个方式—一个com.sun.tools.javac.Main类会简单地转发给com.sun.tools.javac.v8.Maino。每次一个新的Java版本发布，这个代码的包名就改变一次。这就允许一个组件的多个发布版本可以共存在同一个类加载器中并且这使得版本的选择是显式的。但这也不是一个非常好的解决方案，因为或者客户需要准确知道他们计划使用的版本而且必须改变他们的代码来转换到新的版本，或者他们必须依赖于一个包装类来转发方案调用给最新的版本（在这种情况下，这些包装类就会承受我们上面提到的相同问题）。<br />　　<br />　　<b>加载多个发布版本</b><br />　　<br />　　这里我们遇到的问题在大多数项目中也存在，所有的类都会被加载到一个全局命名空间。如果每一个组件有自己的命名空间而且他会加载所有他依赖的组件到这个命名空间而不影响进程的其他部分，那又会怎么样呢？实际上我们可以在Java中这么做！类名不需要是唯一的，只要类名和其所对应的ClassLoader的组合是唯一的就可以了。这意味着ClassLoader类似于命名空间，而如果我们可以加载每一个组件在自己的ClassLoader中，他就可以控制如何满足依赖。他可以代理类定位给其他的包含他的依赖组件所需要的特定版本的ClassLoader。如图1。<br />　　 
<center>　<img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/imgfiles/2005.11.5.13.53.9.11.1.gif" onload="javascript:if(this.width&gt;500)this.style.width=500;" /><br />　　Figure 1. Decentralized class loaders</center><br />　　<br />　　然而这个架构并不比绑定每一个依赖的JAR在自己的JAR中好多少。我们需要的是一个可以确保每一个组件版本仅被一个类加载器加载的中央集权。图2中的架构可以确定每一个组件版本仅被加载一次。<br />　　 
<center>　<img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/imgfiles/2005.11.5.13.53.46.11.2.gif" onload="javascript:if(this.width&gt;500)this.style.width=500;" /><br />　　Figure 2. Class loaders with mediator</center><br />　　<br />　　为了实现这种方式，我们需要创建两个不同类型的类加载器。每一个ComponentClassLoader需要扩展Java的URLClassLoader来提供需要的逻辑来从一个JAR中获取.class文件。当然他也会执行两个其他的任务。在创建的时候，他会获取JAR清单文件并定位一个新属性Restricted-Class-Path。不像Sun提供的Class-Path属性，这个属性暗示特定的JAR应该只对这个组件有效。<br />　　public class ComponentClassLoader extends URLClassLoader {<br />　　// ...　public ComponentClassLoader (MasterClassLoader master, File file)<br />　　{<br />　　// ...　　JarFile jar = new JarFile(file);<br />　　Manifest man = jar.getManifest();<br />　　Attributes attr = man.getMainAttributes();<br />　　List l = new ArrayList();<br />　　String str = attr.getValue("Restricted-Class-Path");<br />　　if (str != null) {<br />　　StringTokenizer tok = new StringTokenizer(str);<br />　　while (tok.hasMoreTokens()) {<br />　　l.add(new File(file.getParentFile(),<br />　　tok.nextToken());<br />　　}<br />　　}<br />　　this.dependencies = l;<br />　　}　public Class loadClass (String name, boolean resolve)<br />　　throws ClassNotFoundException　{<br />　　try {<br />　　// Try to load the class from our JAR.<br />　　return loadClassForComponent(name, resolve);<br />　　} catch (ClassNotFoundException ex) {}<br />　　// Couldn't find it -- let the master look for it<br />　　// in another components.<br />　　return master.loadClassForComponent(name,<br />　　resolve, dependencies);<br />　　}<br />　　public Class loadClassForComponent (String name,<br />　　boolean resolve)<br />　　throws ClassNotFoundException<br />　　{<br />　　C</div>
<img src ="http://www.blogjava.net/landy/aggbug/42943.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/landy/" target="_blank">独孤过客</a> 2006-04-25 09:44 <a href="http://www.blogjava.net/landy/articles/42943.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>关于jvm中类加载完全揭密</title><link>http://www.blogjava.net/landy/articles/42940.html</link><dc:creator>独孤过客</dc:creator><author>独孤过客</author><pubDate>Tue, 25 Apr 2006 01:39:00 GMT</pubDate><guid>http://www.blogjava.net/landy/articles/42940.html</guid><wfw:comment>http://www.blogjava.net/landy/comments/42940.html</wfw:comment><comments>http://www.blogjava.net/landy/articles/42940.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/landy/comments/commentRss/42940.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/landy/services/trackbacks/42940.html</trackback:ping><description><![CDATA[
		<p>类加载是java语言提供的最强大的机制之一。尽管类加载并不是讨论的热点话题，但所有的编程人员都应该了解其工作机制，明白如何做才能让其满足我们的需要。这能有效节省我们的编码时间，从不断调试ClassNotFoundException, ClassCastException的工作中解脱出来。<br /><br />这篇文章从基础讲起，比如代码与数据的不同之处是什么，他们是如何构成一个实例或对象的。然后深入探讨java虚拟机（JVM）是如何利用类加载器读取代码，以及java中类加载器的主要类型。接着用一个类加载的基本算法看一下类加载器如何加载一个内部类。本文的下一节演示一段代码来说明扩展和开发属于自己的类加载器的必要性。紧接着解释如何使用定制的类加载器来完成一个一般意义上的任务，使其可以加载任意远端客户的代码，在JVM中定义，实例化并执行它。本文包括了J2EE关于类加载的规范——事实上这已经成为了J2EE的标准之一。<br /><br /><b><span style="FONT-SIZE: 16px">类与数据<br /></span></b><br />一个类代表要执行的代码，而数据则表示其相关状态。状态时常改变，而代码则不会。当我们将一个特定的状态与一个类相对应起来，也就意味着将一个类事例化。尽管相同的类对应的实例其状态千差万别，但其本质都对应着同一段代码。在JAVA中，一个类通常有着一个.class文件，但也有例外。在JAVA的运行时环境中（Java runtime），每一个类都有一个以第一类(first-class)的Java对象所表现出现的代码，其是java.lang.Class的实例。我们编译一个JAVA文件，编译器都会嵌入一个public, static, final修饰的类型为java.lang.Class，名称为class的域变量在其字节码文件中。因为使用了public修饰，我们可以采用如下的形式对其访问：</p>
		<pre class="overflow" title="pre code">java.lang.Class klass = Myclass.class;</pre>
		<pre class="overflow" title="pre code">一旦一个类被载入JVM中，同一个类就不会被再次载入了（切记，同一个类）。这里存在一个问题就是什么是“同一个类”？正如一个对象有一个具体的状态，即标识，一个对象始终和其代码(类)相关联。同理，载入JVM的类也有一个具体的标识，我们接下来看。<br /><br />在JAVA中，一个类用其完全匹配类名(fully qualified class name)作为标识，这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识。因此，如果一个名为Pg的包中，有一个名为Cl的类，被类加载器KlassLoader的一个实例kl1加载，Cl的实例，即C1.class在JVM中表示为(Cl, Pg, kl1)。这意味着两个类加载器的实例(Cl, Pg, kl1) 和 (Cl, Pg, kl2)是不同的，被它们所加载的类也因此完全不同，互不兼容的。那么在JVM中到底有多少种类加载器的实例？下一节我们揭示答案。<br /></pre>
		<pre class="overflow" title="pre code">
				<br />
				<b>
						<span style="FONT-SIZE: 16px">类加载器</span>
				</b>
		</pre>
		<pre class="overflow" title="pre code">
				<br />
				<br />
				<br />
				<br />
				<br />
				<br />
				<br />
				<br />
				<br />在JVM中，每一个类都被java.lang.ClassLoader的一些实例来加载.类ClassLoader是在包中java.lang里，开发者可以自由地继承它并添加自己的功能来加载类。<br /><br />无论何时我们键入java MyMainClass来开始运行一个新的JVM，“引导类加载器(bootstrap class loader)”负责将一些关键的Java类，如java.lang.Object和其他一些运行时代码先加载进内存中。运行时的类在JRE\lib\rt.jar包文件中。因为这属于系统底层执行动作，我们无法在JAVA文档中找到引导类加载器的工作细节。基于同样的原因，引导类加载器的行为在各JVM之间也是大相径庭。<br />同理，如果我们按照如下方式：</pre>
		<pre class="overflow" title="pre code">log(java.lang.String.class.getClassLoader());</pre>来获取java的核心运行时类的加载器，就会得到null。<br /><br />接下来介绍java的扩展类加载器。扩展库提供比java运行代码更多的特性，我们可以把扩展库保存在由java.ext.dirs属性提供的路径中。<br /><br />(编辑注：java.ext.dirs属性指的是系统属性下的一个key，所有的系统属性可以通过System.getProperties()方法获得。在编者的系统中，java.ext.dirs的value是” C:\Program Files\Java\jdk1.5.0_04\jre\lib\ext”。下面将要谈到的如java.class.path也同属系统属性的一个key。)<br /><br />类ExtClassLoader专门用来加载所有java.ext.dirs下的.jar文件。开发者可以通过把自己的.jar文件或库文件加入到扩展目录的classpath，使其可以被扩展类加载器读取。<br /><br />从开发者的角度，第三种同样也是最重要的一种类加载器是AppClassLoader。这种类加载器用来读取所有的对应在java.class.path系统属性的路径下的类。<br /><br />Sun的java指南中，文章“理解扩展类加载”（Understanding Extension Class Loading）对以上三个类加载器路径有更详尽的解释，这是其他几个JDK中的类加载器<br />●java.net.URLClassLoader <br />●java.security.SecureClassLoader <br />●java.rmi.server.RMIClassLoader <br />●sun.applet.AppletClassLoader <br /><br />java.lang.Thread，包含了public ClassLoader getContextClassLoader()方法，这一方法返回针对一具体线程的上下文环境类加载器。此类加载器由线程的创建者提供，以供此线程中运行的代码在需要加载类或资源时使用。如果此加载器未被建立，缺省是其父线程的上下文类加载器。原始的类加载器一般由读取应用程序的类加载器建立。<br /><br /><b><span style="FONT-SIZE: 16px">类加载器如何工作？</span></b><br />除了引导类加载器，所有的类加载器都有一个父类加载器，不仅如此，所有的类加载器也都是java.lang.ClassLoader类型。以上两种类加载器是不同的，而且对于开发者自订制的类加载器的正常运行也至关重要。最重要的方面是正确设置父类加载器。任何类加载器，其父类加载器是加载该类加载器的类加载器实例。（记住，类加载器本身也是一个类！）<br /><br />使用loadClass()方法可以从类加载器中获得该类。我们可以通过java.lang.ClassLoader的源代码来了解该方法工作的细节，如下：<br /><br /><pre class="overflow" title="pre code">protected synchronized Class&lt;?&gt; loadClass<br />    (String name, boolean resolve)<br />    throws ClassNotFoundException{<br /><br />    // First check if the class is already loaded<br />    Class c = findLoadedClass(name);<br />    if (c == null) {<br />        try {<br />            if (parent != null) {<br />                c = parent.loadClass(name, false);<br />            } else {<br />                c = findBootstrapClass0(name);<br />            }<br />        } catch (ClassNotFoundException e) {<br />            // If still not found, then invoke<br />            // findClass to find the class.<br />            c = findClass(name);<br />        }<br />    }<br />    if (resolve) {<br />            resolveClass(c);<br />    }<br />    return c;<br />}</pre><br /><br />我们可以使用ClassLoader的两种构造方法来设置父类加载器：<br /><br /><pre class="overflow" title="pre code">public class MyClassLoader extends ClassLoader{<br /><br />    public MyClassLoader(){<br />        super(MyClassLoader.class.getClassLoader());<br />    }<br />}</pre><br /><br />或<br /><br /><pre class="overflow" title="pre code">public class MyClassLoader extends ClassLoader{<br /><br />    public MyClassLoader(){<br />        super(getClass().getClassLoader());<br />    }<br />}</pre><br /><br />第一种方式较为常用，因为通常不建议在构造方法里调用getClass()方法，因为对象的初始化只是在构造方法的出口处才完全完成。因此，如果父类加载器被正确建立，当要示从一个类加载器的实例获得一个类时，如果它不能找到这个类，它应该首先去访问其父类。如果父类不能找到它(即其父类也不能找不这个类，等等)，而且如果findBootstrapClass0()方法也失败了，则调用findClass()方法。findClass()方法的缺省实现会抛出ClassNotFoundException，当它们继承java.lang.ClassLoader来订制类加载器时开发者需要实现这个方法。findClass()的缺省实现方式如下：<br /><br /><pre class="overflow" title="pre code">    protected Class&lt;?&gt; findClass(String name)<br />        throws ClassNotFoundException {<br />        throw new ClassNotFoundException(name);<br />    }</pre><br /><br />在findClass()方法内部，类加载器需要获取任意来源的字节码。来源可以是文件系统，URL，数据库，可以产生字节码的另一个应用程序，及其他类似的可以产生java规范的字节码的来源。你甚至可以使用BCEL (Byte Code Engineering Library：字节码工程库)，它提供了运行时创建类的捷径。BCEL已经被成功地使用在以下方面：编译器，优化器，混淆器，代码产生器及其他分析工具。一旦字节码被检索，此方法就会调用defineClass()方法，此行为对不同的类加载实例是有差异的。因此，如果两个类加载实例从同一个来源定义一个类，所定义的结果是不同的。<br /><br />JAVA语言规范（Java language specification）详细解释了JAVA执行引擎中的类或接口的加载（loading），链接（linking）或初始化（initialization）过程。<br /><br />图一显示了一个主类称为MyMainClass的应用程序。依照之前的阐述，MyMainClass.class会被AppClassLoader加载。 MyMainClass创建了两个类加载器的实例：CustomClassLoader1 和 CustomClassLoader2,他们可以从某数据源（比如网络）获取名为Target的字节码。这表示类Target的类定义不在应用程序类路径或扩展类路径。在这种情况下，如果MyMainClass想要用自定义的类加载器加载Target类，CustomClassLoader1和CustomClassLoader2会分别独立地加载并定义Target.class类。这在java中有重要的意义。如果Target类有一些静态的初始化代码，并且假设我们只希望这些代码在JVM中只执行一次，而这些代码在我们目前的步骤中会执行两次——分别被不同的CustomClassLoaders加载并执行。如果类Target被两个CustomClassLoaders加载并创建两个实例Target1和Target2，如图一显示，它们不是类型兼容的。换句话说，在JVM中无法执行以下代码：<br /><br /><pre class="overflow" title="pre code">Target target3 = (Target) target2;</pre><br /><br />以上代码会抛出一个ClassCastException。这是因为JVM把他们视为分别不同的类，因为他们被不同的类加载器所定义。这种情况当我们不是使用两个不同的类加载器CustomClassLoader1 和 CustomClassLoader2，而是使用同一个类加载器CustomClassLoader的不同实例时，也会出现同样的错误。这些会在本文后边用具体代码说明。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120737658.jpg" width="356" onload="javascript:ImgLoad(this);" border="0" resized="1" /><br />图1. 在同一个JVM中多个类加载器加载同一个目标类<br /><br />关于类加载、定义和链接的更多解释，请参考Andreas Schaefer的"Inside Class Loaders."<br /><br /><b><span style="FONT-SIZE: 16px">为什么我们需要我们自己的类加载器</span></b><br />原因之一为开发者写自己的类加载器来控制JVM中的类加载行为，java中的类靠其包名和类名来标识，对于实现了java.io.Serializable接口的类，serialVersionUID扮演了一个标识类版本的重要角色。这个唯一标识是一个类名、接口名、成员方法及属性等组成的一个64位的哈希字段，而且也没有其他快捷的方式来标识一个类的版本。严格说来，如果以上的都匹配，那么则属于同一个类。<br /><br />但是让我们思考如下情况：我们需要开发一个通用的执行引擎。可以执行实现某一特定接口的任何任务。当任务被提交到这个引擎，首先需要加载这个任务的代码。假设不同的客户对此引擎提交了不同的任务，凑巧，这些所有的任务都有一个相同的类名和包名。现在面临的问题就是这个引擎是否可以针对不同的用户所提交的信息而做出不同的反应。这一情况在下文的参考一节有可供下载的代码样例，samepath 和 differentversions，这两个目录分别演示了这一概念。<br /><br />图2 显示了文件目录结构，有三个子目录samepath, differentversions, 和 differentversionspush，里边是例子：<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738658.jpg" width="435" onload="javascript:ImgLoad(this);" border="0" resized="1" /><br />图2. 文件夹结构组织示例<br /><br />在samepath 中，类version.Version保存在v1和v2两个子目录里，两个类具有同样的类名和包名，唯一不同的是下边这行：<br /><br /><pre class="overflow" title="pre code">    public void fx(){<br />        log("this = " + this + "; Version.fx(1).");<br />    }</pre><br /><br />V1中，日志记录中有Version.fx(1)，而在v2中则是Version.fx(2)。把这个两个存在细微不同的类放在一个classpath下，然后运行Test类：<br /><br /><span style="COLOR: blue">set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2<br />%JAVA_HOME%\bin\java Test</span><br /><br />图3显示了控制台输出。我们可以看到对应着Version.fx(1)的代码被执行了，因为类加载器在classpath首先看到此版本的代码。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738345.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图3. 在类路径中samepath测试排在最前面的version 1<br /><br />再次运行，类路径做如下微小改动。 <br /><br /><span style="COLOR: blue">set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1<br />%JAVA_HOME%\bin\java Test</span><br /><br />控制台的输出变为图4。对应着Version.fx(2)的代码被加载，因为类加载器在classpath中首先找到它的路径。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738983.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图4. 在类路径中samepath测试排在最前面的version 2<br /><br />根据以上例子可以很明显地看出，类加载器加载在类路径中被首先找到的元素。如果我们在v1和v2中删除了version.Version，做一个非version.Version形式的.jar文件，如myextension.jar，把它放到对应java.ext.dirs的路径下，再次执行后看到version.Version不再被AppClassLoader加载，而是被扩展类加载器加载。如图5所示。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738475.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图5. AppClassLoader及ExtClassLoader<br /><br />继续这个例子，文件夹differentversions包含了一个RMI执行引擎，客户端可以提供给执行引擎任何实现了common.TaskIntf接口的任务。子文件夹client1 和 client2包含了类client.TaskImpl有个细微不同的两个版本。两个类的区别在以下几行：<br /><br /><pre class="overflow" title="pre code">    static{<br />        log("client.TaskImpl.class.getClassLoader<br />        (v1) : " + TaskImpl.class.getClassLoader());<br />    }<br /><br />    public void execute(){<br />        log("this = " + this + "; execute(1)");<br />    }</pre><br /><br />在client1和client2里分别有getClassLoader(v1) 与 execute(1)和getClassLoader(v2) 与 execute(2)的的log语句。并且，在开始执行引擎RMI服务器的代码中，我们随意地将client2的任务实现放在类路径的前面。<br /><br /><span style="COLOR: blue">CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;<br />    %CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1<br />%JAVA_HOME%\bin\java server.Server</span><br /><br />如图6，7，8的屏幕截图，在客户端VM，各自的client.TaskImpl类被加载、实例化，并发送到服务端的VM来执行。从服务端的控制台，可以明显看到client.TaskImpl代码只被服务端的VM执行一次，这个单一的代码版本在服务端多次生成了许多实例，并执行任务。<br /><br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738256.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图6. 执行引擎服务器控制台<br /><br />图6显示了服务端的控制台，加载并执行两个不同的客户端的请求，如图７，８所示。需要注意的是，代码只被加载了一次（从静态初始化块的日志中也可以明显看出），但对于客户端的调用这个方法被执行了两次。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120738948.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图７. 执行引擎客户端 1控制台　<br /><br />图７中，客户端VM加载了含有client.TaskImpl.class.getClassLoader(v1)的日志内容的类TaskImpl的代码，并提供给服务端的执行引擎。图8的客户端VM加载了另一个TaskImpl的代码，并发送给服务端。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120739526.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图8. 执行引擎客户端 2控制台　<br /><br />在客户端的VM中，类client.TaskImpl被分别加载，初始化，并发送到服务端执行。图6还揭示了client.TaskImpl的代码只在服务端的VM中加载了一次，但这“唯一的一次”却在服务端创造了许多实例并执行。或许客户端1该不高兴了因为并不是它的client.TaskImpl(v1)的方法调用被服务端执行了，而是其他的一些代码。如何解决这一问题？答案就是实现定制的类加载器。<br /><br /><b><span style="FONT-SIZE: 16px">定制类加载器</span></b><br /><br />要较好地控制类的加载，就要实现定制的类加载器。所有自定义的类加载器都应继承自java.lang.ClassLoader。而且在构造方法中，我们也应该设置父类加载器。然后重写findClass()方法。differentversionspush文件夹包含了一个叫做FileSystemClassLoader的自订制的类加载器。其结构如图9所示。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120739337.jpg" width="285" onload="javascript:ImgLoad(this);" border="0" resized="1" /><br />图9. 定制类加载器关系<br /><br />以下是在common.FileSystemClassLoader实现的主方法：<br /><br />    <pre class="overflow" title="pre code">public byte[] findClassBytes(String className){<br /><br />        try{<br />            String pathName = currentRoot +<br />                File.separatorChar + className.<br />                replace('.', File.separatorChar)<br />                + ".class";<br />            FileInputStream inFile = new<br />                FileInputStream(pathName);<br />            byte[] classBytes = new<br />                byte[inFile.available()];<br />            inFile.read(classBytes);<br />            return classBytes;<br />        }<br />        catch (java.io.IOException ioEx){<br />            return null;<br />        }<br />    }<br /><br />    public Class findClass(String name)throws<br />        ClassNotFoundException{<br /><br />        byte[] classBytes = findClassBytes(name);<br />        if (classBytes==null){<br />            throw new ClassNotFoundException();<br />        }<br />        else{<br />            return defineClass(name, classBytes,<br />                0, classBytes.length);<br />        }<br />    }<br /><br />    public Class findClass(String name, byte[]<br />        classBytes)throws ClassNotFoundException{<br /><br />        if (classBytes==null){<br />            throw new ClassNotFoundException(<br />                "(classBytes==null)");<br />        }<br />        else{<br />            return defineClass(name, classBytes,<br />                0, classBytes.length);<br />        }<br />    }<br /><br />    public void execute(String codeName,<br />        byte[] code){<br /><br />        Class klass = null;<br />        try{<br />            klass = findClass(codeName, code);<br />            TaskIntf task = (TaskIntf)<br />                klass.newInstance();<br />            task.execute();<br />        }<br />        catch(Exception exception){<br />            exception.printStackTrace();<br />        }<br />    }</pre><br /><br />这个类供客户端把client.TaskImpl(v1)转换成字节数组，之后此字节数组被发送到RMI服务端。在服务端，一个同样的类用来把字节数组的内容转换回代码。客户端代码如下：<br /><br /><pre class="overflow" title="pre code">public class Client{<br /><br />    public static void main (String[] args){<br /><br />        try{<br />            byte[] code = getClassDefinition<br />                ("client.TaskImpl");<br />            serverIntf.execute("client.TaskImpl",<br />                code);<br />            }<br />            catch(RemoteException remoteException){<br />                remoteException.printStackTrace();<br />            }<br />        }<br /><br />    private static byte[] getClassDefinition<br />        (String codeName){<br />        String userDir = System.getProperties().<br />            getProperty("BytePath");<br />        FileSystemClassLoader fscl1 = null;<br /><br />        try{<br />            fscl1 = new FileSystemClassLoader<br />                (userDir);<br />        }<br />        catch(FileNotFoundException<br />            fileNotFoundException){<br />            fileNotFoundException.printStackTrace();<br />        }<br />        return fscl1.findClassBytes(codeName);<br />    }<br />}</pre><br /><br />在执行引擎中，从客户端收到的代码被送到定制的类加载器中。定制的类加载器把其从字节数组定义成类，实例化并执行。需要指出的是，对每一个客户请求，我们用类FileSystemClassLoader的不同实例来定义客户端提交的client.TaskImpl。而且，client.TaskImpl并不在服务端的类路径中。这也就意味着当我们在FileSystemClassLoader调用findClass()方法时，findClass()调用内在的defineClass()方法。类client.TaskImpl被特定的类加载器实例所定义。因此，当FileSystemClassLoader的一个新的实例被使用，类又被重新定义为字节数组。因此，对每个客户端请求类client.TaskImpl被多次定义，我们就可以在相同执行引擎JVM中执行不同的client.TaskImpl的代码。<br /><br />    <pre class="overflow" title="pre code">public void execute(String codeName, byte[] code)throws RemoteException{<br /><br />        FileSystemClassLoader fileSystemClassLoader = null;<br /><br />        try{<br />            fileSystemClassLoader = new FileSystemClassLoader();<br />            fileSystemClassLoader.execute(codeName, code);<br />        }<br />        catch(Exception exception){<br />            throw new RemoteException(exception.getMessage());<br />        }<br />    }</pre><p align="left"><br /><br />示例在differentversionspush文件夹下。服务端和客户端的控制台界面分别如图10，11，12所示：<br /><br /></p><p align="center"><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120739441.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /></p><p align="left"><br />                                                            图10. 定制类加载器执行引擎<br /><br />图10显示的是定制的类加载器控制台。我们可以看到client.TaskImpl的代码被多次加载。实际上针对每一个客户端，类都被加载并初始化。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120739837.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />    图11. 定制类加载器，客户端1<br /><br />    图11中，含有client.TaskImpl.class.getClassLoader(v1)的日志记录的类TaskImpl的代码被客户端的VM加载，然后送到服务端。图12 另一个客户端把包含有client.TaskImpl.class.getClassLoader(v1)的类代码加载并送往服务端。<br /><br /><img style="CURSOR: pointer" onclick="javascript:window.open(this.src);" src="http://java.chinaitlab.com/UploadFiles_8734/200512/20051215120740394.jpg" onload="javascript:ImgLoad(this);" border="0" resized="0" /><br />图12. 定制类加载器，客户端1<br /><br />这段代码演示了我们如何利用不同的类加载器实例来在同一个VM上执行不同版本的代码。<br /><br /><b><span style="FONT-SIZE: 16px">J2EE的类加载器</span></b></p><b><span style="FONT-SIZE: 16px"></span></b><p><br />J2EE的服务器倾向于以一定间隔频率，丢弃原有的类并重新载入新的类。在某些情况下会这样执行，而有些情况则不。同样，对于一个web服务器如果要丢弃一个servlet实例，可能是服务器管理员的手动操作，也可能是此实例长时间未相应。当一个JSP页面被首次请求，容器会把此JSP页面翻译成一个具有特定形式的servlet代码。一旦servlet代码被创建，容器就会把这个servlet翻译成class文件等待被使用。对于提交给容器的每次请求，容器都会首先检查这个JSP文件是否刚被修改过。是的话就重新翻译此文件，这可以确保每次的请求都是及时更新的。企业级的部署方案以.ear, .war, .rar等形式的文件，同样需要重复加载，可能是随意的也可能是依照某种配置方案定期执行。对所有的这些情况——类的加载、卸载、重新加载……全部都是建立在我们控制应用服务器的类加载机制的基础上的。实现这些需要扩展的类加载器，它可以执行由其自身所定义的类。Brett Peterson已经在他的文章 Understanding J2EE Application Server Class Loading Architectures给出了J2EE应用服务器的类加载方案的详细说明，详见网站TheServerSide.com。<br /><br /><b><span style="FONT-SIZE: 16px">结要<br /></span></b><br />本文探讨了类载入到虚拟机是如何进行唯一标识的，以及类如果存在同样的类名和包名时所产生的问题。因为没有一个直接可用的类版</p><img src ="http://www.blogjava.net/landy/aggbug/42940.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/landy/" target="_blank">独孤过客</a> 2006-04-25 09:39 <a href="http://www.blogjava.net/landy/articles/42940.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>对象引用是怎样严重影响垃圾收集器的</title><link>http://www.blogjava.net/landy/articles/42936.html</link><dc:creator>独孤过客</dc:creator><author>独孤过客</author><pubDate>Tue, 25 Apr 2006 01:35:00 GMT</pubDate><guid>http://www.blogjava.net/landy/articles/42936.html</guid><wfw:comment>http://www.blogjava.net/landy/comments/42936.html</wfw:comment><comments>http://www.blogjava.net/landy/articles/42936.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.blogjava.net/landy/comments/commentRss/42936.html</wfw:commentRss><trackback:ping>http://www.blogjava.net/landy/services/trackbacks/42936.html</trackback:ping><description><![CDATA[
		<p>　　如果您认为 Java 游戏开发人员是 Java 编程世界的一级方程式赛车手，那么您就会明白为什么他们会如此地重视程序的性能。 游戏开发人员几乎每天都要面对的性能问题，往往超过了一般程序员考虑问题的范围。哪里可以找到这些特殊的开发人员呢？Java 游戏社区就是一个好去处（参见 参考资料）。 虽然在这个站点可能没有很多关于服务器端的应用，但是我们依然可以从中受益，看看这些“惜比特如金”的游戏开发人员每天所面对的，我们往往能从中得到宝贵的经验。让我们开始游戏吧！ </p>
		<p>　　<strong>对象泄漏<br /></strong>　　游戏程序员跟其他程序员一样――他们也需要理解 Java 运行时环境的一些微妙之处，比如垃圾收集。垃圾收集可能是使您感到难于理解的较难的概念之一, 因为它并不能总是毫无遗漏地解决 Java 运行时环境中堆管理的问题。似乎有很多类似这样的讨论，它的开头或结尾写着：“我的问题是关于垃圾收集”。</p>
		<p>　　假如您正面遭遇内存耗尽（out-of-memory）的错误。于是您使用检测工具想要找到问题所在，但这是徒劳的。您很容易想到另外一个比较可信的原因：这是 Java 虚拟机堆管理的问题,而不会认为这是您自己的程序的缘故。但是，正如 Java 游戏社区的资深专家不止一次地解释的，Java 虚拟机并不存在任何被证实的对象泄漏问题。实践证明，垃圾收集器一般能够精确地判断哪些对象可被收集，并且重新收回它们的内存空间给 Java 虚拟机。所以，如果您遇到了内存耗尽的错误，那么这完全可能是由您的程序造成的，也就是说您的程序中存在着“无意识的对象保留（unintentional object retention）”。</p>
		<p>　　<strong>内存泄漏与无意识的对象保留</strong><br />　　内存泄漏和无意识的对象保留的区别是什么呢？对于用 Java 语言编写的程序来说，确实没有区别。两者都是指在您的程序中存在一些对象引用，但实际上您并不需要引用这些对象。一个典型的例子是向一个集合中加入一些对象以便以后使用它们，但是您却忘了在使用完以后从集合中删除这些对象。因为集合可以无限制地扩大，并且从来不会变小，所以当您在集合中加入了太多的对象（或者是有很多的对象被集合中的元素所引用）时，您就会因为堆的空间被填满而导致内存耗尽的错误。垃圾收集器不能收集这些您认为已经用完的对象，因为对于垃圾收集器来说，应用程序仍然可以通过这个集合在任何时候访问这些对象，所以这些对象是不可能被当作垃圾的。</p>
		<p>　　对于没有垃圾收集的语言来说，例如 C++ ，内存泄漏和无意识的对象保留是有区别的。C++ 程序跟 Java 程序一样，可能产生无意识的对象保留。但是 C++ 程序中存在真正的内存泄漏，即应用程序无法访问一些对象以至于被这些对象使用的内存无法释放且返还给系统。令人欣慰的是，在 Java 程序中，这种内存泄漏是不可能出现的。所以，我们更喜欢用“无意识的对象保留”来表示这个令 Java 程序员抓破头皮的内存问题。这样，我们就能区别于其他使用没有垃圾收集语言的程序员。</p>
		<p>　　<strong>跟踪被保留的对象</strong><br />　　那么当发现了无意识的对象保留该怎么办呢？首先，需要确定哪些对象是被无意保留的，并且需要找到究竟是哪些对象在引用它们。然后必须安排好 应该在哪里释放它们。最容易的方法是使用能够对堆产生快照的检测工具来标识这些对象，比较堆的快照中对象的数目，跟踪这些对象，找到引用这些对象的对象，然后强制进行垃圾收集。有了这样一个检测器，接下来的工作相对而言就比较简单了: </p>
		<p>　　等待直到系统达到一个稳定的状态，这个状态下大多数新产生的对象都是暂时的，符合被收集的条件；这种状态一般在程序所有的初始化工作都完成了之后。 <br />　　强制进行一次垃圾收集，并且对此时的堆做一份对象快照。 <br />　　进行任何可以产生无意地保留的对象的操作。 <br />　　再强制进行一次垃圾收集，然后对系统堆中的对象做第二次对象快照。 <br />　　比较两次快照，看看哪些对象的被引用数量比第一次快照时增加了。因为您在快照之前强制进行了垃圾收集，那么剩下的对象都应该是被应用程序所引用的对象，并且通过比较两次快照我们可以准确地找出那些被程序保留的、新产生的对象。 <br />　　根据您对应用程序本身的理解，并且根据对两次快照的比较，判断出哪些对象是被无意保留的。 <br />　　跟踪这些对象的引用链，找出究竟是哪些对象在引用这些无意地保留的对象，直到您找到了那个根对象，它就是产生问题的根源。 </p>
		<p>　　<strong>显式地赋空（nulling）变量</strong><br />　　一谈到垃圾收集这个主题，总会涉及到这样一个吸引人的讨论，即显式地赋空变量是否有助于程序的性能。赋空变量是指简单地将 null 值显式地赋值给这个变量，相对于让该变量的引用失去其作用域。</p>
		<p>清单 1. 局部作用域</p>
		<p>public static String scopingExample(String string) { <br />  StringBuffer sb = new StringBuffer(); <br />  sb.append("hello ").append(string); <br />  sb.append(", nice to see you!"); <br />  return sb.toString(); <br />} </p>
		<p>　　当该方法执行时，运行时栈保留了一个对 StringBuffer 对象的引用，这个对象是在程序的第一行产生的。在这个方法的整个执行期间，栈保存的这个对象引用将会防止该对象被当作垃圾。当这个方法执行完毕，变量 sb 也就失去了它的作用域，相应地运行时栈就会删除对该 StringBuffer 对象的引用。于是不再有对该 StringBuffer 对象的引用，现在它就可以被当作垃圾收集了。栈删除引用的操作就等于在该方法结束时将 null 值赋给变量 sb。</p>
		<p>　　<strong>错误的作用域</strong><br />　　既然 Java 虚拟机可以执行等价于赋空的操作，那么显式地赋空变量还有什么用呢？对于在正确的作用域中的变量来说，显式地赋空变量的确没用。但是让我们来看看另外一个版本的 scopingExample 方法，这一次我们将把变量 sb 放在一个错误的作用域中。</p>
		<p>清单 2. 静态作用域</p>
		<p>static StringBuffer sb = new StringBuffer(); <br />public static String scopingExample(String string) { <br />  sb = new StringBuffer(); <br />  sb.append("hello ").append(string); <br />  sb.append(", nice to see you!"); <br />  return sb.toString(); <br />} </p>
		<p>　　现在 sb 是一个静态变量，所以只要它所在的类还装载在 Java 虚拟机中，它也将一直存在。该方法执行一次，一个新的 StringBuffer 将被创建并且被 sb 变量引用。在这种情况下，sb 变量以前引用的 StringBuffer 对象将会死亡，成为垃圾收集的对象。也就是说，这个死亡的 StringBuffer 对象被程序保留的时间比它实际需要保留的时间长得多――如果再也没有对该 scopingExample 方法的调用，它将会永远保留下去。</p>
		<p>　　<strong>一个有问题的例子<br /></strong>　　即使如此，显式地赋空变量能够提高性能吗？我们会发现我们很难相信一个对象会或多或少对程序的性能产生很大影响，直到我看到了一个在 Java Games 的 Sun 工程师给出的一个例子，这个例子包含了一个不幸的大型对象。</p>
		<p>清单 3. 仍在静态作用域中的对象</p>
		<p>private static Object bigObject;</p>
		<p>public static void test(int size) { <br />  long startTime = System.currentTimeMillis(); <br />  long numObjects = 0; <br />  while (true) { <br />    //bigObject = null; //explicit nulling <br />    //SizableObject could simply be a large array, e.g. byte[] <br />    //In the JavaGaming discussion it was a BufferedImage <br />    bigObject = new SizableObject(size); <br />    long endTime = System.currentTimeMillis(); <br />    ++numObjects; <br />    // We print stats for every two seconds <br />    if (endTime - startTime &gt;= 2000) { <br />      System.out.println("Objects created per 2 seconds = " + numObjects); <br />      startTime = endTime; <br />      numObjects = 0; <br />    } <br />  } <br />} </p>
		<p>　　这个例子有个简单的循环，创建一个大型对象并且将它赋给同一个变量，每隔两秒钟报告一次所创建的对象个数。现在的 Java 虚拟机采用 generational 垃圾收集机制，新的对象创建之后放在一个内存空间（取名 Eden）内，然后将那些在第一次垃圾收集以后仍然保留的对象转移到另外一个内存空间。在 Eden，即创建新对象时所在的新一代空间中，收集对象要比在“老一代”空间中快得多。但是如果 Eden 空间已经满了，没有空间可供分配，那么就必须把 Eden 中的对象转移到老一代空间中，腾出空间来给新创建的对象。如果没有显式地赋空变量，而且所创建的对象足够大，那么 Eden 就会填满，并且垃圾收集器就不能收集当前所引用的这个大型对象。所产生的后果是，这个大型对象被转移到“老一代空间”，并且要花更多的时间来收集它。 </p>
		<p>　　通过显式地赋空变量，Eden 就能在新对象创建之前获得自由空间，这样垃圾收集就会更快。实际上，在显式赋空的情况下，该循环在两秒钟内创建的对象个数是没有显式赋空时的5倍――但是仅当您选择创建的对象要足够大而可以填满 Eden 时才是如此, 在 Windows 环境、Java虚拟机 1.4 的默认配置下大概需要 500KB。那就是一行赋空操作产生的 5 倍的性能差距。但是请注意这个性能差别产生的原因是变量的作用域不正确，这正是赋空操作发挥作用的地方，并且是因为所创建的对象非常大。</p>
		<p>　　<strong>最佳实践</strong><br />　　这是一个有趣的例子，但是值得强调的是，最佳实践是正确地设置变量的作用域，而不要显式地赋空它们。虽然显式赋空变量一般应该没有影响，但总有一些反面的例子证明这样做会对性能产生巨大的负面影响。例如，迭代地或者递归地赋空集合内的元素使得这些集合中的对象能够满足垃圾收集的条件，实际上是增加了系统的开销而不是帮助垃圾收集。请记住这是个有意弄错作用域的例子，其实质是一个无意识的对象保留的例子。</p>
<img src ="http://www.blogjava.net/landy/aggbug/42936.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.blogjava.net/landy/" target="_blank">独孤过客</a> 2006-04-25 09:35 <a href="http://www.blogjava.net/landy/articles/42936.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>