正文

Java中类的查找与装载出现的问题总是会时不时出现在Java程序员面前,这并不是什么丢脸的事情,相信没有一个Java程序员没遇到过ClassNotException,因此不要为被人瞅见自己也犯这样的错误而觉得不自然,但是在如果出现了ClassNotFoundException后异常后一脸的茫然,那我想你该了解一下java的类装载的体制了,同时为了进行下面的关于类装载器之间的隔离性的讨论,我们先简单介绍一下类装载的体系结构。

1. Java类装载体系结构

装载类的过程非常简单:查找类所在位置,并将找到的Java类的字节码装入内存,生成对应的Class对象。Java的类装载器专门用来实现这样的过程,JVM并不止有一个类装载器,事实上,如果你愿意的话,你可以让JVM拥有无数个类装载器,当然这除了测试JVM外,我想不出还有其他的用途。你应该已经发现到了这样一个问题,类装载器自身也是一个类,它也需要被装载到内存中来,那么这些类装载器由谁来装载呢,总得有个根吧?没错,确实存在这样的根,它就是神龙见首不见尾的Bootstrap ClassLoader. 为什么说它神龙见首不见尾呢,因为你根本无法在Java代码中抓住哪怕是它的一点点的尾巴,尽管你能时时刻刻体会到它的存在,因为java的运行环境所需要的所有类库,都由它来装载,而它本身是C++写的程序,可以独立运行,可以说是JVM的运行起点,伟大吧。在Bootstrap完成它的任务后,会生成一个AppClassLoader(实际上之前系统还会使用扩展类装载器ExtClassLoader,它用于装载Java运行环境扩展包中的类),这个类装载器才是我们经常使用的,可以调用ClassLoader.getSystemClassLoader() 来获得,我们假定程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类通通会由它来装载,值得尊敬吧。AppClassLoader查找类的区域就是耳熟能详的Classpath,也是初学者必须跨过的门槛,有没有灵光一闪的感觉,我们按照它的类查找范围给它取名为类路径类装载器。还是先前假定的情况,当Java中出现新的类,AppClassLoader首先在类传递给它的父类类装载器,也就是Extion ClassLoader,询问它是否能够装载该类,如果能,那AppClassLoader就不干这活了,同样Extion ClassLoader在装载时,也会先问问它的父类装载器。我们可以看出类装载器实际上是一个树状的结构图,每个类装载器有自己的父亲,类装载器在装载类时,总是先让自己的父类装载器装载(多么尊敬长辈),如果父类装载器无法装载该类时,自己就会动手装载,如果它也装载不了,那么对不起,它会大喊一声:Exception,class not found。有必要提一句,当由直接使用类路径装载器装载类失败抛出的是NoClassDefFoundException异常。如果使用自定义的类装载器loadClass方法或者ClassLoader的findSystemClass方法装载类,如果你不去刻意改变,那么抛出的是ClassNotFoundException。

我们简短总结一下上面的讨论:

1.JVM类装载器的体系结构可以看作是树状结构。

2.父类装载器优先装载。在父类装载器装载失败的情况下再装载,如果都装载失败则抛出ClassNotFoundException或者NoClassDefFoundError异常。

那么我们的类在什么情况下被装载的呢?

2. 类如何被装载

在java2中,JVM是如何装载类的呢,可以分为两种类型,一种是隐式的类装载,一种式显式的类装载。

2.1 隐式的类装载

隐式的类装载是编码中最常用得方式:

A b = new A();

如果程序运行到这段代码时还没有A类,那么JVM会请求装载当前类的类装器来装载类。问题来了,我把代码弄得复杂一点点,但依旧没有任何难度,请思考JVM得装载次序:

package test;

Public class A{

public void static main(String args[]){

B b = new B();

}

}

class B{C c;}

class C{}

揭晓答案,类装载的次序为A->B,而类C根本不会被JVM理会,先不要惊讶,仔细想想,这不正是我们最需要得到的结果。我们仔细了解一下JVM装载顺序。当使用Java A命令运行A类时,JVM会首先要求类路径类装载器(AppClassLoader)装载A类,但是这时只装载A,不会装载A中出现的其他类(B类),接着它会调用A中的main函数,直到运行语句b = new B()时,JVM发现必须装载B类程序才能继续运行,于是类路径类装载器会去装载B类,虽然我们可以看到B中有有C类的声明,但是并不是实际的执行语句,所以并不去装载C类,也就是说JVM按照运行时的有效执行语句,来决定是否需要装载新类,从而装载尽可能少的类,这一点和编译类是不相同的。

2.2 显式的类装载

使用显示的类装载方法很多,我们都装载类test.A为例。

使用Class类的forName方法。它可以指定装载器,也可以使用装载当前类的装载器。例如:

Class.forName("test.A");

它的效果和

Class.forName("test.A",true,this.getClass().getClassLoader());

是一样的。

使用类路径类装载装载.

ClassLoader.getSystemClassLoader().loadClass("test.A");

使用当前进程上下文的使用的类装载器进行装载,这种装载类的方法常常被有着复杂类装载体系结构的系统所使用。

Thread.currentThread().getContextClassLoader().loadClass("test.A")

使用自定义的类装载器装载类

public class MyClassLoader extends URLClassLoader{

public MyClassLoader() {

super(new URL[0]);

}

}

MyClassLoader myClassLoader = new MyClassLoader();

myClassLoader.loadClass("test.A");

MyClassLoader继承了URLClassLoader类,这是JDK核心包中的类装载器,在没有指定父类装载器的情况下,类路径类装载器就是它的父类装载器,MyClassLoader并没有增加类的查找范围,因此它和类路径装载器有相同的效果。

我们已经知道Java的类装载器体系结构为树状,多个类装载器可以指定同一个类装载器作为自己的父类,每个子类装载器就是树状结构的一个分支,当然它们又可以个有子类装载器类装载器,类装载器也可以没有父类装载器,这时Bootstrap类装载器将作为它的隐含父类,实际上Bootstrap类装载器是所有类装载器的祖先,也是树状结构的根。这种树状体系结构,以及父类装载器优先的机制,为我们编写自定义的类装载器提供了便利,同时可以让程序按照我们希望的方式进行类的装载。例如某个程序的类装载器体系结构图如下:

图2:某个程序的类装载器的结构

解释一下上面的图,ClassLoaderA为自定义的类装载器,它的父类装载器为类路径装载器,它有两个子类装载器ClassLoaderAA和ClassLaderAB,ClassLoaderB为程序使用的另外一个类装载器,它没有父类装载器,但有一个子类装载器ClassLoaderBB。你可能会说,见鬼,我的程序怎么会使用这么复杂的类装载器结构。为了进行下面的讨论,暂且委屈一下。

3. 奇怪的隔离性

我们不难发现,图2中的类装载器AA和AB, AB和BB,AA和B等等位于不同分支下,他们之间没有父子关系,我不知道如何定义这种关系,姑且称他们位于不同分支下。两个位于不同分支的类装载器具有隔离性,这种隔离性使得在分别使用它们装载同一个类,也会在内存中出现两个Class类的实例。因为被具有隔离性的类装载器装载的类不会共享内存空间,使得使用一个类装载器不可能完成的任务变得可以轻而易举,例如类的静态变量可能同时拥有多个值(虽然好像作用不大),因为就算是被装载类的同一静态变量,它们也将被保存不同的内存空间,又例如程序需要使用某些包,但又不希望被程序另外一些包所使用,很简单,编写自定义的类装载器。类装载器的这种隔离性在许多大型的软件应用和服务程序得到了很好的应用。下面是同一个类静态变量为不同值的例子。

package test;

public class A {

public static void main( String[] args ) {

try {

//定义两个类装载器

MyClassLoader aa= new MyClassLoader();

MyClassLoader bb = new MyClassLoader();

//用类装载器aa装载testb.B类

Class clazz=aa.loadClass("testb. B");

Constructor constructor=

clazz.getConstructor(new Class[]{Integer.class});

Object object =

constructor.newInstance(new Object[]{new Integer(1)});

Method method =

clazz.getDeclaredMethod("printB",new Class[0]);

//用类装载器bb装载testb.B类

Class clazz2=bb.loadClass("testb. B");

Constructor constructor2 =

clazz2.getConstructor(new Class[]{Integer.class});

Object object2 =

constructor2.newInstance(new Object[]{new Integer(2)});

Method method2 =

clazz2.getDeclaredMethod("printB",new Class[0]);

//显示test.B中的静态变量的值

method.invoke( object,new Object[0]);

method2.invoke( object2,new Object[0]);

} catch ( Exception e ) {

e.printStackTrace();

}

}

}

//Class B 必须位于MyClassLoader的查找范围内,

//而不应该在MyClassLoader的父类装载器的查找范围内。

package testb;

public class B {

static int b ;

public B(Integer testb) {

b = testb.intValue();

}

public void printB() {

System.out.print("my static field b is ", b);

}

}

public class MyClassLoader extends URLClassLoader{

private static File file = new File("c:\\classes ");

//该路径存放着class B,但是没有class A

public MyClassLoader() {

super(getUrl());

}

public static URL[] getUrl() {

try {

return new URL[]{file.toURL()};

} catch ( MalformedURLException e ) {

return new URL[0];

}

}

}

程序的运行结果为:

my static field b is 1

my static field b is 2

程序的结果非常有意思,从编程者的角度,我们甚至可以把不在同一个分支的类装载器看作不同的java虚拟机,因为它们彼此觉察不到对方的存在。程序在使用具有分支的类装载的体系结构时要非常小心,弄清楚每个类装载器的类查找范围,尽量避免父类装载器和子类装载器的类查找范围中有相同类名的类(包括包名和类名),下面这个例子就是用来说明这种情况可能带来的问题。

假设有相同名字却不同版本的接口 A,

版本 1:

package test;

Intefer Same{ public String getVersion(); }

版本 2:

Package test;

Intefer Same{ public String getName(); }

接口A两个版本的实现:

版本1的实现

package test;

public class Same1Impl implements Same {

public String getVersion(){ return "A version 1";}

}

版本2的实现

public class Same 2Impl implements Same {

public String getName(){ return "A version 2";}

}

我们依然使用图2的类装载器结构,首先将版本1的Same和Same的实现类Same1Impl打成包same1.jar,将版本2的Same和Same的实现类Same1Impl打成包same2.jar。现在,做这样的事情,把same1.jar放入类装载器ClassLoaderA的类查找范围中,把same2.jar放入类装器ClassLoaderAB的类查找范围中。当你兴冲冲的运行下面这个看似正确的程序。

实际上这个错误的是由父类载器优先装载的机制造成,当类装载器ClassLoaderAB在装载Same2Impl类时发现必须装载接口test.Same,于是按规定请求父类装载器装载,父类装载器发现了版本1的test.Same接口并兴冲冲的装载,但是却想不到Same2Impl所希望的是版本2 的test.Same,后面的事情可想而知了,异常被抛出。

我们很难责怪Java中暂时并没有提供区分版本的机制,如果使用了比较复杂的类装载器体系结构,在出现了某个包或者类的多个版本时,应特别注意。

掌握和灵活运用Java的类装载器的体系结构,对程序的系统设计,程序的实现,已经程序的调试,都有相当大的帮助。希望以上的内容能够对您有所帮助。

命名空间和工程名java_Java的类装载器(Class Loader)和命名空间(NameSpace)相关推荐

  1. Java的类装载器(Class Loader)和命名空间(NameSpace)

    摘要 Java的类装载器是Java动态性的核心,本文将向大家简要介绍Java的类装载器,及相关的parent delegation模型,命名空间,运行时包等概念,同时讨论一些在学习中容易混淆的问题. ...

  2. weblogic服务器部署的程序,如何直接通过IP访问(即URL中去掉工程名)

    用weblogic部署的程序,怎么能够直接通过IP访问呢? 下面就是了 打开你的工程,看看webroot下的WEB-INF中有没有一个weblogic.xml文件. 1.如果没有,自己建一个,里面写上 ...

  3. 如何修改visual-studio的sln文件和project工程名

    关于VS的 .sln 文件和 .suo 文件 *.sln:(Visual Studio.Solution) 通过为环境提供对项目.项目项和解决方案项在磁盘上位置的引用,可将它们组织到解决方案中.比如是 ...

  4. 使用Xcode修改iOS项目工程名和路径名

    旧工程名 MyProject-iPad 修改之后 新工程名 FjSk-iPad 点击项目,进入 可以看到右侧Identity,修改Project Name为:FjSk-iPad,点击enter键,出现 ...

  5. VS2008修改工程名

    修改工程名: 1.重命名.sln文件为想要的名字 2.用记事本方式打开.sln文件,将文件中所有原工程名字替换为想要的名字. 3.重命名.vcprj文件名为想要的名字 4.用记事本方式打开.vcpro ...

  6. Errors running builder 'DeploymentBuilder' on project '工程名'

    打开myEclipse就会报 Errors running builder 'DeploymentBuilder' on project '工程名' xxxNullpointException 的错误 ...

  7. Xcode 修改工程名以及注意事项

    1.先把整个工程文件夹名改为新的工程名. 2.打开工程,单击,输入新的工程名,会出现,点击确定. 3.回到工程界面,在中选择 Manage Schemes,然后再弹出的对话框,把工程名改为新的名字. ...

  8. Keil | 解决Keil双击工程名无法打开.map的问题

    文章目录 一.前言 二.解决方法 一.前言 在实际工作中,习惯查看.map文件非常重要.但是,使用CubeMX生成STM32H743的Keil工程后,发现无法通过双击工程文件名打开.map文件. 问题 ...

  9. Errors running builder #39;DeploymentBuilder#39; on project #39;工程名#39;

    打开myEclipse时,报 Errors running builder 'DeploymentBuilder' on project '工程名' xxxNullpointException 的错误 ...

最新文章

  1. 面向对象的继承关系体现在数据结构上时,如何表示
  2. Altium Designer -- PCB布局与布线
  3. redis(17)--集群
  4. OCA第4部分中的Java难题
  5. python使用opencv实现人脸识别系统
  6. c语言宏 转换字符串,c预处理程序-如何从C宏的值生成char字符串?
  7. rails 3 中 app/model 目录下添加继承
  8. Cocos2d-x windows + vs2010 配置图文详解
  9. python范围数字求和_Python范围()
  10. 微信小程序文本溢出的处理方法
  11. 计算机组成原理期末复习整理 白中英版本
  12. 复制百度文库文字最简单的方法
  13. Windows 使用技巧 -- 自定义桌面图标显示
  14. fastjson基本使用
  15. vue-父子组件传参以及无限级评论
  16. 追赶的腾讯云 | 深网
  17. 元气骑士+蒲公英联机平台联机教程
  18. php 判断当前手机类型_php中HTTP_USER_AGENT判断手机类型的函数
  19. 实现一个自定义的删除shell脚本
  20. Web渗透信息收集之域名、端口、服务、指纹、旁站、CDN和敏感信息

热门文章

  1. 算法练习day10——190328(二叉树的先序、 中序、 后序遍历, 包括递归方式和非递归方式、找到一个节点的后继节点、二叉树的序列化和反序列化)
  2. antd 中table上加不同字体颜色_字体渲染系统!微软终于决定优化Win10字体模糊问题...
  3. 净误差与遗漏为负值的含义_巴丹吉林沙漠湖泊水位变化及地下水净补给量
  4. eos操作系统_如何基于EOS区块链发一个自己的币,挣它一个亿?
  5. busybox 安装mysql_安装busybox
  6. java8 lambda python_【学习笔记】java8 Lambda表达式语法及应用
  7. a股用计算机模拟走势,咬牙跺脚!A股牛市不改的铁证!
  8. asp.net代码审计起始篇之系统搭建
  9. 【小o地图Excel插件版】不止能做图表,还能抓58、大众点评网页数据...
  10. 对于es6的小小理解之generator函数