??xml version="1.0" encoding="utf-8" standalone="yes"?>
JAR清单文g
实际上你不需要这样做QJava的类加蝲机制可以更优雅地解决q个问题。一U方案是需要每一个组件的作者在JAR清单中定义内部组件的依赖关系。这里清单是指一个被包含在JAR中的定义文g元数据的文本文gQMETA-INF/MANIFEST.MFQ。最常用的属性是Main-ClassQ定义了通过java –jar方式定位哪个cM被调用。然而,q有一个不那么有名的属性Class-Path可以用来定义他所依赖的其他JAR。Java~省的ClassLoader会检查这些属性ƈ且自动附加这些特定的依赖到classpath中?br />
让我们来看一个例子。考虑一个实C通模拟的Java应用Q他׃个JARl成Q?br />
·simulator-ui.jarQ基于Swing的视图来昄模拟的过E?br /> ·simulator.jar:用来表示模拟状态的数据对象和实现模拟的控制cR?br /> ·rule-engine.jar:常用的第三方规则引擎被用来徏立模拟规则的模型?br /> simulator-ui.jar依赖simulator.jarQ而simulator.jar依赖rule-engine.jar?br />
而通常执行q个应用的方法如下:
$ java -classpath
simulator-ui.jar:simulator.jar:rule-engine.jar
com.oreilly.simulator.ui.Main
~者注Q上面的命o行应该在同一行键入;只是׃|页布局的限制看h好像是多行?br />
但我们也可以在JAR的清单文件中定义q些信息Qsimulator-ui的MANIFEST.MF如下Q?br />
Main-Class: com.oreilly.simulator.ui.Main
Class-Path: simulator.jar
而simulator的MANIFEST.MF包含Q?br /> Class-Path: rule-engine.jar
rule-engine.jar或者没有清单文Ӟ或者清单文件ؓI?br />
现在我们可以q样做:
$ java -jar simulator-ui.jar
Java会自动解析清单的入口来取得主cd修改classpathQ甚臛_以确定simulator-ui.jar的\径和解释所有与q个路径相关的Class-Path属性,所以我们可以简单按照下面的方式之一来做Q?br /> $ java -jar ../simulator-ui.jar
$ java -jar /home/don/build/simulator-ui.jar
依赖冲突
Java的Class-Path属性的实现相对于手工定义整个classpath是一个大的改善。然而,两种方式都有自己的限制。一个重要的限制是你只能加载组件的一个特定版本。这看v来是很显然的因ؓ许多~程环境都有q个限制。但是在大的包含多个W三方依赖的多JAR目中依赖冲H是很常见的?br />
例如Q你正在开发一个通过查询多个搜烦引擎q比较他们的l果的搜索引擎。Google和Amazon的Alexa都支持用SOAP作ؓ通讯机制的网l服务APIQ也都提供了相应的Javacd方便讉Kq些API。让我们假设你的JAR- metasearch.jarQ依赖于google.jar和amazon.jarQ而他们都依赖于公qsoap.jar?br />
现在是没有问题,但如果将来SOAP协议或API发生改变时会怎么样呢Q很可能q两个搜索引擎不会选择同时升。可能在某一天你讉KAmazon旉要SOAP1.x版本而访问Google旉要SOAP2.x版本Q而这两个版本的SOAPq不能在同一个进E空间中共存。在q里Q我们可能包含下面的JAR依赖Q?br />
$ cat metasearch/META-INF/MANIFEST.MF
Main-Class: com.onjava.metasearch.Main
Class-Path: google.jar amazon.jar
$ cat amazon/META-INF/MANIFEST.MF
Class-Path: soap-v1.jar
$ cat google/META-INF/MANIFEST.MF
Class-Path: soap-v2.jar
上面正确地描qC依赖关系Q但q里q没有包含什么魔?-q样讄q不会像我们期望地那样工作。如果soap-v1.jar和soap-v2.jar定义了许多相同的c,我们肯定q是会出问题的?br /> $ java -jar metasearch.jar
SOAP v1: remotely invoking searchAmazon
SOAP v1: remotely invoking searchGoogle
你可以看刎ͼsoap-v1.jar被首先加在classpath中,因此实际上也只有他会被用。上面的例子{h于:
$ java -classpath
metasearch.jar:amazon.jar:google.jar:soap-v1.jar:soap-v2.jar
# WRONG!
~者注Q上面的命o行应该在同一行键入;只是׃|页布局的限制看h好像是多行?br />
有趣的是如果Yahoo也发布了一个网l服务APIQ而他看v来ƈ没有依赖于现有的SOAP/XML-RPCcd。在较小的项目中Q组件依赖冲H常被用来作为在你只要手工包装方案或者只需要一两个cL而不使用让你不用全量组Ӟ如集合类库)的原因之一。手工包装方案有他的用处Q但使用已有的组件是更普遍的方式。而且复制其他lg的类C的代码库永远不是一个好L。实际上你已l与lg的开发生分岐而且没有Z在有问题修复或安全升U时合ƈ他?br />
许多大的目Q如主要的商业组Ӟ已经采用他们用的整个lg构徏C们的JAR内部。ؓ了这么做Q他们改变了包名使其唯一Q如com/acme/foobar/org/freeware/utilityQ,而且直接在他们的JAR中包含类。这样做的好处是可以防止在这些组件中多个版本的冲H,但这也是有代L。这么做对开发h员来说完全隐藏了对第三方的依赖。但如果q种方式大规模的应用Q将会导致效率的降低Q包括JAR文g的大和加蝲多个JAR版本到进E中的效率降低)。这U方式的问题在于如果两个lg依赖于同一个版本的W三方组件时Q就没有协调机制来确定共享的lg只被加蝲一ơ。这个问题我们会在下一节进行研I。除了效率的降低外,很可能你q种l定W三方Y件的方式会与那些软g的许可协议冲H?br />
另一U解册个问题的方式是每一个组件的开发h员显式的在他的包名中~码一个版本号。Sun的javac代码采用这个方式—一个com.sun.tools.javac.MaincM单地转发lcom.sun.tools.javac.v8.Maino。每ơ一个新的Java版本发布Q这个代码的包名改变一ơ。这允怸个组件的多个发布版本可以共存在同一个类加蝲器中q且q得版本的选择是显式的。但q也不是一个非常好的解x案,因ؓ或者客户需要准知道他们计划用的版本而且必须改变他们的代码来转换到新的版本,或者他们必M赖于一个包装类来{发方案调用给最新的版本Q在q种情况下,q些包装cd会承受我们上面提到的相同问题Q?br />
加蝲多个发布版本
q里我们遇到的问题在大多数项目中也存在,所有的c都会被加蝲C个全局命名I间。如果每一个组件有自己的命名空间而且他会加蝲所有他依赖的组件到q个命名I间而不影响q程的其他部分,那又会怎么样呢Q实际上我们可以在Java中这么做Q类名不需要是唯一的,只要cd和其所对应的ClassLoader的组合是唯一的就可以了。这意味着ClassLoadercM于命名空_而如果我们可以加载每一个组件在自己的ClassLoader中,他就可以控制如何满依赖。他可以代理cd位给其他的包含他的依赖组件所需要的特定版本的ClassLoader。如??br />
Figure 1. Decentralized class loaders
然而这个架构ƈ不比l定每一个依赖的JAR在自qJAR中好多少。我们需要的是一个可以确保每一个组件版本仅被一个类加蝲器加载的中央集权。图2中的架构可以定每一个组件版本仅被加载一ơ?br />
Figure 2. Class loaders with mediator
Z实现q种方式Q我们需要创Z个不同类型的cd载器。每一个ComponentClassLoader需要扩展Java的URLClassLoader来提供需要的逻辑来从一个JAR中获?class文g。当然他也会执行两个其他的Q务。在创徏的时候,他会获取JAR清单文gq定位一个新属性Restricted-Class-Path。不像Sun提供的Class-Path属性,q个属性暗C特定的JAR应该只对q个lg有效?br /> public class ComponentClassLoader extends URLClassLoader {
// ... public ComponentClassLoader (MasterClassLoader master, File file)
{
// ... JarFile jar = new JarFile(file);
Manifest man = jar.getManifest();
Attributes attr = man.getMainAttributes();
List l = new ArrayList();
String str = attr.getValue("Restricted-Class-Path");
if (str != null) {
StringTokenizer tok = new StringTokenizer(str);
while (tok.hasMoreTokens()) {
l.add(new File(file.getParentFile(),
tok.nextToken());
}
}
this.dependencies = l;
} public Class loadClass (String name, boolean resolve)
throws ClassNotFoundException {
try {
// Try to load the class from our JAR.
return loadClassForComponent(name, resolve);
} catch (ClassNotFoundException ex) {}
// Couldn't find it -- let the master look for it
// in another components.
return master.loadClassForComponent(name,
resolve, dependencies);
}
public Class loadClassForComponent (String name,
boolean resolve)
throws ClassNotFoundException
{
C
]]>
q篇文章从基讲vQ比如代码与数据的不同之处是什么,他们是如何构成一个实例或对象的。然后深入探讨java虚拟机(JVMQ是如何利用cd载器d代码Q以及java中类加蝲器的主要cd。接着用一个类加蝲的基本算法看一下类加蝲器如何加载一个内部类。本文的下一节演CZD代码来说明扩展和开发属于自qcd载器的必要性。紧接着解释如何使用定制的类加蝲器来完成一个一般意义上的Q务,使其可以加蝲Lq端客户的代码,在JVM中定义,实例化ƈ执行它。本文包括了J2EE关于cd载的规范——事实上q已l成ZJ2EE的标准之一?br />
cM数据
一个类代表要执行的代码Q而数据则表示其相关状态。状态时常改变,而代码则不会。当我们一个特定的状态与一个类相对应v来,也就意味着一个类事例化。尽相同的cd应的实例其状态千差万别,但其本质都对应着同一D代码。在JAVA中,一个类通常有着一?class文gQ但也有例外。在JAVA的运行时环境中(Java runtimeQ,每一个类都有一个以W一c?first-class)的Java对象所表现出现的代码,其是java.lang.Class的实例。我们编译一个JAVA文gQ编译器都会嵌入一个public, static, final修饰的类型ؓjava.lang.ClassQ名UCؓclass的域变量在其字节码文件中。因Z用了public修饰Q我们可以采用如下的形式对其讉KQ?/p>
java.lang.Class klass = Myclass.class;
一旦一个类被蝲入JVM中,同一个类׃会被再次载入了(切记Q同一个类Q。这里存在一个问题就是什么是“同一个类”?正如一个对象有一个具体的状态,x识,一个对象始l和其代?c?相关联。同理,载入JVM的类也有一个具体的标识Q我们接下来看?br />
在JAVA中,一个类用其完全匚wcd(fully qualified class name)作ؓ标识Q这里指的完全匹配类名包括包名和cd。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识。因此,如果一个名为Pg的包中,有一个名为Cl的类Q被cd载器KlassLoader的一个实例kl1加蝲QCl的实例,即C1.class在JVM中表CZؓ(Cl, Pg, kl1)。这意味着两个cd载器的实?Cl, Pg, kl1) ?(Cl, Pg, kl2)是不同的Q被它们所加蝲的类也因此完全不同,互不兼容的。那么在JVM中到底有多少U类加蝲器的实例Q下一节我们揭C答案?br />
cd载器
在JVM中,每一个类都被java.lang.ClassLoader的一些实例来加蝲.cClassLoader是在包中java.lang里,开发者可以自由地l承它ƈd自己的功能来加蝲cR?br />
无论何时我们键入java MyMainClass来开始运行一个新的JVMQ“引导类加蝲?bootstrap class loader)”负责将一些关键的Javac,如java.lang.Object和其他一些运行时代码先加载进内存中。运行时的类在JRE\lib\rt.jar包文件中。因属于pȝ底层执行动作Q我们无法在JAVA文档中找到引导类加蝲器的工作l节。基于同L原因Q引导类加蝲器的行ؓ在各JVM之间也是大相径庭?br />同理Q如果我们按照如下方式:log(java.lang.String.class.getClassLoader());
来获取java的核心运行时cȝ加蝲器,׃得到null?br />
接下来介ljava的扩展类加蝲器。扩展库提供比javaq行代码更多的特性,我们可以把扩展库保存在由java.ext.dirs属性提供的路径中?br />
(~辑注:java.ext.dirs属性指的是pȝ属性下的一个keyQ所有的pȝ属性可以通过System.getProperties()Ҏ获得。在~者的pȝ中,java.ext.dirs的value是?C:\Program Files\Java\jdk1.5.0_04\jre\lib\ext”。下面将要谈到的如java.class.path也同属系l属性的一个key?
cExtClassLoader专门用来加蝲所有java.ext.dirs下的.jar文g。开发者可以通过把自q.jar文g或库文g加入到扩展目录的classpathQ其可以被扩展cd载器d?br />
从开发者的角度Q第三种同样也是最重要的一U类加蝲器是AppClassLoader。这U类加蝲器用来读取所有的对应在java.class.pathpȝ属性的路径下的cR?br />
Sun的java指南中,文章“理解扩展类加蝲”(Understanding Extension Class LoadingQ对以上三个cd载器路径有更详尽的解释,q是其他几个JDK中的cd载器
●java.net.URLClassLoader
●java.security.SecureClassLoader
●java.rmi.server.RMIClassLoader
●sun.applet.AppletClassLoader
java.lang.ThreadQ包含了public ClassLoader getContextClassLoader()ҎQ这一Ҏq回针对一具体U程的上下文环境cd载器。此cd载器qE的创徏者提供,以供此线E中q行的代码在需要加载类或资源时使用。如果此加蝲器未被徏立,~省是其父线E的上下文类加蝲器。原始的cd载器一般由d应用E序的类加蝲器徏立?br />
cd载器如何工作Q?/span>
除了引导cd载器Q所有的cd载器都有一个父cd载器Q不仅如此,所有的cd载器也都是java.lang.ClassLoadercd。以上两U类加蝲器是不同的,而且对于开发者自订制的类加蝲器的正常q行也至关重要。最重要的方面是正确讄父类加蝲器。Q何类加蝲器,其父cd载器是加载该cd载器的类加蝲器实例。(CQ类加蝲器本w也是一个类Q)
使用loadClass()Ҏ可以从类加蝲器中获得该类。我们可以通过java.lang.ClassLoader的源代码来了解该Ҏ工作的细节,如下Q?br />protected synchronized Class<?> loadClass
(String name, boolean resolve)
throws ClassNotFoundException{
// First check if the class is already loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke
// findClass to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
我们可以使用ClassLoader的两U构造方法来讄父类加蝲器:public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(MyClassLoader.class.getClassLoader());
}
}
?br />public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(getClass().getClassLoader());
}
}
W一U方式较为常用,因ؓ通常不徏议在构造方法里调用getClass()ҎQ因为对象的初始化只是在构造方法的出口处才完全完成。因此,如果父类加蝲器被正确建立Q当要示从一个类加蝲器的实例获得一个类Ӟ如果它不能找到这个类Q它应该首先去访问其父类。如果父cM能找到它(卛_父类也不能找不这个类Q等{?Q而且如果findBootstrapClass0()Ҏ也失败了Q则调用findClass()Ҏ。findClass()Ҏ的缺省实C抛出ClassNotFoundExceptionQ当它们l承java.lang.ClassLoader来订制类加蝲器时开发者需要实现这个方法。findClass()的缺省实现方式如下: protected Class<?> findClass(String name)
throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
在findClass()Ҏ内部Q类加蝲器需要获取Q意来源的字节码。来源可以是文gpȝQURLQ数据库Q可以生字节码的另一个应用程序,及其他类似的可以产生java规范的字节码的来源。你甚至可以使用BCEL (Byte Code Engineering LibraryQ字节码工程?Q它提供了运行时创徏cȝ捷径。BCEL已经被成功地使用在以下方面:~译器,优化器,h器,代码产生器及其他分析工具。一旦字节码被检索,此方法就会调用defineClass()ҎQ此行ؓ对不同的cd载实例是有差异的。因此,如果两个cd载实例从同一个来源定义一个类Q所定义的结果是不同的?br />
JAVA语言规范QJava language specificationQ详l解释了JAVA执行引擎中的cL接口的加载(loadingQ,链接QlinkingQ或初始化(initializationQ过E?br />
图一昄了一个主cȝ为MyMainClass的应用程序。依照之前的阐述QMyMainClass.class会被AppClassLoader加蝲?MyMainClass创徏了两个类加蝲器的实例QCustomClassLoader1 ?CustomClassLoader2,他们可以从某数据源(比如|络Q获取名为Target的字节码。这表示cTarget的类定义不在应用E序c\径或扩展c\径。在q种情况下,如果MyMainClass惌用自定义的类加蝲器加载Targetc,CustomClassLoader1和CustomClassLoader2会分别独立地加蝲q定义Target.classcR这在java中有重要的意义。如果TargetcL一些静态的初始化代码,q且假设我们只希望这些代码在JVM中只执行一ơ,而这些代码在我们目前的步骤中会执行两ơ——分别被不同的CustomClassLoaders加蝲q执行。如果类Target被两个CustomClassLoaders加蝲q创Z个实例Target1和Target2Q如图一昄Q它们不是类型兼容的。换句话_在JVM中无法执行以下代码:Target target3 = (Target) target2;
以上代码会抛Z个ClassCastException。这是因为JVM把他们视为分别不同的c,因ؓ他们被不同的cd载器所定义。这U情况当我们不是使用两个不同的类加蝲器CustomClassLoader1 ?CustomClassLoader2Q而是使用同一个类加蝲器CustomClassLoader的不同实例时Q也会出现同L错误。这些会在本文后边用具体代码说明?br />
?. 在同一个JVM中多个类加蝲器加载同一个目标类
关于cd载、定义和链接的更多解释,请参考Andreas Schaefer?Inside Class Loaders."
Z么我们需要我们自qcd载器
原因之一为开发者写自己的类加蝲器来控制JVM中的cd载行为,java中的c靠其包名和cd来标识,对于实现了java.io.Serializable接口的类QserialVersionUID扮演了一个标识类版本的重要角艌Ӏ这个唯一标识是一个类名、接口名、成员方法及属性等l成的一?4位的哈希字段Q而且也没有其他快L方式来标识一个类的版本。严D来,如果以上的都匚wQ那么则属于同一个类?br />
但是让我们思考如下情况:我们需要开发一个通用的执行引擎。可以执行实现某一特定接口的Q何Q务。当d被提交到q个引擎Q首先需要加载这个Q务的代码。假设不同的客户Ҏ引擎提交了不同的dQ凑巧,q些所有的d都有一个相同的cd和包名。现在面临的问题是q个引擎是否可以针对不同的用h提交的信息而做Z同的反应。这一情况在下文的参考一节有可供下蝲的代码样例,samepath ?differentversionsQ这两个目录分别演示了这一概念?br />
? 昄了文件目录结构,有三个子目录samepath, differentversions, ?differentversionspushQ里Ҏ例子Q?br />
?. 文g夹结构组l示?br />
在samepath 中,cversion.Version保存在v1和v2两个子目录里Q两个类h同样的类名和包名Q唯一不同的是下边q行Q?br /> public void fx(){
log("this = " + this + "; Version.fx(1).");
}
V1中,日志记录中有Version.fx(1)Q而在v2中则是Version.fx(2)。把q个两个存在l微不同的类攑֜一个classpath下,然后q行Testc:
set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2
%JAVA_HOME%\bin\java Test
?昄了控制台输出。我们可以看到对应着Version.fx(1)的代码被执行了,因ؓcd载器在classpath首先看到此版本的代码?br />
?. 在类路径中samepath试排在最前面的version 1
再次q行Q类路径做如下微改动?
set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1
%JAVA_HOME%\bin\java Test
控制台的输出变ؓ?。对应着Version.fx(2)的代码被加蝲Q因为类加蝲器在classpath中首先找到它的\径?br />
?. 在类路径中samepath试排在最前面的version 2
Ҏ以上例子可以很明昑֜看出Q类加蝲器加载在c\径中被首先找到的元素。如果我们在v1和v2中删除了version.VersionQ做一个非version.Version形式?jar文gQ如myextension.jarQ把它放到对应java.ext.dirs的\径下Q再ơ执行后看到version.Version不再被AppClassLoader加蝲Q而是被扩展类加蝲器加载。如?所C?br />
?. AppClassLoader及ExtClassLoader
l箋q个例子Q文件夹differentversions包含了一个RMI执行引擎Q客L可以提供l执行引擎Q何实Ccommon.TaskIntf接口的Q务。子文g夹client1 ?client2包含了类client.TaskImpl有个l微不同的两个版本。两个类的区别在以下几行Q?br /> static{
log("client.TaskImpl.class.getClassLoader
(v1) : " + TaskImpl.class.getClassLoader());
}
public void execute(){
log("this = " + this + "; execute(1)");
}
在client1和client2里分别有getClassLoader(v1) ?execute(1)和getClassLoader(v2) ?execute(2)的的log语句。ƈ且,在开始执行引擎RMI服务器的代码中,我们随意地将client2的Q务实现放在类路径的前面?br />
CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;
%CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1
%JAVA_HOME%\bin\java server.Server
如图6Q?Q?的屏q截图,在客LVMQ各自的client.TaskImplc被加蝲、实例化Qƈ发送到服务端的VM来执行。从服务端的控制収ͼ可以明显看到client.TaskImpl代码只被服务端的VM执行一ơ,q个单一的代码版本在服务端多ơ生成了许多实例Qƈ执行d?br />
?. 执行引擎服务器控制台
?昄了服务端的控制台Q加载ƈ执行两个不同的客L的请求,如图Q,Q所C。需要注意的是,代码只被加蝲了一ơ(从静态初始化块的日志中也可以明显看出Q,但对于客L的调用这个方法被执行了两ơ?br />
图7. 执行引擎客户?1控制台
图7中,客户端VM加蝲了含有client.TaskImpl.class.getClassLoader(v1)的日志内容的cTaskImpl的代码,q提供给服务端的执行引擎。图8的客LVM加蝲了另一个TaskImpl的代码,q发送给服务端?br />
?. 执行引擎客户?2控制台
在客L的VM中,cclient.TaskImpl被分别加载,初始化,q发送到服务端执行。图6q揭CZclient.TaskImpl的代码只在服务端的VM中加载了一ơ,但这“唯一的一ơ”却在服务端创造了许多实例q执行。或许客L1该不高兴了因为ƈ不是它的client.TaskImpl(v1)的方法调用被服务端执行了Q而是其他的一些代码。如何解册一问题Q答案就是实现定制的cd载器?br />
定制cd载器
要较好地控制cȝ加蝲Q就要实现定制的cd载器。所有自定义的类加蝲器都应承自java.lang.ClassLoader。而且在构造方法中Q我们也应该讄父类加蝲器。然后重写findClass()Ҏ。differentversionspush文g夹包含了一个叫做FileSystemClassLoader的自订制的类加蝲器。其l构如图9所C?br />
?. 定制cd载器关系
以下是在common.FileSystemClassLoader实现的主ҎQ?br />
public byte[] findClassBytes(String className){
try{
String pathName = currentRoot +
File.separatorChar + className.
replace('.', File.separatorChar)
+ ".class";
FileInputStream inFile = new
FileInputStream(pathName);
byte[] classBytes = new
byte[inFile.available()];
inFile.read(classBytes);
return classBytes;
}
catch (java.io.IOException ioEx){
return null;
}
}
public Class findClass(String name)throws
ClassNotFoundException{
byte[] classBytes = findClassBytes(name);
if (classBytes==null){
throw new ClassNotFoundException();
}
else{
return defineClass(name, classBytes,
0, classBytes.length);
}
}
public Class findClass(String name, byte[]
classBytes)throws ClassNotFoundException{
if (classBytes==null){
throw new ClassNotFoundException(
"(classBytes==null)");
}
else{
return defineClass(name, classBytes,
0, classBytes.length);
}
}
public void execute(String codeName,
byte[] code){
Class klass = null;
try{
klass = findClass(codeName, code);
TaskIntf task = (TaskIntf)
klass.newInstance();
task.execute();
}
catch(Exception exception){
exception.printStackTrace();
}
}
q个cM客户端把client.TaskImpl(v1)转换成字节数l,之后此字节数l被发送到RMI服务端。在服务端,一个同Lcȝ来把字节数组的内容{换回代码。客L代码如下Q?br />public class Client{
public static void main (String[] args){
try{
byte[] code = getClassDefinition
("client.TaskImpl");
serverIntf.execute("client.TaskImpl",
code);
}
catch(RemoteException remoteException){
remoteException.printStackTrace();
}
}
private static byte[] getClassDefinition
(String codeName){
String userDir = System.getProperties().
getProperty("BytePath");
FileSystemClassLoader fscl1 = null;
try{
fscl1 = new FileSystemClassLoader
(userDir);
}
catch(FileNotFoundException
fileNotFoundException){
fileNotFoundException.printStackTrace();
}
return fscl1.findClassBytes(codeName);
}
}
在执行引擎中Q从客户端收到的代码被送到定制的类加蝲器中。定制的cd载器把其从字节数l定义成c,实例化ƈ执行。需要指出的是,Ҏ一个客戯求,我们用类FileSystemClassLoader的不同实例来定义客户端提交的client.TaskImpl。而且Qclient.TaskImplq不在服务端的类路径中。这也就意味着当我们在FileSystemClassLoader调用findClass()ҎӞfindClass()调用内在的defineClass()Ҏ。类client.TaskImpl被特定的cd载器实例所定义。因此,当FileSystemClassLoader的一个新的实例被使用Q类又被重新定义为字节数l。因此,Ҏ个客Lhcclient.TaskImpl被多ơ定义,我们可以在相同执行引擎JVM中执行不同的client.TaskImpl的代码?br />
public void execute(String codeName, byte[] code)throws RemoteException{
FileSystemClassLoader fileSystemClassLoader = null;
try{
fileSystemClassLoader = new FileSystemClassLoader();
fileSystemClassLoader.execute(codeName, code);
}
catch(Exception exception){
throw new RemoteException(exception.getMessage());
}
}
CZ在differentversionspush文g夹下。服务端和客L的控制台界面分别如图10Q?1Q?2所C:
?0. 定制cd载器执行引擎
?0昄的是定制的类加蝲器控制台。我们可以看到client.TaskImpl的代码被多次加蝲。实际上针对每一个客LQ类都被加蝲q初始化?br />
?1. 定制cd载器Q客L1
?1中,含有client.TaskImpl.class.getClassLoader(v1)的日志记录的cTaskImpl的代码被客户端的VM加蝲Q然后送到服务端。图12 另一个客L把包含有client.TaskImpl.class.getClassLoader(v1)的类代码加蝲q往服务端?br />
?2. 定制cd载器Q客L1
q段代码演示了我们如何利用不同的cd载器实例来在同一个VM上执行不同版本的代码?br />
J2EE的类加蝲?/span>
J2EE的服务器們于以一定间隔频率,丢弃原有的类q新蝲入新的类。在某些情况下会q样执行Q而有些情况则不。同P对于一个web服务器如果要丢弃一个servlet实例Q可能是服务器管理员的手动操作,也可能是此实例长旉未相应。当一个JSP面被首ơ请求,容器会把此JSP面译成一个具有特定Ş式的servlet代码。一旦servlet代码被创建,容器׃把这个servlet译成class文g{待被用。对于提交给容器的每ơ请求,容器都会首先查这个JSP文g是否刚被修改q。是的话重新翻译此文gQ这可以保每次的请求都是及时更新的。企业的部|方案以.ear, .war, .rar{Ş式的文gQ同样需要重复加载,可能是随意的也可能是依照某种配置Ҏ定期执行。对所有的q些情况——类的加载、卸载、重新加载……全部都是徏立在我们控制应用服务器的cd载机制的基础上的。实现这些需要扩展的cd载器Q它可以执行由其自n所定义的类。Brett Peterson已经在他的文?Understanding J2EE Application Server Class Loading Architecturesl出了J2EE应用服务器的cd载方案的详细说明Q详见网站TheServerSide.com?br />
l要
本文探讨了类载入到虚拟机是如何进行唯一标识的,以及cd果存在同Lcd和包名时所产生的问题。因为没有一个直接可用的cȝ
]]>
对象泄漏
游戏E序员跟其他E序员一样――他们也需要理?Java q行时环境的一些微妙之处,比如垃圾攉。垃圾收集可能是使您感到难于理解的较隄概念之一, 因ؓ它ƈ不能L毫无遗漏地解?Java q行时环境中堆管理的问题。似乎有很多cMq样的讨论,它的开头或l尾写着Q“我的问题是关于垃圾攉”?/p>
假如您正面遭遇内存耗尽Qout-of-memoryQ的错误。于是您使用工h要找到问题所在,但这是徒劳的。您很容易想到另外一个比较可信的原因Q这?Java 虚拟机堆理的问?而不会认是您自己的程序的~故。但是,正如 Java 游戏C的资׃家不止一ơ地解释的,Java 虚拟机ƈ不存在Q何被证实的对象泄漏问题。实践证明,垃圾攉器一般能够精地判断哪些对象可被攉Qƈ且重新收回它们的内存I间l?Java 虚拟机。所以,如果您遇C内存耗尽的错误,那么q完全可能是由您的程序造成的,也就是说您的E序中存在着“无意识的对象保留(unintentional object retentionQ”?/p>
内存泄漏与无意识的对象保?/strong> 对于没有垃圾攉的语a来说Q例?C++ Q内存泄漏和无意识的对象保留是有区别的。C++ E序?Java E序一P可能产生无意识的对象保留。但?C++ E序中存在真正的内存泄漏Q即应用E序无法讉K一些对象以至于被这些对象用的内存无法释放且返q给pȝ。o人欣慰的是,?Java E序中,q种内存泄漏是不可能出现的。所以,我们更喜Ƣ用“无意识的对象保留”来表示q个?Java E序员抓破头皮的内存问题。这P我们p区别于其他用没有垃圾收集语a的程序员?/p>
跟踪被保留的对象 {待直到pȝ辑ֈ一个稳定的状态,q个状态下大多数新产生的对象都是暂时的Q符合被攉的条Ӟq种状态一般在E序所有的初始化工作都完成了之后? 昑ּ地赋I(nullingQ变?/strong> 清单 1. 局部作用域 public static String scopingExample(String string) { 当该Ҏ执行Ӟq行时栈保留了一个对 StringBuffer 对象的引用,q个对象是在E序的第一行生的。在q个Ҏ的整个执行期_栈保存的q个对象引用会防止该对象被当作垃圾。当q个Ҏ执行完毕Q变?sb 也就失去了它的作用域Q相应地q行时栈׃删除对该 StringBuffer 对象的引用。于是不再有对该 StringBuffer 对象的引用,现在它就可以被当作垃圾收集了。栈删除引用的操作就{于在该Ҏl束时将 null Dl变?sb?/p>
错误的作用域 清单 2. 静态作用域 static StringBuffer sb = new StringBuffer(); 现在 sb 是一个静态变量,所以只要它所在的c还装蝲?Java 虚拟ZQ它也将一直存在。该Ҏ执行一ơ,一个新?StringBuffer 被创徏q且?sb 变量引用。在q种情况下,sb 变量以前引用?StringBuffer 对象会MQ成为垃圾收集的对象。也是_q个M?StringBuffer 对象被程序保留的旉比它实际需要保留的旉长得多――如果再也没有对?scopingExample Ҏ的调用,它将会永q保留下厅R?/p>
一个有问题的例?br /> 即如此Q显式地赋空变量能够提高性能吗?我们会发现我们很隄信一个对象会或多或少对程序的性能产生很大影响Q直到我看到了一个在 Java Games ?Sun 工程师给出的一个例子,q个例子包含了一个不q的大型对象?/p>
清单 3. 仍在静态作用域中的对象 private static Object bigObject; public static void test(int size) { q个例子有个单的循环Q创Z个大型对象ƈ且将它赋l同一个变量,每隔两秒钟报告一ơ所创徏的对象个数。现在的 Java 虚拟机采?generational 垃圾攉机制Q新的对象创Z后放在一个内存空_取名 EdenQ内Q然后将那些在第一ơ垃圾收集以后仍然保留的对象转移到另外一个内存空间。在 EdenQ即创徏新对象时所在的C代空间中Q收集对象要比在“老一代”空间中快得多。但是如?Eden I间已经满了Q没有空间可供分配,那么必L Eden 中的对象转移到老一代空间中Q腾出空间来l新创徏的对象。如果没有显式地赋空变量Q而且所创徏的对象够大Q那?Eden ׃填满Qƈ且垃圾收集器׃能收集当前所引用的这个大型对象。所产生的后果是Q这个大型对象被转移到“老一代空间”,q且要花更多的时间来攉它? 通过昑ּ地赋I变量,Eden p在新对象创徏之前获得自由I间Q这样垃圾收集就会更快。实际上Q在昑ּ赋空的情况下Q该循环在两U钟内创建的对象个数是没有显式赋I时?倍――但是仅当您选择创徏的对象要_大而可以填?Eden 时才是如? ?Windows 环境、Java虚拟?1.4 的默认配|下大概需?500KB。那是一行赋I操作生的 5 倍的性能差距。但是请注意q个性能差别产生的原因是变量的作用域不正,q正是赋I操作发挥作用的地方Qƈ且是因ؓ所创徏的对象非常大?/p>
最佛_?/strong>
内存泄漏和无意识的对象保留的区别是什么呢Q对于用 Java 语言~写的程序来_实没有区别。两者都是指在您的程序中存在一些对象引用,但实际上您ƈ不需要引用这些对象。一个典型的例子是向一个集合中加入一些对象以便以后用它们,但是您却忘了在用完以后从集合中删除q些对象。因为集合可以无限制地扩大,q且从来不会变小Q所以当您在集合中加入了太多的对象(或者是有很多的对象被集合中的元素所引用Q时Q您׃因ؓ堆的I间被填满而导致内存耗尽的错误。垃圾收集器不能攉q些您认为已l用完的对象Q因为对于垃圾收集器来说Q应用程序仍然可以通过q个集合在Q何时候访问这些对象,所以这些对象是不可能被当作垃圾的?/p>
那么当发C无意识的对象保留该怎么办呢Q首先,需要确定哪些对象是被无意保留的Qƈ且需要找到究竟是哪些对象在引用它们。然后必d排好 应该在哪里释攑֮们。最Ҏ的方法是使用能够对堆产生快照的检工h标识q些对象Q比较堆的快照中对象的数目,跟踪q些对象Q找到引用这些对象的对象Q然后强制进行垃圾收集。有了这样一个检器Q接下来的工作相对而言比较简单了:
强制q行一ơ垃圾收集,q且Ҏ时的堆做一份对象快照?
q行M可以产生无意C留的对象的操作?
再强制进行一ơ垃圾收集,然后对系l堆中的对象做第二次对象快照?
比较两次快照Q看看哪些对象的被引用数量比W一ơ快照时增加了。因为您在快照之前强制进行了垃圾攉Q那么剩下的对象都应该是被应用程序所引用的对象,q且通过比较两次快照我们可以准确地找出那些被E序保留的、新产生的对象?
Ҏ您对应用E序本n的理解,q且Ҏ对两ơ快照的比较Q判断出哪些对象是被无意保留的?
跟踪q些对象的引用链Q找出究竟是哪些对象在引用这些无意地保留的对象,直到您找C那个根对象,它就是生问题的Ҏ?
一谈到垃圾攉q个主题QM涉及到这样一个吸引h的讨论,x式地赋空变量是否有助于程序的性能。赋I变量是指简单地?null 值显式地赋值给q个变量Q相对于让该变量的引用失d作用域?/p>
StringBuffer sb = new StringBuffer();
sb.append("hello ").append(string);
sb.append(", nice to see you!");
return sb.toString();
}
既然 Java 虚拟机可以执行等价于赋空的操作,那么昑ּ地赋I变量还有什么用呢?对于在正的作用域中的变量来_昑ּ地赋I变量的没用。但是让我们来看看另外一个版本的 scopingExample ҎQ这一ơ我们将把变?sb 攑֜一个错误的作用域中?/p>
public static String scopingExample(String string) {
sb = new StringBuffer();
sb.append("hello ").append(string);
sb.append(", nice to see you!");
return sb.toString();
}
long startTime = System.currentTimeMillis();
long numObjects = 0;
while (true) {
//bigObject = null; //explicit nulling
//SizableObject could simply be a large array, e.g. byte[]
//In the JavaGaming discussion it was a BufferedImage
bigObject = new SizableObject(size);
long endTime = System.currentTimeMillis();
++numObjects;
// We print stats for every two seconds
if (endTime - startTime >= 2000) {
System.out.println("Objects created per 2 seconds = " + numObjects);
startTime = endTime;
numObjects = 0;
}
}
}
q是一个有的例子Q但是值得的是Q最佛_跉|正确地设|变量的作用域,而不要显式地赋空它们。虽然显式赋I变量一般应该没有媄响,但L一些反面的例子证明q样做会Ҏ能产生巨大的负面媄响。例如,q代地或者递归地赋I集合内的元素得这些集合中的对象能够满_圾收集的条gQ实际上是增加了pȝ的开销而不是帮助垃圾收集。请Cq是个有意弄错作用域的例子,其实质是一个无意识的对象保留的例子?/p>
]]>
վ֩ģ壺
һƬѲ|
õ788Ƶ|
ۺAVһҳ|
ŷպۺϾþþþ|
ѹۿëƬȫ|
¶ۺƵ|
freežž|
AVۺɫ͵|
Դ̼ƬƵ|
ĻһԾ|
˳Ƶۿ|
˾Ʒձ|
aƬav|
ԻƵ߿Ƭ|
ӰԺ߹ۿ|
ɫһ վ|
Ʒmv߹ۿ|
ŮƵaƵȫվɫ|
ɫŮһ˿|
ѹۿþþƵ|
ƷƵ߹ۿ|
߹ۿav|
鵺̳߹ۿ
|
AVպAVþ|
պ߹ۿƵ|
˳Ļ|
ѶëƬƵ|
Ůһһ鴤Ƶ|
˾Ʒþ|
ˬAëƬѿ|
˾þùѹۿƵ|
ͼƬһ|
ҳƵ߹ۿ
|
ҹҹƵ|
˴WWW|
ɫվwwwһ|
߿Ƭ˳Ƶ|
Ʒ鶹ר|
ĻӰӾƷ|
ѹ˦Ƭ|
͵Ʒ|