服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

技术分析java类加载内幕详细讲解一(图)


  类加载是java语言提供的最强大的机制之一。尽管类加载并不是讨论的热点话题,但所有的编程人员都应该了解其工作机制,明白如何做才能让其满足我们的需要。这能有效节省我们的编码时间,从不断调试classnotfoundexception, classcastexception的工作中解脱出来。
  
  这篇文章从基础讲起,比如代码与数据的不同之处是什么,他们是如何构成一个实例或对象的。然后深入探讨java虚拟机(jvm)是如何利用类加载器读取代码,以及java中类加载器的主要类型。接着用一个类加载的基本算法看一下类加载器如何加载一个内部类。本文的下一节演示一段代码来说明扩展和开发属于自己的类加载器的必要性。紧接着解释如何使用定制的类加载器来完成一个一般意义上的任务,使其可以加载任意远端客户的代码,在jvm中定义,实例化并执行它。本文包括了j2ee关于类加载的规范――事实上这已经成为了j2ee的标准之一。
  
  类与数据
  
  一个类代表要执行的代码,而数据则表示其相关状态。状态时常改变,而代码则不会。当我们将一个特定的状态与一个类相对应起来,也就意味着将一个类事例化。尽管相同的类对应的实例其状态千差万别,但其本质都对应着同一段代码。在java中,一个类通常有着一个.class文件,但也有例外。在java的运行时环境中(java runtime),每一个类都有一个以第一类(first-class)的java对象所表现出现的代码,其是java.lang.class的实例。我们编译一个java文件,编译器都会嵌入一个public, static, final修饰的类型为java.lang.class,名称为class的域变量在其字节码文件中。因为使用了public修饰,我们可以采用如下的形式对其访问:
  
  java.lang.class klass = myclass.class;
  
  一旦一个类被载入jvm中,同一个类就不会被再次载入了(切记,同一个类)。这里存在一个问题就是什么是“同一个类”?正如一个对象有一个具体的状态,即标识,一个对象始终和其代码(类)相关联。同理,载入jvm的类也有一个具体的标识,我们接下来看。
  
  在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中到底有多少种类加载器的实例?下一节我们揭示答案。
  
  类加载器
  
  在jvm中,每一个类都被java.lang.classloader的一些实例来加载.类classloader是在包中java.lang里,开发者可以自由地继承它并添加自己的功能来加载类。
  
  无论何时我们键入java mymainclass来开始运行一个新的jvm,“引导类加载器(bootstrap class loader)”负责将一些关键的java类,如java.lang.object和其他一些运行时代码先加载进内存中。运行时的类在jre/lib/rt.jar包文件中。因为这属于系统底层执行动作,我们无法在java文档中找到引导类加载器的工作细节。基于同样的原因,引导类加载器的行为在各jvm之间也是大相径庭。
  
  同理,如果我们按照如下方式:
  
  log(java.lang.string.class.getclassloader());
  
  来获取java的核心运行时类的加载器,就会得到null。
  
  接下来介绍java的扩展类加载器。扩展库提供比java运行代码更多的特性,我们可以把扩展库保存在由java.ext.dirs属性提供的路径中。
  
  (编辑注: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。)
  
  类extclassloader专门用来加载所有java.ext.dirs下的.jar文件。开发者可以通过把自己的.jar文件或库文件加入到扩展目录的classpath,使其可以被扩展类加载器读取。
  
  从开发者的角度,第三种同样也是最重要的一种类加载器是appclassloader。这种类加载器用来读取所有的对应在java.class.path系统属性的路径下的类。
  
  sun的java指南中,文章“理解扩展类加载”(understanding extension class loading)对以上三个类加载器路径有更详尽的解释,这是其他几个jdk中的类加载器
  
  ●java.net.urlclassloader
  ●java.security.secureclassloader
  ●java.rmi.server.rmiclassloader
  ●sun.applet.appletclassloader
  
  java.lang.thread,包含了public classloader getcontextclassloader()方法,这一方法返回针对一具体线程的上下文环境类加载器。此类加载器由线程的创建者提供,以供此线程中运行的代码在需要加载类或资源时使用。如果此加载器未被建立,缺省是其父线程的上下文类加载器。原始的类加载器一般由读取应用程序的类加载器建立。
  
  类加载器如何工作?
  
  除了引导类加载器,所有的类加载器都有一个父类加载器,不仅如此,所有的类加载器也都是java.lang.classloader类型。以上两种类加载器是不同的,而且对于开发者自订制的类加载器的正常运行也至关重要。最重要的方面是正确设置父类加载器。任何类加载器,其父类加载器是加载该类加载器的类加载器实例。(记住,类加载器本身也是一个类!)
  
  使用loadclass()方法可以从类加载器中获得该类。我们可以通过java.lang.classloader的源代码来了解该方法工作的细节,如下:
  
  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的两种构造方法来设置父类加载器:
  
  public class myclassloader extends classloader{
  public myclassloader(){
  super(myclassloader.class.getclassloader());
  }}
  
  或
  
  public class myclassloader extends classloader{
  public myclassloader(){
  super(getclass().getclassloader());
  }}
  
  第一种方式较为常用,因为通常不建议在构造方法里调用getclass()方法,因为对象的初始化只是在构造方法的出口处才完全完成。因此,如果父类加载器被正确建立,当要示从一个类加载器的实例获得一个类时,如果它不能找到这个类,它应该首先去访问其父类。如果父类不能找到它(即其父类也不能找不这个类,等等),而且如果findbootstrapclass0()方法也失败了,则调用findclass()方法。findclass()方法的缺省实现会抛出classnotfoundexception,当它们继承java.lang.classloader来订制类加载器时开发者需要实现这个方法。findclass()的缺省实现方式如下:
  
  protected class<?> findclass(string name)
  throws classnotfoundexception {
  throw new classnotfoundexception(name);
  }
  
  在findclass()方法内部,类加载器需要获取任意来源的字节码。来源可以是文件系统,url,数据库,可以产生字节码的另一个应用程序,及其他类似的可以产生java规范的字节码的来源。你甚至可以使用bcel (byte code engineering library:字节码工程库),它提供了运行时创建类的捷径。bcel已经被成功地使用在以下方面:编译器,优化器,混淆器,代码产生器及其他分析工具。一旦字节码被检索,此方法就会调用defineclass()方法,此行为对不同的类加载实例是有差异的。因此,如果两个类加载实例从同一个来源定义一个类,所定义的结果是不同的。
  
  java语言规范(java language specification)详细解释了java执行引擎中的类或接口的加载(loading),链接(linking)或初始化(initialization)过程。
  
  图一显示了一个主类称为mymainclass的应用程序。依照之前的阐述,mymainclass.class会被appclassloader加载。 mymainclass创建了两个类加载器的实例:customclassloader1 和 customclassloader2,他们可以从某数据源(比如网络)获取名为target的字节码。这表示类target的类定义不在应用程序类路径或扩展类路径。在这种情况下,如果mymainclass想要用自定义的类加载器加载target类,customclassloader1和customclassloader2会分别独立地加载并定义target.class类。这在java中有重要的意义。如果target类有一些静态的初始化代码,并且假设我们只希望这些代码在jvm中只执行一次,而这些代码在我们目前的步骤中会执行两次――分别被不同的customclassloaders加载并执行。如果类target被两个customclassloaders加载并创建两个实例target1和target2,如图一显示,它们不是类型兼容的。换句话说,在jvm中无法执行以下代码:
  
  target target3 = (target) target2;
  
  以上代码会抛出一个cla

扫描关注微信公众号