Java虚拟机类装载:原理、实现与应用[资料]

来源:互联网 发布:吉利知豆电动汽车 编辑:程序博客网 时间:2024/06/06 04:34
一、引言
  Java虚拟机(JVM)的类装载就是指将包含在类文件中的字节码装载到JVM中, 并使其成为JVM一部分的过程。JVM的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块, 而不影响系统其他功能模块的正常运行。本文将分析JVM中的类装载系统,探讨JVM中类装载的原理、实现以及应用。
  
  二、Java虚拟机的类装载实现与应用
  2.1 装载过程简介
  
  所谓装载就是寻找一个类或是一个接口的二进制形式并用该二进制形式来构造代表这个类或是这个接口的class对象的过程,其中类或接口的名称是给定了的。当然名称也可以通过计算得到,但是更常见的是通过搜索源代码经过编译器编译后所得到的二进制形式来构造。
  
  在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:
  
  装载:查找和导入类或接口的二进制数据;
  链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
  校验:检查导入类或接口的二进制数据的正确性;
  准备:给类的静态变量分配并初始化存储空间;
  解析:将符号引用转成直接引用;
  初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
  至于在类装载和虚拟机启动的过程中的具体细节和可能会抛出的错误,请参看《Java虚拟机规范》以及《深入Java虚拟机》,它们在网络上面的资源地址是: http://java.sun.com/docs/books/vmspec/2nd-edition/html/Preface.doc.html 和 http://www.artima.com/insidejvm/ed2/index.html。 由于本文的讨论重点不在此就不再多叙述。
  
  2.2 装载的实现
  
  JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
  
  在Java中,ClassLoader是一个抽象类,它在包java.lang中,可以这样说,只要了解了在ClassLoader中的一些重要的方法,再结合上面所介绍的JVM中类装载的具体的过程,对动态装载类这项技术就有了一个比较大概的掌握,这些重要的方法包括以下几个:
  
  ①loadCass方法 loadClass(String name ,boolean resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个方法是ClassLoader 的入口点。
  
  ②defineClass方法 这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。
  
  ③findSystemClass方法 findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM 正常装入类的缺省机制。
  
  ④resolveClass方法 resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。
  
  ⑤findLoadedClass方法 当调用loadClass方法装入类时,调用findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错误。
  
  2.3 装载的应用
  
  一般来说,我们使用虚拟机的类装载时需要继承抽象类java.lang.ClassLoader,其中必须实现的方法是loadClass(),对于这个方法需要实现如下操作:(1) 确认类的名称;(2) 检查请求要装载的类是否已经被装载;(3) 检查请求加载的类是否是系统类;(4) 尝试从类装载器的存储区获取所请求的类;(5) 在虚拟机中定义所请求的类;(6) 解析所请求的类;(7) 返回所请求的类。
  
  所有的Java 虚拟机都包括一个内置的类装载器,这个内置的类库装载器被称为根装载器(bootstrap ClassLoader)。根装载器的特殊之处是它只能够装载在设计时刻已知的类,因此虚拟机假定由根装载器所装载的类都是安全的、可信任的,可以不经过安全认证而直接运行。当应用程序需要加载并不是设计时就知道的类时,必须使用用户自定义的装载器(user-defined ClassLoader)。下面我们举例说明它的应用。
  
  public abstract class MultiClassLoader extends ClassLoader{
  ...
  public synchronized Class loadClass(String s, boolean flag)
  throws ClassNotFoundException
  {
  /* 检查类s是否已经在本地内存*/
  Class class1 = (Class)classes.get(s);
  
  /* 类s已经在本地内存*/
  if(class1 != null) return class1;
  try/*用默认的ClassLoader 装入类*/ {
  class1 = super.findSystemClass(s);
  return class1;
  }
  catch(ClassNotFoundException _ex) {
  System.out.println(">> Not a system class.");
  }
  
  /* 取得类s的字节数组*/
  byte abyte0[] = loadClassBytes(s);
  if(abyte0 == null)  throw new ClassNotFoundException();
  
  /* 将类字节数组转换为类*/
  class1 = defineClass(null, abyte0, 0, abyte0.length);
  if(class1 == null) throw new ClassformatError();
  if(flag)  resolveClass(class1); /*解析类*/
  
  /* 将新加载的类放入本地内存*/
  classes.put(s, class1);
  System.out.println(">> Returning newly loaded class.");
  
  /* 返回已装载、解析的类*/
  return class1;
  }
  ...
  }
  
  三、Java虚拟机的类装载原理
  前面我们已经知道,一个Java应用程序使用两种类型的类装载器:根装载器(bootstrap)和用户定义的装载器(user-defined)。根装载器是Java虚拟机实现的一部分,举个例子来说,如果一个Java虚拟机是在现在已经存在并且正在被使用的操作系统的顶部用C程序来实现的,那么根装载器将是那些C程序的一部分。根装载器以某种默认的方式将类装入,包括那些Java API的类。在运行期间一个Java程序能安装用户自己定义的类装载器。根装载器是虚拟机固有的一部分,而用户定义的类装载器则不是,它是用Java语言写的,被编译成class文件之后然后再被装入到虚拟机,并像其它的任何对象一样可以被实例化。 Java类装载器的体系结构如下所示:
  
 

  
图1 Java的类装载的体系结构

  
  Java的类装载模型是一种代理(delegation)模型。当JVM 要求类装载器CL(ClassLoader)装载一个类时,CL首先将这个类装载请求转发给他的父装载器。只有当父装载器没有装载并无法装载这个类时,CL才获得装载这个类的机会。这样, 所有类装载器的代理关系构成了一种树状的关系。树的根是类的根装载器(bootstrap ClassLoader) , 在JVM 中它以"null"表示。除根装载器以外的类装载器有且仅有一个父装载器。在创建一个装载器时, 如果没有显式地给出父装载器, 那么JVM将默认系统装载器为其父装载器。Java的基本类装载器代理结构如图2所示:
  

  
图2 Java类装载的代理结构


  
  下面针对各种类装载器分别进行详细的说明。
  
  根(Bootstrap) 装载器:该装载器没有父装载器,它是JVM实现的一部分,从sun.boot.class.path装载运行时库的核心代码。
  
  扩展(Extension) 装载器:继承的父装载器为根装载器,不像根装载器可能与运行时的操作系统有关,这个类装载器是用纯Java代码实现的,它从java.ext.dirs (扩展目录)中装载代码。
  
  系统(System or Application) 装载器:装载器为扩展装载器,我们都知道在安装JDK的时候要设置环境变量(CLASSPATH ),这个类装载器就是从java.class.path(CLASSPATH 环境变量)中装载代码的,它也是用纯Java代码实现的,同时还是用户自定义类装载器的缺省父装载器。
  
  小应用程序(Applet) 装载器: 装载器为系统装载器,它从用户指定的网络上的特定目录装载小应用程序代码。
  
  在设计一个类装载器的时候,应该满足以下两个条件:
  
  对于相同的类名,类装载器所返回的对象应该是同一个类对象
  如果类装载器CL1将装载类C的请求转给类装载器CL2,那么对于以下的类或接口,CL1和CL2应该返回同一个类对象:a)S为C的直接超类;b)S为C的直接超接口;c)S为C的成员变量的类型;d)S为C的成员方法或构建器的参数类型;e)S为C的成员方法的返回类型。
  每个已经装载到JVM中的类都隐式含有装载它的类装载器的信息。类方法getClassLoader 可以得到装载这个类的类装载器。一个类装载器认识的类包括它的父装载器认识的类和它自己装载的类,可见类装载器认识的类是它自己装载的类的超集。注意我们可以得到类装载器的有关的信息,但是已经装载到JVM中的类是不能更改它的类装载器的。
  
  Java中的类的装载过程也就是代理装载的过程。比如:Web浏览器中的JVM需要装载一个小应用程序TestApplet。JVM调用小应用程序装载器ACL(Applet ClassLoader)来完成装载。ACL首先请求它的父装载器, 即系统装载器装载TestApplet是否装载了这个类, 由于TestApplet不在系统装载



虚拟机这个概念不好理解,什么叫虚拟机?这和JAVA特有的可移植性有关,既然要可移动就不能和具体的计算机硬件有关,你这样想吧,假如WINDOWS机器是美国人,UNIX机器是法国人,APPLE机器是德国人,并且假设你只会汉语而且你是这三个只懂本国语言人的老板,你需要给他们布置工作,于是你找到他们三个,面对面讲话,他们谁也听不懂啊,所以你们在一起工作没有结果可言。所以你就想了个主意,你为他们每个人都配了个形影不离的翻译,你说话时对着翻译,翻译就把你的话解释给他们三人,OK,问题解决,你应该会问这样做不是要增加成本吗,没错,翻译的过程是要损失效率的,但这使你不至于什么事都做不了。现在你应该能多少了解JVM的概念了吧,就象个翻译。

这个将是很容易被忽视但是极端重要的东东—JAVA元素在内存中的分配情况。首先假定你有STACK堆栈的概念,它是内存中的一个部分,特性是数据访问是先进后出,后进先出,这里你要明白这8个字有个隐含的约束,就是时间,如果你要明确数据进出的顺序,就要明确它们进出的时机,主要是出栈的时间,就是说你要明确指出什么时候这个数据应该出栈,这样才能保证先前进栈的数据有机会出栈。还有就是堆的概念,堆是程序运行时大量OBJECT对象存在的空间,你要有个形象的想象图,不要以为计算机是在神奇地凭空完成程序,就象自然万物数据也是要有空间才能存在的,回顾刚才说到的堆栈以及它的特性,先告诉你它被用来存放基本数据类型和REFERENCE引用,什么是引用呢,书上说它是指向OBJECT物体的东西,用它来访问具体的OBJECT,那为什么不直接访问OBJECT呢?我猜想有如下原因(不一定是事实,也不是无道理),首先是因为效率,访问堆栈的速度要比堆快,因为堆相对堆栈比较无序、无组织性,你也许要问,那为什么不把OBJECT对象放到堆栈里呢,那样不是更快么?别忘了OBJECT和自然物体一样也有属性的,属性不好理解,你就当它是体积、重量什么的吧,把OBJECT放到堆栈是不可能的,堆栈容纳不下(我猜测堆栈的大小是固定的值而且不会很大),即使能容纳下个别的OBJECT,注意OBJECT的体积可不全一样。明智的方法是用REFERENCE做个标示,假设你在一个阿拉伯国家当老师,那里的人名字可是很长的,又假使你很熟悉你的每个学生所以不会认错人并且你给他们每人一个固定的学号,上课时你要叫学生回答问题,你选择叫他们完整的全名呢(阿拉伯国家叫全名以外的外号和缩短的名字是不尊重的行为,也许吧,哈哈),还是叫他们的学号?REFERENCE引用就好比学号吧。另外还有STATIC静态内存区,是用来专门保存静态数据的,他们有着特殊的作用和意义,先不说这个。前面说到的这些还不完全,但有一点可以肯定,明确了解他们会使你更快地掌握编程语言,帮助理解和分析具体的程序,这些对学习任何一门语言来说都同样重要。

现在来说一下OVERLOAD和OVERRIDE这两个概念,不幸的是这两个非常重要的概念被许多人错误地理解了,真是要细细说道一下。首先,看这两个英文单词:OVERLOAD—
超载,过载,重载,超出标准负荷;OVERRIDE:重置,覆盖,使原来的失去效果。这两个词在外国人来看绝对不会弄混,可换了国人恐怕没几个初学者能搞得明白,问题在哪呢?我来告诉你,就在这个“重”字上,有许多人(包括我的大学C++课老师)读OVERLOAD的中文译名为—重(chong 二声,升调)载,实际上应该读(zhong 四声,降调),OVERRIDE的中文译名是—重(chong 二声,升调)置,把重载和重置混为一体了,接着就有人把二者混用,原因主要还是对二者的涵义不明晰。先说OVERLOAD,其实OVERLOAD和OVERRIDE不是什么具体的东西,二者都是机制,OVERLOAD我喜欢叫它超载,是对函数而言(如果不知道什么是函数……那你不要看了)也就是方法(JAVA的函数标准称谓),这里引用一个经典的说明:你怎么表达洗东西,是不是说洗手、洗车、洗衣服之类的?这些你平时再也熟悉不过的词语的涵义是什么你真的清楚吗?比如洗车怎么洗,洗手又怎么洗,它们是相同的过程吗?你肯用洗车的方式洗手?还是用洗衣服的方式去洗车?懂了吗,尽管你说话的时候没有明确地说我要洗我的手,用洗手的方式;或者我要用洗衣服的方式洗一件衣服。不需要那样麻烦地表达,这就是超载的意思了,具体的语法用我解释吗?好吧,OVERLOAD超载是指在同一可访问区内被声名的、几个、具有不同参数列的(参数类型/个数/顺序,不同)、同名函数,程序会根据不同的参数列来确定需要调用的函数,这种机制叫超载,超载不关心函数的返回值类型(返回值类型及其为何不能作为超载函数的判断因素在这里不描述)。OVERLOAD覆盖是指派生类(派生类这里不做描述)中存在重新定义的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖。明白了没?这两个看似生僻怪异的机制可是以后编程中经常用到的哦。

先写到这里了,提醒大家一下如果没有英语基础还是别学编程了,不然困难会很大,99%的技术文档都是英文,MICROSOFT的MSDN应该有中文版本。另外90%的优秀书籍都是英文版本,比如BRUCE ECKEL的大作 “THINKING IN XXX”系列在他的网站直接提供电子版本的免费下载,很棒的书,目前免费电子版已经出到第3版。相信国内很多人都知道“侯捷”这个台湾人,他曾翻译过前面提到的B。E。大师的“THINKING IN JAVA Second Edition”,该书口碑极佳,书中对全数的名词术语的翻译比较精准,基本表达了原作者的意思,也是我看过的JAVA书中最为出众的一本,可惜世上无完美,由于侯先生是台湾人,书中使用的词语有的偏重台湾化,尽管他已经在序中说明将尽量使用大陆的构词习惯,有些地方还是差强人意。我在这里强烈推荐中级和刚入门的朋友吃透这本书,书中的知识点阐述相当详尽(个人认为不完美,其实没有什么是完美的),函盖了大部分重点要素,是一本系统学习JAVA的好书,但要注意,该书不是面向从零开始的读者,使用者应该至少具有C或VB的语法概念,最好是懂点C,比如理解C中的数据类型概念、基本IF语句、变量声明什么的。在前面提到的TIJ 2nd版本中随书带有1CD,里面包括了B。E。所著的“THINKING IN C --foundation for learning JAVA/C++”电子版,是学习JAVA的基础。该书据说可在B。E。的网站找到,可能我笨,没找到。在这里希望大家多看英文原版书,大部分的优秀书籍都只有英文版可以看一下,因为译者的功力实在有限,有的译本简直惨不忍睹,真该扁那些出版社,没本事学什么人家出书嘛!另外,本土的作者水平普遍不高,而且书写的比较没有章法,个人感觉有的象无头苍蝇到处乱撞,细节交代不清,可能写得连作者都不知其所云,就此收笔,主次轻重不分、不从读者角度考虑、看了之后有十万个为什么的想法;有的象蜻蜓点水,什么都只触及皮毛,当作简介尚可,做教程看了也白看。大家也不要盲目地看英文书,外国也有不会念经的和尚,不要轻信书评和受欢迎度,我就看过几个据说很有水平作者的拙作,很是糟糕,有的书甚至连基本原理都有错误,比如我曾亲眼见过有本书中写到:JAVA语言里为原始数据类型分配的内存空间随具体实现的机器不同而不同。这样的书看了无异饮鸩止渴呀!建议大致看一下全书,然后对比较熟悉的几个地方仔细看一下,如果发现作者或译者有明显错误就放弃它。还有,如果一本书读了大半也觉不出什么新领悟的东西,就说明这本书内容和你掌握的重复,不要细读了。好书真的会说话,每次看到一个新的亮点就是它在和你交谈。至于那些高手嘛(再高的高手也有手软的时候)光看技术文档就可以了。这里提醒大家技术文档十分重要,一个不看技术文档就写代码的程序员永远也不是合格的程序员!希望大家多读完整无错的原代码,对编程进步很有帮助,不要着急去调程序,没会说全话就想唱歌,唱的出来么?等到需要实际动手的时候就去做,那时侯你就知道自己并不是什么都行的,实际情况和你计划好的会有出入,是不是有的标准类函数记错了呀,拿起书再看看吧。

原创粉丝点击