`
nonopo
  • 浏览: 14338 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
文章分类
社区版块
存档分类
最新评论

译 Java类加载机制(二)

    博客分类:
  • java
阅读更多

译:ayi 译文疏漏,请多多指点

原文:http://www.onjava.com/pub/a/onjava/2005/01/26/classloading.html

 

注:因内容太多,分为一、二两篇文章,建议下载附件查看

 

为什么我们要使用我们自己的类加载器?

 

    开发者编写自己的类加载器的一个理由是控制JVM的行为。在java中区分一个类是通过包名加类名。对于实现了java.io.Serializable接口的类,serialVersionUID 将在类的版本化中担当一个重要角色。这种流唯一标志( stream-unique identifier )是一个由类名、接口名、方法以及字段生成的一个64位的哈希码。除了这些,没有其它的直接的结构来版本化一个类。单从技术上来说,如果上面所说的匹配的话,这些类就是相同的版本。

 

想一下这种情况,当我们想要去开发一个面向一般性的执行引擎,它能够执行任何实现了某个特定接口的任务。当这些任务被提交到引擎,引擎首先需要去加载这些任务的代码。假如很多不同的客户提交不同的任务(就是:不同的代码)给我们的引擎,碰巧,这些任务都有相同的类名和包名。问题产生了,引擎将要为不同的客户调用上下文加载不同的客户版本,使客户能得到它们想要的正确结果吗?这将在下面,通过加载相同的客户代码来证实。在samepath 和 differentversions这两个目录中,均包括独立的代码来证实这个原理。

图2 展示了示例在samepath, differentversions, 和 differentversionspush 这三个子目录中。

 

 

 

 

图2 示例的目录结构

在samepath目录,我们有均version.Version 类分别在它的子目录v1和v2下面。这两个类均有相同的类名和包名。唯一的不同是:

 public void fx(){ 

        log("this = " + this + "; Version.fx(1)."); 

    } 

在v1中,我们有Version.fx(1)在log 语句中;而在v2中,是Version.fx(2)。在相同的路径下,这两个不同版本的类仅有这点区别。现在执行Test 如下:

set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2 

%JAVA_HOME%\bin\java Test 

在图3中,我们将看到控制台的输出,Version.fx(1) 被加载了,因为ClassLoader在classpath中首先找到了v1中的类。

 

 

图3 在版本1的test类的路径放在前面

我们稍微改变一下classpath中个路径的先后顺序,再次运行一次:

set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1 

%JAVA_HOME%\bin\java Test 

控制台的输出改变了,如图4。现在,Version.fx(2) 被执行了,因为class loader 先找到了这个版本的类。

 

图4 在版本2的test类的路径放在前面

从上面的可以看出,class loader 会加载在classpath中先找到的类。如果我们不这样,从v1、v2中删除version.Version ,而把它们打包在一个myextension.jar的.jar文件中,并把它们放到java.ext.dirs目录下面,重新测试,我们将看到,它将不再被AppClassLoader加载,而是被扩展类加载器加载。如图5所示:

图5 AppClassLoader 和 ExtClassLoader 类加载器

进一步来研究这个例子,目录differentversions 下面有一个RMI 执行引擎。客户能够提交任何实现了common.TaskIntf 接口的任务给这个引擎。两个子目录client1 和client2 均包含了稍有区别的这个类client.TaskImpl。它们的区别如下代码所示:

 static{ 

        log("client.TaskImpl.class.getClassLoader 

        (v1) : " + TaskImpl.class.getClassLoader()); 

    } 

 

    public void execute(){ 

        log("this = " + this + "; execute(1)"); 

    } 

 

在client1的log语句中执行的是getClassLoader(v1) 是 execute(1),在client2的log语句中执行的是getClassLoader(v2) 是 execute(2)。而且,在启动RMI服务器引擎的脚本中,我们不妨把client2路径放在前面:

CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server; 

    %CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1 

%JAVA_HOME%\bin\java server.Server 

在图6、7、8的屏幕截屏中展示了这时的情况。这时,在两个客户虚拟机中,单独的client.TaskImpl类,被加载、初始化,并传递到了服务器端虚拟机的执行引擎中。从服务器端的控制台可以看出,client.TaskImpl仅被加载了一次。这个单独“版本”的代码在服务器端

被用来产生许多client.TaskImpl 实例,来执行任务。

图6 服务器端控制台输出

图6展示了服务器端引擎控制台的输出,它加载、执行两个各自的客户请求(如图7、8所示)。要指出的是,在这里代码仅被执行了一次(很明显,从静态初始化语句块中的log语句来看),但是这个方法被执行了两次,每个客户的一次调用。

图7 客户1的控制台

在图7中,服务器端提供的类TaskImpl 包括语句client.TaskImpl.class.getClassLoader(v1) 被加载入了客户虚拟机。在图8中,客户虚拟机加载了不同的,包含client.TaskImpl.class.getClassLoader(v2)的,TaskImpl 类代码。

图8 客户2的控制台

这里,各自的client.TaskImpl 类被加载、初始化,并且交给服务器端虚拟机来执行。再次看一下图6所展示的服务器端的控制台,显示了client.TaskImpl 加载且仅被加载了一次。这个单独的代码将被用来在服务器端生成client.TaskImpl 实例。Client1将不高兴了,因为它的语句client.TaskImpl(v1)并没有被执行,而是其它的代码在执行当它在客户端调用时。我们怎样处理这种情况呢?答案是实现客户的类加载器。

客户类加载器

较好的控制类加载的解决办法是实现自己的客户类加载器。任何客户类加载器都必须直接或间接继承java.lang.ClassLoader。而且在构造函数中,我们也必须把父类设置为类加载器。然后,我们要重写findClass()方法。在differentversionspush 目录中包括了一个名为FileSystemClassLoader的类加载器。目录结果如图9所示:

图9 客户类加载器关系

下面是在common.FileSystemClassLoader实现的主要方法:

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(); 

        } 

    } 

这个类被客户用来转换client.TaskImpl(v1)为byte[]。这个byte[]将被传递给服务器端执行引擎。在服务器端,相同的类将被用来从字节数组逆转定义这个类。客户端代码如下:

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); 

    } 

从服务器端来看,从客户端接受的代码将交给客户类加载器。客户类加载器首先从接收到的字节数组中逆定义类,实例化,然后执行。这里值得指出的是,对于每个客户请求,都将使用各自的FileSystemClassLoader实例来加载提供的client.TaskImpl。而且,client.TaskImpl类并不在服务器端的classpath范围内。这就意味着,当我们在FileSystemClassLoader中调用findClass() 时,findClass()会从内部调用defineClass(), client.TaskImpl将被各自的类加载器实例加载。 当有一个新的FileSystemClassLoader 实例来加载时,照样从逆定义类开始重新做一遍。所以,对每个客户调用,类client.TaskImpl 都被重新定义了,使我们能够在同一个虚拟机中执行“不同版本”的client.TaskImpl 代码。

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()); 

        } 

    } 

目录differentversionspush 下的examples。服务器和客户端控制台输出,如图10 、11、12中所示:

图10 服务器端的Custom class loader 的执行结果

图10 展示了客户类加载器的虚拟机控制台。我们可以看到client.TaskImpl 被执行了不只一次。事实上,对每个客户执行上下文环境,这个类被新加载和初始化一次。

图11 客户类加载器引擎 Client 1

在图11中,包含了语句client.TaskImpl.class.getClassLoader(v1) 的TaskImpl类的这些代码在客户端被加载,然后被放到服务器端执行。图12,包含了语句client.TaskImpl.class.getClassLoader(v2) 的TaskImpl的不同类在客户端2的加载情况,并在服务器端执行。

图12 客户类加载引擎 ,client2

 

这例子向我们展示了,当在同一个JVM中有“不同版本”的代码时,我们怎样使用各自的类加载器实例来实现一一对应的边对边执行。

类加载器在J2EE中

在j2ee中,类加载器倾向于在不同的时间段移除和重新加载类。这种情况在某些实现中存在,在某些中没有。web服务器可能会移除以前的一个已经加载的servlet实例,可能因为是管理员明确地要这么做,也可能是这个servlet已经空闲的很长一段时间。当第一次请求一个jsp(假设这个jsp还没有被初次编译),JSP引擎将转译这个jsp为一个页面实现类,切实一个标准的servlet类。只要这个页面实现类servlet一创建,它将被JSP引擎编译为一个class文件、备用。每当客户请求这个jsp时,编译器将首先会检查这个jsp是否被修改了。如果是的话,JSP引擎将把它重新转译,以确保给客户端的响应是最新的jsp页面实现所生成的。以.ear, .war, .rar形式的企业应用程序部署单元,也需要在需要的时候或配置策略改变时,被加载或重新加载。对所有的情况来说,都有可能被加载、移除以及重新加载,除非我们已经控制了应用程序服务器的类加载策略。这通过扩展类加载器可以做到,因为它能执行在它范围内的代码。

Brett Peterson已经在发表在 TheServerSide.com的"Understanding J2EE Application Server Class Loading Architectures"上给出了J2EE application server 的类加载模式的解释。

总结

这篇文章讲述了怎样把类加载到JVM并唯一标识,和具有相同类名和包名时的一些限制。

因为没有直接的类版本结构,如果我们想按我们自己得到想法来加载类时,我们不得不使用客户类加载器来扩展实现。许多J2EE应用程序服务器有“热部署”的能力,能够使我们以一个新的类定义来重新加载应用程序,而不需要关闭服务器。这些应用程序服务器利用了客户类加载器。尽管我们不使用应用程序服务器,我们可以创建和使用客户类加载器来更好的控制java应用程序的类加载。 Ted Neward的书Server-Based Java Programming

非常好的描述了java类加载的详细细节,并告诉了我们一些在 J2EE APIs中没有提到的东西以及怎样更好的去使用它们。

 

 

参考

Sample code for this article 

JDK 1.5 API Docs 

The Java language specification 

"Understanding Extension Class Loading " in the Java tutorial 

"Inside Class Loaders" from ONJava 

"Inside Class Loaders: Debugging" from ONJava 

"What version is your Java code?" from JavaWorld 

"Understanding J2EE Applicatio

 

 

 

 

 

  • java_____.rar (318.8 KB)
  • 描述: 为方便查看,建议下载此word文档,包括"译 Java类加载机制(一、二)"
  • 下载次数: 251
分享到:
评论
2 楼 javatozhang 2013-03-06  
译文不错!受用了!谢谢!如果能把文章中的例子能在译文中添加详细的讲解和怎样在测试电脑上运行会更好!谢谢!!!
1 楼 huaerfan 2011-04-01  
 

相关推荐

    深入JAVA虚拟机第二版 Bill Venners著 曹晓钢 蒋靖译

    2. **类加载机制**:JVM的类加载过程包括加载、验证、准备、解析和初始化五个阶段。这个过程确保了类的安全性和正确性。 3. **内存管理**:JVM内存分为堆内存、方法区、虚拟机栈、本地方法栈和程序计数器等几部分。...

    侯捷-java编程思想.pdf

    10. **Java虚拟机(JVM)**:作者简要分析了JVM的工作原理,包括类加载机制、内存模型以及垃圾收集。这部分内容有助于理解Java程序的运行过程和性能优化。 11. **编程实践**:最后,书中提供了许多实用的编程技巧和...

    Java教学大纲总结

    Java 程序设计是工科电类和管理类高年级本科生的专业课。通过本课程的学习,使学生掌握 Java 的基础知识。掌握 JAVA 的 JDK 开发环境和系统配置,能够使用面向对象思想进行程序设计,掌握图形用户界面的设计方法,...

    Practical Java (侯捷 刘永丹译-超星版

    9. **JVM与内存管理**:讲解Java虚拟机的工作原理,包括类加载机制、垃圾收集以及性能优化。 10. **单元测试与调试**:介绍JUnit等测试框架,以及如何编写和执行单元测试,调试技巧和工具有助于提高代码质量。 11....

    Java核心技术(原书第八版)一二卷(含有目录)pdf

    10. **JVM优化**:如何通过JVM参数调整程序性能,理解类加载机制,以及对内存和垃圾收集的调优。 这本书的目录设计得非常清晰,方便读者快速定位到所需的内容。无论你是初学者还是有经验的开发者,都可以从中获得...

    Java语言概述- Java语言

    - **类加载器**:在运行时动态加载类文件。 - **运行环境**:包括Java Development Kit (JDK) 和Java Runtime Environment (JRE),提供了运行和开发Java程序所需的所有工具和库。 学习Java语言,你可以参考以下书籍...

    武汉大学JAVA教程

    - 《Java编程思想》(Bruce Eckel,候捷译,机械工业出版社) - The Java Tutorial(http://java.sun.com/docs/books/tutorial) - Thinking in Java(Bruce Eckel,多个版本) 7. Java核心技术 - Applet:早期...

    java课件--1.语言概述

    7. **动态性**:Java允许程序在运行时加载新类和资源,适应变化的需求。 学习Java的基础包括: 1. **Java语言语法基础**:涵盖变量、数据类型、控制结构、方法等基本元素。 2. **面向对象的特征**:深入理解类、...

    JVM原理.pdf

    类加载机制是Java语言中的一个核心概念,它负责将.class文件加载到内存中,创建对应的java.lang.Class对象。JVM将类加载过程分为加载、链接(验证、准备、解析)、初始化三个阶段。这个过程是动态的,它允许程序在...

    Java数据编程指南

    【原 书 名】 Professional Java Data 【原出版社】 Wrox 【作 者】[美] Danny Ayers ,John Bell ,Carl Calvert Bettis等 【译 者】 戴英 张晓晖 王辉 等 【丛 书 名】 乐思公司编程指南系列 ...

    Java中的反射机制详解

    1. 反编译:通过反射机制,可以将.class文件反编译成.java文件。 2. 访问java对象的属性、方法、构造方法等:通过反射机制,可以访问java对象的属性、方法、构造方法等。 3. 动态加载类:通过反射机制,可以在运行时...

    机器学习中的机器翻译,通过调用百度翻译的api实现汉译英,再英译汉

    6. **错误处理**:在实现过程中,应考虑到可能出现的异常情况,比如网络连接问题、API调用超时、无效的API密钥等,都需要适当的错误处理机制,确保程序的健壮性。 7. **IDEA集成**:项目可以在IntelliJ IDEA中直接...

    《Java应用架构设计-模块化模式与OSGi》书中源码,美国 Kirk著,张卫滨译

    《Java应用架构设计-模块化模式与OSGi》是一本由Kirk著、张卫滨翻译的书籍,它深入探讨了在Java环境中如何构建可扩展、可维护的大型应用程序。书中的源码提供了丰富的实例,帮助读者理解并实践模块化模式和OSGi...

    译How Tomcat Works(第二章)

    《译How Tomcat Works(第二章)》这篇文章主要讲解了Apache Tomcat服务器的工作原理,它是一个开源的Java Servlet容器,广泛用于部署Web应用程序。在这一章中,我们将深入探讨Tomcat如何处理HTTP请求,以及其内部架构...

Global site tag (gtag.js) - Google Analytics