JVM ClassLoader 介绍

与C或C++编写的程序不同,Java程序并不是一个可执行文件,而是由许多独立的类文件组成,每一个文件对应于一个Java类。

此外,这些类文件并非立即全部都装入内存,而是根据程序需要装入内存。ClassLoader是JVM中将类装入内存的那部分。

而且,Java ClassLoader就是用Java语言编写的。这意味着创建您自己的ClassLoader非常容易,不必了解JVM的微小细节。

[b]为什么编写ClassLoader?[/b]

如果JVM已经有一个ClassLoader,那么为什么还要编写另一个呢?问得好。缺省的ClassLoader只知道如何从本地文件系统装入类文件。不过这只适合于常规情况,即已全部编译完Java程序,并且计算机处于等待状态。

但Java语言最具新意的事就是JVM可以非常容易地从那些非本地硬盘或从网络上获取类。例如,浏览者可以使用定制的ClassLoader从Web站点装入可执行内容。

有许多其它方式可以获取类文件。除了简单地从本地或网络装入文件以外,可以使用定制的ClassLoader完成以下任务:

◆在执行非置信代码之前,自动验证数字签名;
◆使用用户提供的密码透明地解密代码;
◆动态地创建符合用户特定需要的定制化构建类;
◆任何您认为可以生成Java字节码的内容都可以集成到应用程序中。

[b]定制ClassLoader示例[/b]

如果使用过JDK或任何基于Java浏览器中的Applet查看器,那么您差不多肯定使用过定制的ClassLoader。

Sun最初发布Java语言时,其中最令人兴奋的一件事是观看这项新技术是如何执行在运行时从远程的Web服务器装入的代码。(此外,还有更令人兴奋的事--Java技术提供了一种便于编写代码的强大语言。)更一些令人激动的是它可以执行从远程Web服务器通过HTTP连接发送过来的字节码。

此项功能归功于Java语言可以安装定制ClassLoader。Applet查看器包含一个ClassLoader,它不在本地文件系统中寻找类,而是访问远程服务器上的Web站点,经过HTTP装入原始的字节码文件,并把它们转换成JVM内的类。

浏览器和Applet查看器中的ClassLoaders还可以做其它事情:它们支持安全性以及使不同的Applet在不同的页面上运行而互不干扰。

LukeGorrie编写的Echidna是一个开放源码包,它可以使您在单个虚拟机上运行多个Java应用程序。它使用定制的ClassLoader,通过向每个应用程序提供该类文件的自身副本,以防止应用程序互相干扰。

[b]我们的ClassLoader示例[/b]

了解了ClassLoader如何工作以及如何编写ClassLoader之后,我们将创建称作CompilingClassLoader(CCL)的Classloader。CCL为我们编译Java代码,而无需要我们干涉这个过程。它基本上就类似于直接构建到运行时系统中的“make”程序。

注:进一步了解之前,应注意在JDK版本1.2中已改进了ClassLoader系统的某些方面(即Java2平台)。本教程是按JDK版本1.0和1.1写的,但也可以在以后的版本中运行。

Java2中ClassLoader的变动描述了Java版本1.2中的变动,并提供了一些详细信息,以便修改ClassLoader来利用这些变动。

ClassLoader的基本目标是对类的请求提供服务。当JVM需要使用类时,它根据名称向ClassLoader请求这个类,然后ClassLoader试图返回一个表示这个类的Class对象。通过覆盖对应于这个过程不同阶段的方法,可以创建定制的ClassLoader。

在本文的其余部分,您会学习Java ClassLoader的关键方法。您将了解每一个方法的作用以及它是如何适合装入类文件这个过程的。您也会知道,创建自己的ClassLoader时,需要编写什么代码。

在下文中,您将会利用这些知识来使用我们的ClassLoader示例--CompilingClassLoader。

[b]方法loadClass[/b]

ClassLoader.loadClass()是ClassLoader的入口点。其特征如下:

Class loadClass(String name, boolean resolve);

name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Foo或java.lang.Object。

resolve参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。并不总是需要解析。如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。

在Java版本1.1和以前的版本中,loadClass方法是创建定制的ClassLoader时唯一需要覆盖的方法。(Java 2中ClassLoader的变动提供了关于Java 1.2中findClass()方法的信息。)

[b]方法defineClass[/b]

defineClass方法是ClassLoader的主要诀窍。该方法接受由原始字节组成的数组并把它转换成Class对象。原始数组包含如从文件系统或网络装入的数据。

defineClass管理JVM的许多复杂、神秘和倚赖于实现的方面--它把字节码分析成运行时数据结构、校验有效性等等。不必担心,您无需亲自编写它。事实上,即使您想要这么做也不能覆盖它,因为它已被标记成最终的。

[b]方法findSystemClass[/b]

findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将原始字节转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM正常装入类的缺省机制。(Java 2中ClassLoader的变动提供了关于Java版本1.2这个过程变动的详细信息。)

对于定制的ClassLoader,只有在尝试其它方法装入类之后,再使用findSystemClass。原因很简单:ClassLoader是负责执行装入类的特殊步骤,不是负责所有类。例如,即使ClassLoader从远程的Web站点装入了某些类,仍然需要在本地机器上装入大量的基本Java库。而这些类不是我们所关心的,所以要JVM以缺省方式装入它们:从本地文件系统。这就是findSystemClass的用途。

其工作流程如下:

1、请求定制的ClassLoader装入类。
2、检查远程Web站点,查看是否有所需要的类。
3、如果有,那么好;抓取这个类,完成任务。
4、如果没有,假定这个类是在基本Java库中,那么调用findSystemClass,使它从文件系统装入该类。

在大多数定制ClassLoaders中,首先调用findSystemClass以节省在本地就可以装入的许多Java库类而要在远程Web站点上查找所花的时间。然而,正如,在下一章节所看到的,直到确信能自动编译我们的应用程序代码时,才让JVM从本地文件系统装入类。

[b]方法resolveClass[/b]

正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的loadClass时,可以调用resolveClass,这取决于loadClass的resolve参数的值。

[b]方法findLoadedClass[/b]

findLoadedClass充当一个缓存:当请求loadClass装入类时,它调用该方法来查看ClassLoader是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。

[b]组装[/b]

让我们看一下如何组装所有方法。

我们的loadClass实现示例执行以下步骤。(这里,我们没有指定生成类文件是采用了哪种技术--它可以是从Net上装入、或者从归档文件中提取、或者实时编译。无论是哪一种,那是种特殊的神奇方式,使我们获得了原始类文件字节。)

[b]CCL揭密[/b]

我们的ClassLoader(CCL)的任务是确保代码被编译和更新。

下面描述了它的工作方式:

1、当请求一个类时,先查看它是否在磁盘的当前目录或相应的子目录。
2、如果该类不存在,但源码中有,那么调用Java编译器来生成类文件。
3、如果该类已存在,检查它是否比源码旧。如果是,调用Java编译器来重新生成类文件。
4、如果编译失败,或者由于其它原因不能从现有的源码中生成类文件,返回ClassNotFoundException。
5、如果仍然没有该类,也许它在其它库中,所以调用findSystemClass来寻找该类。
6、如果还是没有,则返回ClassNotFoundException。
7、否则,返回该类。
8、调用findLoadedClass来查看是否存在已装入的类。
9、如果没有,那么采用那种特殊的神奇方式来获取原始字节。
10、如果已有原始字节,调用defineClass将它们转换成Class对象。
11、如果没有原始字节,然后调用findSystemClass查看是否从本地文件系统获取类。
12、如果resolve参数是true,那么调用resolveClass解析Class对象。
13、如果还没有类,返回ClassNotFoundException。
14、否则,将类返回给调用程序。

[b]Java编译的工作方式[/b]

在深入讨论之前,应该先退一步,讨论Java编译。通常,Java编译器不只是编译您要求它编译的类。

它还会编译其它类,如果这些类是您要求编译的类所需要的类。

CCL逐个编译应用程序中的需要编译的每一个类。但一般来说,在编译器编译完第一个类后,CCL会查找所有需要编译的类,然后编译它。为什么?Java编译器类似于我们正在使用的规则:如果类不存在,或者与它的源码相比,它比较旧,那么它需要编译。其实,Java编译器在CCL之前的一个步骤,它会做大部分的工作。

当CCL编译它们时,会报告它正在编译哪个应用程序上的类。在大多数的情况下,CCL会在程序中的主类上调用编译器,它会做完所有要做的--编译器的单一调用已足够了。

然而,有一种情形,在第一步时不会编译某些类。如果使用Class.forName方法,通过名称来装入类,Java编译器会不知道这个类时所需要的。在这种情况下,您会看到CCL再次运行Java编译器来编译这个类。在源代码中演示了这个过程。

[b]使用CompilationClassLoader[/b]

要使用CCL,必须以特殊方式调用程序。不能直接运行该程序,如:% java Foo arg1 arg2;

应以下列方式运行它:


% java CCLRun Foo arg1 arg2


CCLRun是一个特殊的存根程序,它创建CompilingClassLoader并用它来装入程序的主类,以确保通过CompilingClassLoader来装入整个程序。CCLRun使用JavaReflectionAPI 来调用特定类的主方法并把参数传递给它。有关详细信息,请参阅源代码。

[b]运行示例[/b]

源码包括了一组小类,它们演示了工作方式。主程序是Foo类,它创建类Bar的实例。类Bar创建另一个类Baz的实例,它在baz包内,这是为了展示CCL是如何处理子包里的代码。Bar也是通过名称装入的,其名称为Boo,这用来展示它也能与CCL工作。

每个类都声明已被装入并运行。现在用源代码来试一下。编译CCLRun和CompilingClassLoader。确保不要编译其它类(Foo、Bar、Baz和Boo),否则将不会使用CCL,因为这些类已经编译过了。


% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!


请注意,首先调用编译器,Foo.java管理Bar和baz.Baz。直到Bar通过名称来装入Boo时,被调用它,这时CCL会再次调用编译器来编译它。

[b]CompilingClassLoader.java[/b]

以下是CompilingClassLoader.java的源代码

  
// $Id$
import java.io.*;
/*
A CompilingClassLoader compiles your Java source on-the-fly. It checks
for nonexistent .class files, or .class files that are older than their
corresponding source code.
*/
public class CompilingClassLoader extends ClassLoader
{
// Given a filename, read the entirety of that file from disk
// and return it as a byte array.
private byte[] getBytes( String filename ) throws IOException {
// Find out the length of the file
File file = new File( filename );
long len = file.length();
// Create an array that's just the right size for the file's
// contents
byte raw[] = new byte[(int)len];
// Open the file
FileInputStream fin = new FileInputStream( file );
// Read all of it into the array; if we don't get all,
// then it's an error.
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can't read all, "+r+" != "+len );
// Don't forget to close the file!
fin.close();
// And finally return the file contents as an array
return raw;
}
// Spawn a process to compile the java source code file
// specified in the 'javaFile' parameter. Return a true if
// the compilation worked, false otherwise.
private boolean compile( String javaFile ) throws IOException {
// Let the user know what's going on
System.out.println( "CCL: Compiling "+javaFile+"..." );
// Start up the compiler
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// Wait for it to finish running
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// Check the return code, in case of a compilation error
int ret = p.exitValue();
// Tell whether the compilation worked
return ret==0;
}
// The heart of the ClassLoader -- automatically compile
// source as necessary when looking for class files
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
// Our goal is to get a Class object
Class clas = null;
// First, see if we've already dealt with this one
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// Create a pathname from the class name
// E.g. java.lang.Object => java/lang/Object
String fileStub = name.replace( '.', '/' );
// Build objects pointing to the source code (.java) and object
// code (.class)
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
File javaFile = new File( javaFilename );
File classFile = new File( classFilename );
//System.out.println( "j "+javaFile.lastModified()+" c "+
// classFile.lastModified() );
// First, see if we want to try compiling. We do if (a) there
// is source code, and either (b0) there is no object code,
// or (b1) there is object code, but it's older than the source
if (javaFile.exists() &&
(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// Try to compile it. If this doesn't work, then
// we must declare failure. (It's not good enough to use
// and already-existing, but out-of-date, classfile)
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// Another place where we might come to if we fail
// to compile
throw new ClassNotFoundException( ie.toString() );
}
}
// Let's try to load up the raw bytes, assuming they were
// properly compiled, or didn't need to be compiled
try {
// read the bytes
byte raw[] = getBytes( classFilename );
// try to turn them into a class
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// This is not a failure! If we reach here, it might
// mean that we are dealing with a class in a library,
// such as java.lang.Object
}
//System.out.println( "defineClass: "+clas );
// Maybe the class is in a library -- try loading
// the normal way
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// Resolve the class, if any, but only if the "resolve"
// flag is set to true
if (resolve && clas != null)
resolveClass( clas );
// If we still don't have a class, it's an error
if (clas == null)
throw new ClassNotFoundException( name );
// Otherwise, return the class
return clas;
}
}




[b]CCRun.java[/b]

以下是CCRun.java的源代码



// $Id$
import java.lang.reflect.*;
/*
CCLRun executes a Java program by loading it through a
CompilingClassLoader.
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// The first argument is the Java program (class) the user
// wants to run
String progClass = args[0];
// And the arguments to that program are just
// arguments 1..n, so separate those out into
// their own array
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// Create a CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// Load the main class through our CCL
Class clas = ccl.loadClass( progClass );
// Use reflection to call its main() method, and to
// pass the arguments in.
// Get a class representing the type of the main method's argument
Class mainArgType[] = { (new String[0]).getClass() };
// Find the standard main method in the class
Method main = clas.getMethod( "main", mainArgType );
// Create a list containing the arguments -- in this case,
// an array of strings
Object argsArray[] = { progArgs };
// Call the method
main.invoke( null, argsArray );
}
}




[b]Foo.java[/b]

以下是Foo.java的源代码
 
// $Id$
public class Foo{
static public void main( String args[] ) throws Exception{
System.out.println( "foo! "+args[0]+" "+args[1] );
new Bar( args[0], args[1] );
}
}



[b]Bar.java[/b]

以下是Bar.java的源代码


 


// $Id$
import baz.*;
public class Bar{
public Bar( String a, String b ) {
System.out.println( "bar! "+a+" "+b );
new Baz( a, b );
try {
Class booClass = Class.forName( "Boo" );
Object boo = booClass.newInstance();
} catch( Exception e ) {
e.printStackTrace();
}
}
}



[b]baz/Baz.java[/b]

以下是baz/Baz.java的源代码


 
//
// $Id$
package baz;
public class Baz{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}




[b]Boo.java[/b]

以下是Boo.java的源代码


 
// $Id$
public class Boo{
public Boo() {
System.out.println( "Boo!" );
}
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值